求极大子矩阵的两种方法
例1:玉蟾宫
一句话题意:给出一个元素有R和F两种值的矩阵,求全为F的面积最大的子矩阵的面积。
关于这种求极大子矩阵的问题,比较常用的(本蒟蒻会的)有两种:
(1)悬线法
/*以下摘自luogu某dalao的解说(略有改动)
用途:
解决给定矩阵中满足条件的最大子矩阵
做法:
用一条线(横竖貌似都行)左右移动直到不满足约束条件或者到达边界
定义几个东西:
left[i][j]:代表从(i,j)能到达的最左位置
right[i][j]:代表从(i,j)能到达的最右位置
up[i][j]:代表从(i,j)向上扩展最长长度.
递推公式:
left[i][j]=max(left[i][j],left[i-1][j])
right[i][j]=min(right[i][j],right[i-1][j])
up[i][j]=up[i-1][j]+1
当前矩阵的面积S=长*宽(高)=(r[i][j]-l[i][j]+1)*(up[i][j])
答案
即所有S中的最大值
至于为什么递推公式中考虑上一层的情况?
是因为up数组的定义,up数组代表向上扩展最长长度, 所以需要考虑上一层的情况.
摘抄结束qwq*/
玉蟾宫code(1)
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<iostream>
using namespace std;
const int N=1005;
inline int read(){
int X=0,w=0;char ch=0;
while(!isdigit(ch)) w|=ch=='-',ch=getchar();
while(isdigit(ch)) X=(X<<1)+(X<<3)+(ch^48),ch=getchar();
return w?-X:X;
}
inline int readc(){
char c=getchar();
while(c!='R'&&c!='F')c=getchar();
if(c=='F')return 1;
return 0;
}
int n,m,ans1,ans,map[N][N];
int l[N][N],r[N][N],up[N][N];
int main(){
register int i,j;
n=read(),m=read();
for(i=1;i<=n;i++)
for(j=1;j<=m;j++){
map[i][j]=readc();
if(map[i][j])up[i][j]=1,r[i][j]=l[i][j]=j;
}
for(i=1;i<=n;i++)
for(j=2;j<=m;j++)
if(map[i][j]==1&&map[i][j-1]==1)
l[i][j]=l[i][j-1];
for(i=1;i<=n;i++)
for(j=m-1;j>0;j--)
if(map[i][j]==1&&map[i][j+1]==1)
r[i][j]=r[i][j+1];
for(i=1;i<=n;i++)
for(j=1;j<=m;j++){
if(i>1&&map[i][j]==1&&map[i-1][j]==1){
r[i][j]=min(r[i][j],r[i-1][j]);
l[i][j]=max(l[i][j],l[i-1][j]);
up[i][j]=up[i-1][j]+1;
}
ans=max(ans,(r[i][j]-l[i][j]+1)*up[i][j]);
}
cout<<ans*3<<endl;
return 0;
}
代码应该比较好懂就没注释了。。。
(2)单调栈
我先假定你会单调栈(如果不会的推荐看这里),我们知道利用单调栈,可以找到数列中任意元素中从左/右遍历第一个比它小/大的元素的位置,利用这一性质能解决的比较经典的问题有求直方图中最大矩形的面积。
那么这道题其实也可以转化成求直方图中最大矩形的面积,具体做法是:
- 预处理s[i][j]表示(i,j)向上有几个连续的"F"
- 用单调栈统计两个数组:l[i][j]表示s[i][j]的左边第一个比s[i][j]小的位置,r[i][j]表示s[i][j]的右边第一个比s[i][j]小的位置
- 那么此时矩形的面积就是(r[i][j]-1-l[i][j])*s[i][j]
- 返回到2.,如此对每一行处理
对于第一步,可以想象处理出了许多条竖直的线段。处理到第i行就以第i行为下界与竖直的线段构成了一个直方图,然后第2、3步就直接套用求直方图中最大矩形的面积的做法就行了,s就相当于高度,l和r分别是左右两边能控制的极位置。
玉蟾宫code(2)
#include<cstdio> #include<cstring> #include<stack> #include<algorithm> #include<iostream> #define lop(i,s,t) for(register int (i)=(s);(i)<=(t);++(i))//我懒到写宏定义了 。。。 #define nop(i,s,t) for(register int (i)=(s);(i)>=(t);--(i))//应该不算太丑吧。。。 using namespace std; const int N=1005; inline bool getc(){ char ch=0; while(ch^'R'&&ch^'F') ch=getchar(); return ch=='F'; } bool mp[N][N]; int n,m,ans,s[N][N],l[N][N],r[N][N]; stack<int> st; inline void init(){ lop(i,1,n) lop(j,1,m) if(mp[i][j]=getc()) //如果(i,j)合法,那么它可以向上延伸(或者说可以接在上面的竖线之下) s[i][j]=s[i-1][j]+1; } inline void clear(){while(!st.empty()) st.pop();} //没办法,stack没有clear() inline void solve(){ lop(i,1,n){ clear(); //不要忘了清空栈 lop(j,1,m){ while(!st.empty()&&s[i][j]<=s[i][st.top()]) st.pop(); if(st.empty()) l[i][j]=0; else l[i][j]=st.top(); st.push(j); } clear(); nop(j,m,1){ while(!st.empty()&&s[i][j]<=s[i][st.top()]) st.pop(); if(st.empty()) r[i][j]=m+1; //边的边界定为m+1 else r[i][j]=st.top(); st.push(j); } } lop(i,1,n) lop(j,1,m) if(s[i][j]) //有高度的才能统计答案,其实也可以不加,反正没有高度的话乘出来是0不影响答案 ans=max(ans,(r[i][j]-1-l[i][j])*s[i][j]); } int main() { cin>>n>>m; init(); solve(); cout<<ans*3<<endl; return 0; }
两种方法的对比
单调栈和悬线法在理论上时间复杂度差不多,都是O(4*mn)的样子,但是code(2)常数惊人,luogu上跑1000多ms,而code(1)只用了200多ms,这可能是我用了STL栈的缘故(明明是因为人菜)。。。
例2:棋盘制作
一句话题意:给一个01矩阵,定义合法子矩阵为任意相邻元素的颜色都不相同的子矩阵(即国际象棋棋盘的模样),求面积最大的正方形合法子矩阵和面积最大的合法子矩阵的面积(当然两者可以相同)。
可以发现这道题只是判断合法的条件成了相邻元素的颜色不能相同而已。
解法(1)
判断是否合法的时候改用!=就行了
解法(2)
可以发现对于图上所有的元素一定属于以下两种类型:
1.行列奇偶性相同的黑格 || 行列奇偶性不同的白格
2.行列奇偶性相同的白格 || 行列奇偶性不同的黑格
那么在输入的时候属于第一种情况的赋1,属于第二种情况的赋0
这样就把问题转换成了求最大同色矩阵(想一想为什么),注意要统计两遍,分别是同为1和同为0的矩阵,比较方便的做法是第一遍统计为1的矩阵,全部元素取反后再统计一遍为1的矩阵。
这里给出解法(1)的code
#include<cstdio> #include<cstring> #include<cctype> #include<algorithm> #include<iostream> #define lop(i,s,t) for(register int (i)=(s);(i)<=(t);++(i)) #define nop(i,s,t) for(register int (i)=(s);(i)>=(t);--(i)) using namespace std; const int N=2005; inline int read(){ int X=0,w=0;char ch=0; while(!isdigit(ch)) w|=ch=='-',ch=getchar(); while(isdigit(ch)) X=(X<<1)+(X<<3)+(ch^48),ch=getchar(); return w?-X:X; } int n,m,ans1,ans2,a[N][N],l[N][N],r[N][N],up[N][N]; int main() { n=read(),m=read(); lop(i,1,n) lop(j,1,m){ a[i][j]=read(); l[i][j]=r[i][j]=j; up[i][j]=1; } lop(i,1,n) lop(j,2,m) if(a[i][j]!=a[i][j-1]) l[i][j]=l[i][j-1]; lop(i,1,n) nop(j,m-1,1) if(a[i][j]!=a[i][j+1]) r[i][j]=r[i][j+1]; lop(i,1,n) lop(j,1,m) 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]); up[i][j]=up[i-1][j]+1; int a=up[i][j],b=r[i][j]-l[i][j]+1,c=min(a,b); ans1=max(ans1,c*c),ans2=max(ans2,a*b); //顺便统计了正方形 } cout<<ans1<<endl<<ans2<<endl; return 0; }
注意一点
你也许会发现,例1用的悬线法与例2用的悬线法在预处理时略有不同,例1里多了一个判断:
if(map[i][j]) up[i][j]=1,r[i][j]=l[i][j]=j;
这是因为两题的题设略有不同,例2里点的合法性是取决于元素间的相对位置,也就是说黑点白点都有可能有贡献,但例1是只有‘F’点才可能有贡献,‘R’点是绝对不可能有贡献的,因此只初始化‘F’点。不信你把那个判断去掉然后输入一个全为‘R’的矩阵,程序会输出非0值。
2018-10-29