懸線法是一種求解給定矩陣中的極大矩陣的算法。ios
所謂「給定矩陣中的極大矩陣」是指一個矩形中有一些障礙點,求內部不包含任何障礙點且邊界與座標軸平行的子矩形。算法
咱們定義有效子矩形爲內部不包含任何障礙點且邊界與座標軸平行的子矩形。
顯然圖中第一個矩形是有效子矩形而第二個矩形不是。數組
對於一個有效子矩形,若是不存在包含它且比它大的有效子矩形則稱它爲極大有效子矩形,而最大有效子矩形爲極大有效子矩形中最大的一個(或多個)。優化
由此咱們能夠獲得定理:一個極大有效子矩形的四條邊必定不能向外擴展。也就是說,一個極大有效子矩形的一條邊,要麼與矩形邊界重合,要麼往外一格就是障礙點,這個定理的證實很顯然,若是有邊能夠向外擴展,咱們要求最大有效子矩形,必定會向外擴展以擴大面積。網站
對於此類問題,咱們顯然須要一種快速的方法求解(暴力效率極低)。spa
咱們求一個最大有效子矩形,必定是極大有效子矩形中的一個,也就是說,咱們能夠求全部極大有效子矩形,而後掃描它們,求出面積最大值。3d
因此如今問題就轉變爲了求全部的極大有效子矩形。code
咱們引入懸線的概念。咱們稱除兩個端點外不覆蓋任何障礙點的豎直線段爲有效豎線,上端點覆蓋了一個障礙點或達到整個矩形上端的有效豎線爲懸線。htm
咱們能夠發現,每個極大有效子矩形均可以由一條懸線向左右儘量的移動獲得,也就是說,咱們只要處理出每條懸線的極大有效子矩形就能夠求出全部的極大有效子矩形。
圖爲將一條懸線左右儘量移動獲得的極大子矩形。
因此,當前問題轉化成了處理全部懸線。
容易發現,懸線全部的點都是由其最下面的點決定的,因此咱們只要肯定了下面的點,整個懸線的長度也就隨之肯定。咱們利用遞推來處理。設\(up_{ij}\)表示底部點爲(i,j)的懸線長度,初始化是若是這個格子不是障礙,那麼其懸線的長度爲1,容易寫出遞推式,若是格子(i,j)和格子(i-1,j)都不是障礙的話,能夠知道\(up_{i,j}=up_{i-1,j}+1\)。
代碼:
for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) if(!a[i][j]&&!a[i-1][j]) up[i][j]=up[i-1][j]+1;
接下來咱們要作的就是處理出全部懸線能左右延伸的最長長度。若是咱們知道每一個點能左右延伸的最長長度,那麼,這個長度的最小值就是懸線能左右延伸的長度。全部咱們優先處理每一個點所能延伸的最長長度。
咱們設\(l_{i,j}\)爲點(i,j)能往左延伸的列號,即能向左延伸的最長長度的那一列,或是向左最多能擴展到第幾列,容易發現這也是能夠遞推來完成的。若是格子(i,j)和(i-1,j)都不是障礙,那麼就有\(l_{i,j}=l_{i-1,j}\),處理向右延伸的最長長度同理,設\(r_{i,j}\)爲該點能向右延伸最多到哪一列,則有\(r_{i,j}=r_{i,j+1}\),一樣格子(i,j)(i,j+1)也不是障礙。l、r數組的初始化都是若是該點不是障礙,則值爲該點的列號,尤爲注意的是,l爲從左到有更新,從第2列開始更新,不然第一列數據會被覆蓋。r爲從右到左更新,從倒數第二列開始更新,不然最後一列數據會被覆蓋。
代碼:
for(int i=1;i<=n;i++) for(int j=2;j<=m;j++) if(!a[i][j]&&!a[i][j-1]) l[i][j]=l[i][j-1]; for(int i=1;i<=n;i++) for(int j=m-1;j>=1;j--) if(!a[i][j]&&!a[i][j+1]) r[i][j]=r[i][j+1];
處理完這些以後,咱們開始下一步。
在這一步中,咱們要同時完成兩件事情,1.對l、r數組的合併,統計答案。
枚舉順序是從上到下,從左到右。
在這一步,l和r的定義發生變化,它們再也不是表示該點向左(右)最多擴展到哪一列,而是表示以該點爲最下面的點的懸線向左(右)最多擴展到哪一列。一樣,還能夠用遞推來實現。由於是最低點,因此只要與上面的點進行合併便可。
代碼:
for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ if(i>=2&&!a[i][j]&&!a[i-1][j]){ l[i][j]=Max(l[i][j],l[i-1][j]); r[i][j]=Min(r[i][j],r[i-1][j]); } ans=Max(ans,up[i][j]*(r[i][j]-l[i][j]+1)); }
在遞推的同時,要注意i爲1是不能更新l和r,不然在沒有對第0行初始化的前提下會覆蓋答案。若是存在障礙也不能更新。
值得注意的是,雖然第一行不參與遞推,但參與統計答案。答案更新的方式就是用懸線的長度乘上它左右可以擴展的最大長度。
例題:https://www.luogu.com.cn/problem/P4147
這就是個裸的最大子矩陣面積,選線法徹底能夠作。代碼:
#include<iostream> #include<cstdio> #include<cmath> #include<algorithm> #include<cstring> #include<sstream> #include<queue> #include<map> #include<vector> #include<set> #include<deque> #include<cstdlib> #include<ctime> #define dd double #define ll long long #define ld long double #define ull unsigned long long #define N 1010 #define M number using namespace std; bool a[N][N]; int up[N][N],l[N][N],r[N][N]; int n,m,ans=-1; inline int Max(int a,int b){ return a>b?a:b; } inline int Min(int a,int b){ return a>b?b:a; } int main(){ scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) for(int j=1;j<=m;j++){ char x;x=getchar(); while(x!='R'&&x!='F') x=getchar(); if(x=='R') a[i][j]=1; else a[i][j]=0,up[i][j]=1,l[i][j]=r[i][j]=j; } for(int i=1;i<=n;i++) for(int j=2;j<=m;j++) if(!a[i][j]&&!a[i][j-1]) l[i][j]=l[i][j-1]; for(int i=1;i<=n;i++) for(int j=m-1;j>=1;j--) if(!a[i][j]&&!a[i][j+1]) r[i][j]=r[i][j+1]; for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) if(!a[i][j]&&!a[i-1][j]) up[i][j]=up[i-1][j]+1; for(int i=1;i<=n;i++){ for(int j=1;j<=m;j++){ if(i>=2&&!a[i][j]&&!a[i-1][j]){ l[i][j]=Max(l[i][j],l[i-1][j]); r[i][j]=Min(r[i][j],r[i-1][j]); } ans=Max(ans,up[i][j]*(r[i][j]-l[i][j]+1)); } } printf("%d\n",ans*3); return 0; }
根據上面的代碼,不可貴出懸線法的時間複雜度:O(nm)。
新增長一道例題,這是一種只能用懸線法作的最大子矩陣問題,由於它的障礙點是「不固定的」,對於不一樣的點來講,障礙點不一樣,但仍是能夠在稍稍變形的基礎上,用懸線法過掉:
例題:https://www.luogu.com.cn/problem/P1169
代碼:(後面有提示)
#include<iostream> #include<cstdio> #include<cmath> #include<algorithm> #include<cstring> #include<sstream> #include<queue> #include<map> #include<vector> #include<set> #include<deque> #include<cstdlib> #include<ctime> #define dd double #define ll long long #define ld long double #define ull unsigned long long #define N 2010 #define M number using namespace std; int n,m,a[N][N],ans1,ans2;//1: zheng 2.ju int up[N][N],l[N][N],r[N][N]; inline int Max(int a,int b){ return a>b?a:b; } inline int Min(int a,int b){ return a>b?b:a; } int main(){ scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) for(int j=1;j<=m;j++){ scanf("%d",&a[i][j]); up[i][j]=1;l[i][j]=r[i][j]=j; } for(int i=2;i<=n;i++) for(int j=1;j<=m;j++) if(a[i][j]^a[i-1][j]) up[i][j]=up[i-1][j]+1; for(int i=1;i<=n;i++) for(int j=2;j<=m;j++) if(a[i][j]^a[i][j-1]) l[i][j]=l[i][j-1]; for(int i=1;i<=n;i++) for(int j=m-1;j>=1;j--) if(a[i][j]^a[i][j+1]) r[i][j]=r[i][j+1]; for(int i=1;i<=n;i++) for(int j=1;j<=m;j++){ // printf("i:%d j:%d l:%d r:%d\n",i,j,l[i][j],r[i][j]); if(i!=1&&a[i][j]^a[i-1][j]){ l[i][j]=Max(l[i][j],l[i-1][j]); r[i][j]=Min(r[i][j],r[i-1][j]); } int juli=r[i][j]-l[i][j]+1; int minn=Min(juli,up[i][j]); ans1=Max(ans1,minn*minn); ans2=Max(ans2,juli*up[i][j]); } printf("%d\n%d",ans1,ans2); return 0; }
這裏利用了異或"^"的一個性質,相同則爲0,不相同則爲1,異或是不進位的加法。
可是,若是n和m很大,但障礙數又很小的狀況下,其實咱們統計了許多對答案沒有貢獻的狀況。這個答案還不夠優。
聽說能夠用離散化對懸線法進行優化,可是1來我不會用離散化,2來加上離散化後的懸線法(據某些博客上說)還不如用下一種方法:
昨天(2月4號)寫的懸線法,今日去找了洛谷上一個題打了一下掃描法,發現這麼多Hack數據直接把我嚇蒙了。。。聽說洛谷加了hack數據後直接卡掉了80%的代碼
話歸正題。由於沒有找到寫掃描法的好博客(能上日報的那種),因此只能本身敲。(可是圖仍是能夠借一下的)
一下引用一大段文字,看不懂的一些的地方(我認爲的)後面會有一些講解。
算法的思路是這樣的,先枚舉極大子矩形的左邊界,而後從左到右依次掃描每個障礙點,並不斷修改可行的上下邊界,從而枚舉出全部以這個定點爲左邊界的極大子矩形。考慮如圖2中的三個點,如今咱們要肯定全部以1號點爲左邊界的極大矩形。先將1號點右邊的點按橫座標排序。而後按從左到右的順序依次掃描1號點右邊的點,同時記錄下當前的可行的上下邊界。
開始時令當前的上下邊界分別爲整個矩形的上下邊界。而後開始掃描。第一次遇到2號點,以2號點做爲右邊界,結合當前的上下邊界,就獲得一個極大子矩形(如圖3)。
同時,因爲所求矩形不能包含2號點,且2號點在1號點的下方,因此須要修改當前的下邊界,即以2號點的縱座標做爲新的下邊界。第二次遇到3號點,這時以3號點的橫座標做爲右邊界又能夠獲得一個知足性質1的矩形(如圖4)。4.png相似的,須要相應地修改上邊界。以此類推,若是這個點是在當前點(肯定左邊界的點)上方,則修改上邊界;若是在下方,則修改下邊界;若是處在同一行,則可停止搜索(由於後面的矩形面積都是0了)。因爲已經在障礙點集合中增長了整個矩形右上角和右下角的兩個點,因此不會遺漏右邊界與整個矩形的右邊重合的極大子矩形(如圖5)
須要注意的是,若是掃描到的點不在當前的上下邊界內,那麼就不須要對這個點進行處理。
這樣作是否將全部的極大子矩形都枚舉過了呢?能夠發現,這樣作只考慮到了左邊界覆蓋一個點的矩形,所以咱們還須要枚舉左邊界與整個矩形的左邊界重合的狀況。這還能夠分爲兩類狀況。一種是左邊界與整個舉行的左邊界重合,而右邊界覆蓋了一個障礙點的狀況,對於這種狀況,能夠用相似的方法從右到左掃描每個點做爲右邊界的狀況。另外一種是左右邊界均與整個矩形的左右邊界重合的狀況,對於這類狀況咱們能夠在預處理中完成:先將全部點按縱座標排序,而後能夠獲得以相鄰兩個點的縱座標爲上下邊界,左右邊界與整個矩形的左右邊界重合的矩形,顯然這樣的矩形也是極大子矩形,所以也須要被枚舉到。
經過前面兩步,能夠枚舉出全部的極大子矩形。算法1的時間複雜度是O(S2)。這樣,能夠解決大多數最大子矩形和相關問題了。
以上文字均引用於:王知昆《淺談用極大化思想解決最大子矩形問題》
網站:http://www.javashuo.com/article/p-nvhfmohf-vd.html
想看論文原版的能夠去百度文庫上搜,能搜到。值得一提的是,王知昆dalao的代碼有鍋。
這並非個人一家之言,洛谷的例題好多人推翻了王知昆大佬的代碼。不過這並不重要。
接下來我簡單說一說這個代碼要注意到的地方。
1.要掃兩遍,從左到右一遍,從右到左一遍。
2.值得注意的是,咱們枚舉的左邊這個邊界(假設咱們如今正在從左往右掃),根據上面的算法講解,必定是根據,某個障礙點枚舉的,這個障礙點必定要在這個邊界上,堵住這個邊界向左延伸,這也是爲何王知昆同志說道「,若是這個點是在當前點(肯定左邊界的點)上方,則修改上邊界;若是在下方,則修改下邊界;」,有可能修改另外一個邊界更優,可是由於若是你枚舉另外一條邊界,你的左邊界上定然沒有障礙點來阻礙。因此它能夠向左延伸,直到它遇到了另外一個障礙點或是碰到了矩形的左邊界,可是不管是哪一種狀況,都會被咱們已經枚舉到的,或是將要枚舉到的,跟如今咱們枚舉的狀況,沒有關係。
3.可能有些同窗不是很理解,爲何說「若是處在同一行,則可停止搜索(由於後面的矩形面積都是0了)」,其實我也沒有理解括號裏王知昆的意思,可是這種方法是正確的,咱們不妨設想有這樣一個在同一行的點,不管咱們提下邊界,仍是更新上邊界,咱們的左邊界在上下邊界更新完後,必定不包括障礙點,即便這個障礙點在這個咱們枚舉的矩陣的左上角或右下角,這個矩陣仍然能夠向左繼續延伸,從而會被其它狀況所枚舉。
事實上,我的以爲懸線法比掃描法要簡單,並且掃描法細節更多。
例題:https://www.luogu.com.cn/problem/P1578
#include<iostream> #include<cstdio> #include<cmath> #include<algorithm> #include<cstring> #include<sstream> #include<queue> #include<map> #include<vector> #include<set> #include<deque> #include<cstdlib> #include<ctime> #define dd double #define ll long long #define ld long double #define ull unsigned long long #define N 5010 #define M number using namespace std; struct point{ int x,y; }; point a[N]; int n,m,q,ans; inline int Min(int a,int b){ return a>b?b:a; } inline int Max(int a,int b){ return a>b?a:b; } inline bool cmp1(point a,point b){ if(a.x!=b.x) return a.x<b.x; else return a.y<b.y; } inline bool cmp2(point a,point b){ if(a.y!=b.y) return a.y<b.y; else return a.x<b.x; } inline void t_left(int k){ int maxup=0,maxdown=n; for(int i=k-1;i>=0;i--){ if(a[i].y==a[k].y) continue; if((a[i].x<=maxup||a[i].x>=maxdown)&&i!=0) continue; ans=Max(ans,(a[k].y-a[i].y)*(maxdown-maxup)); if(a[i].x==a[k].x) break; if(a[i].x<a[k].x) maxup=Max(maxup,a[i].x); else maxdown=Min(maxdown,a[i].x); } } inline void t_right(int k){ int maxup=0,maxdown=n; for(int i=k+1;i<=q+1;i++){ if(a[i].y==a[k].y) continue; if((a[i].x<=maxup||a[i].x>=maxdown)&&i!=q+1) continue; ans=Max(ans,(a[i].y-a[k].y)*(maxdown-maxup)); if(a[i].x==a[k].x) break; if(a[i].x<a[k].x) maxup=Max(maxup,a[i].x); else maxdown=Min(maxdown,a[i].x); } } int main(){ scanf("%d%d%d",&n,&m,&q); for(int i=1;i<=q;i++) scanf("%d%d",&a[i].x,&a[i].y); sort(a+1,a+q+1,cmp1); int maxx=0; a[0].y=0;a[0].x=0;a[q+1].y=m;a[q+1].x=n; for(int i=1;i<=q+1;i++) maxx=Max(maxx,a[i].x-a[i-1].x); ans=maxx*m; sort(a+1,a+q+1,cmp2); for(int i=1;i<=q;i++){ t_left(i); t_right(i); } printf("%d\n",ans); return 0; }
注意1.要加上兩個障礙點:最左上角和最右下角。 緣由就是咱們枚舉每個點最終都是到了左邊界或右邊界,因此加上了這兩個點,至於這兩個點在不在咱們極大子矩陣的周圍到是可有可無,在掃描時咱們只用到他們的y值,而又給他們加上x值得緣由是咱們預處理一遍左右邊都在左右邊界上的最大子矩陣。在排除上下邊界之外的點時,必定要注意排除咱們新加上去的兩個障礙點,畢竟這兩個點表明的是邊界。