最大子矩阵问题&&悬线法 学习小结
最近在写dp的题目
但是我这个同学又又又又生病了
学习了一下多叉树的背包问题和最大子矩阵的问题,还有攒了几道期望,先总结一下矩阵问题;
问题模型:
在一个给定的矩形中有一些障碍点,找出内部不包含障碍点的(或者黑白相间)、轮廓与整个矩形平行或重合的最大子矩形。
//根据问题我们改动程序,我们现在学习这种解决问题的思想;
有关论文:王知昆《浅谈用极大化思想解决最大子矩阵问题》;
那么我们先来讲一下什么是极大化思想;
先介绍一下子矩阵的概念;
有效子矩形:内部不包含障碍点的(或者满足黑白相间)、轮廓与整个矩形平行或重合的子矩形。
极大子矩形:每条边都不能向外扩展的有效子矩形。
最大子矩形:所有有效子矩形中最大的一个(或多个)。
满足:
在一个有障碍点的矩形中最大子矩形一定是极大子矩形。
枚举所有的极大子矩形,找到最大子矩形。
设NM分别为整个矩形的长和宽,S为内部的障碍点数;
我们知道极大子矩阵一定不能再向外扩展,所以极大子矩阵的边界要么为障碍点所处位置,要么与位于整个矩阵的边界;
我们可以枚举左右上下四个边界,然后判断组成的矩阵是否具有有效子矩阵;
但是枚举了很多无效的子矩阵,复杂度(S^5)
我们可以枚举左右边界,然后将边界内的点排序,每个相邻的点左右边界组成一个矩阵; 我们将复杂度降至(S^3)
但是我们枚举中一部分不是极大子矩阵;
我们要做的就是让枚举的每一个矩阵都是有效的并且是极大的;
我们可以设计以下算法:
1/将所有点按照横坐标排序,并编号1,2,3.....n;
2/枚举每一个点作为左边界,设定此时上下边界为矩阵的上下边界;
3.扫描后面的点作为右边界,确定一个极大子矩阵,根据纵坐标的关系,我们修改当前的上下边界;
4.以此类推,枚举所有的点;
但是我们还要额外考虑一些情况:
1.矩形的左边界和矩阵的左边界重合;
这依旧可以分类讨论(1),左边界与矩阵左边界重合,而右边界覆盖了一个障碍点,我们可以从右向左是扫描,把当前点作为右边界的情况;
(2)左右边界都与矩阵的左右边界重合,我们可以预处理出来,具体做法就是按照纵坐标从小到达排序,相邻两个点纵坐标上下边界为上下边界,而矩阵左右边界作为边界,
这也是我们需要枚举到的;
但是这个题适用于障碍点较少的矩阵中,代码容易;时间复杂度(NM) 空间复杂度(NM)
除了这种方法,对于障碍点比较密集的情况,我们可以采取悬线法解决;
我们定义 :
有效竖线:对于上下两个端点不包含障碍点的竖直直线;
悬线:上端覆盖了一个障碍点或者到达整个矩形上边界的有效线段。
每个悬线都与它底部的点一一对应,矩形中的每一个点(矩形顶部的点除外)都对应了一个悬线。
悬线的个数=(N-1)*M;
对于一个极大子矩阵按照横坐标不同我们切割多个与y轴平行的线段,那么其中至少有一个悬线;
我们将一个悬线对应向左右拓展,就得到一个矩阵,悬线对应的矩阵不一定是极大矩阵,下坐标可能拓展;
所以极大矩阵个数<=悬先个数;
我们枚举每一个悬线,找到极大子矩阵;
我们设left[i][j]为(i,j)对应的悬线做多能向左拓展的位置;
同样的,right[i][j]为(i,j)对应的悬线做多能向右拓展的位置;
up[i][j]为]为(i,j)对应的悬线的长度;
我们考虑(i,j)和(i-1,j)的对应关系;
如果(i-1,j)是障碍点,那么(i,j)对应悬线长度为1,左右边界为整个矩阵的左右边界;
如果(i-1,j)步是障碍点,那么(i,j)对应的悬线长度为up[i][j]=up[i-1][j]+1,对应我们改变left[i][j]为max(left[i][j],left[i-1][j]),right=min(right[i][j],right[i-1][j]);
注意初始化;以下代码是寻找全为0的最大矩阵,1为障碍点;
for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) l[i][j]=r[i][j]=j; up[i][j]=1; for(int i=1;i<=n;i++) for(int j=2;j<=n;j++) if(a[i][j]==0&&a[i][j-1]==0) l[i][j]=l[i][j-1]; for(int i=1;i<=n;i++) for(int j=n-1;j>0;j--) if(a[i][j]==0&&a[i][j+1]==0) r[i][j]=r[i][j+1];
实现起来比较容易,我们接下来看几道例题;
luogu1578奶牛浴室
这里用到第一种极大化思想,按照前面所讲的排序,我中间对于j特判了==n和==1的情况,没有的话会wa一个点,也就是极大化矩阵是左右边界的情况,其实后来有的题解并没有这一点
有待解决这个问题,而且洛谷第一篇题解我测了以下一个数据,被hack掉了;
题目第是11组数据:
6 4
4
1 2
4 1
4 3
2 1
输出应该是10
但是第一篇题解是9;
#include<bits/stdc++.h> using namespace std; template<typename T>inline void read(T &x) { x=0;T f=1,ch=getchar(); while(!isdigit(ch)) {if(ch=='-') f=-1;ch=getchar();} while(isdigit(ch)) {x=(x<<1)+(x<<3)+(ch^48);ch=getchar();} x*=f; } int L,H,n; struct node { int x,y; } a[5010]; inline bool cmp1(node A,node B) { if(A.x!=B.x) { return A.x<B.x; } else { return A.y<B.y; } } inline bool cmp2(node A,node B) { if(A.y!=B.y) { return A.y<B.y; } else { return A.x<B.x; } } int main() { read(L); read(H); read(n); for(int i=1;i<=n;i++) { read(a[i].x); read(a[i].y); } a[++n].x=0,a[n].y=0; a[++n].x=0,a[n].y=H; a[++n].x=L,a[n].y=0; a[++n].x=L,a[n].y=H; sort(a+1,a+1+n,cmp1); int ans=0; for(int i=1;i<=n;i++) { int l=0,h=H,maxl=L-a[i].x; for(int j=i+1;j<=n;j++) { if(j==n) {//这个特判不写不过第11组?? ans=max(ans,maxl*(h-l)); } else { if(a[j].y<=h&&a[j].y>=l) { if(maxl*(h-l)<=ans) { break; } ans=max(ans,(a[j].x-a[i].x)*(h-l)); if(a[j].y==a[i].y) { break; } if(a[j].y>a[i].y) { h=min(h,a[j].y); } else { l=max(l,a[j].y); } } } } l=0,h=H,maxl=a[i].x; for(int j=i-1; j>=1; j--) { if(a[j].y<=h&&a[j].y>=l) { if(maxl*(h-l)<=ans) { break; } ans=max(ans,(a[i].x-a[j].x)*(h-l)); if(a[j].y==a[i].y) { break; } if(a[j].y>a[i].y) { h=min(h,a[j].y); } else { l=max(l,a[j].y); } } if(j==1) { ans=max(ans,maxl*(h-l)); } } } sort(a+1,a+1+n,cmp2); for(int i=1;i<n;i++) { ans=max(ans,(a[i+1].y-a[i].y)*L); } printf("%d",ans); return 0; }
给定矩阵,寻找一个黑白相间最大矩形和最大正方形;
那么我们只需要特判不等于就可以了,这里利用了悬线法;
代码也是比较方便好写,我个人比较喜欢这一种;
#include<bits/stdc++.h> using namespace std; template<typename T>inline void read(T &x) { x=0; T f=1,ch=getchar(); while (!isdigit(ch)) {if(ch=='-') f=-1; ch=getchar();} while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48), ch=getchar(); x*=f; } const int N=2010; int n,m; int a[N][N],l[N][N],r[N][N],up[N][N],ans1,ans2; int main() { read(n); read(m); for(int i=1;i<=n;i++) { for(int j=1;j<=m;j++) { read(a[i][j]); l[i][j]=r[i][j]=j; up[i][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>0;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(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=r[i][j]-l[i][j]+1; int b=min(a,up[i][j]); ans1=max(ans1,b*b); ans2=max(ans2,a*up[i][j]); } } cout<<ans1<<endl<<ans2<<endl; return 0; }
寻找最大全0矩形,裸题,和上题目一样;按照题目要求答案*3;
#include<bits/stdc++.h> using namespace std; int n,m,a[1010][1010],l[1010][1010],r[1010][1010],up[1010][1010]; char ch[1010][1010]; template<typename T>inline void read(T &x) { x=0;T f=1,ch=getchar(); while(!isdigit(ch)) {if(ch=='-') f=-1;ch=getchar();} while(isdigit(ch)) {x=(x<<1)+(x<<3)+(ch^48);ch=getchar();} x*=f; } int main() { //freopen("1.in","r",stdin); read(n); read(m); for(int i=1;i<=n;i++) { for(int j=1;j<=m;j++) { cin>>ch[i][j]; if(ch[i][j]=='F') a[i][j]=0; else a[i][j]=1; l[i][j]=r[i][j]=j; up[i][j]=1; } } for(int i=1;i<=n;i++) for(int j=2;j<=m;j++) if(a[i][j]==0&&a[i][j-1]==0) l[i][j]=l[i][j-1]; for(int i=1;i<=n;i++) for(int j=m-1;j>0;j--) if(a[i][j]==0&&a[i][j+1]==0) r[i][j]=r[i][j+1]; int ans=0; for(int i=1;i<=n;i++) { for(int j=1;j<=m;j++){ if(i>1&&a[i][j]==0&&a[i-1][j]==0){ 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; }
裸题,寻找没有障碍点,这大概好几倍经验了;是正方形;
#include<bits/stdc++.h> using namespace std; int n,m,T,x,y,a[1010][1010],l[1010][1010],r[1010][1010],up[1010][1010]; template<typename T>inline void read(T &x) { x=0;T f=1,ch=getchar(); while(!isdigit(ch)) {if(ch=='-') f=-1;ch=getchar();} while(isdigit(ch)) {x=(x<<1)+(x<<3)+(ch^48);ch=getchar();} x*=f; } int main() { //freopen("1.in","r",stdin); read(n); read(T); memset(a,0,sizeof(a)); for(int i=1;i<=T;i++) { read(x); read(y); a[x][y]=1; } for(int i=1;i<=n;i++) { for(int j=1;j<=n;j++) { l[i][j]=r[i][j]=j; up[i][j]=1; } } for(int i=1;i<=n;i++) for(int j=2;j<=n;j++) if(a[i][j]==0&&a[i][j-1]==0) l[i][j]=l[i][j-1]; for(int i=1;i<=n;i++) for(int j=n-1;j>0;j--) if(a[i][j]==0&&a[i][j+1]==0) r[i][j]=r[i][j+1]; int ans=0; for(int i=1;i<=n;i++) { for(int j=1;j<=n;j++) { if(i>1&&a[i][j]==0&&a[i-1][j]==0){ 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; } int a=r[i][j]-l[i][j]+1; int b=min(a,up[i][j]); ans=max(ans,b); } } cout<<ans<<endl; return 0; }
和棋盘制作一样,再加一倍经验;
#include<bits/stdc++.h> using namespace std; template<typename T>inline void read(T &x) { x=0; T f=1,ch=getchar(); while (!isdigit(ch)) {if(ch=='-') f=-1; ch=getchar();} while (isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48), ch=getchar(); x*=f; } const int N=2010; int n,m; int a[N][N],l[N][N],r[N][N],up[N][N],ans1,ans2; int main() { read(n); read(m); for(int i=1;i<=n;i++) { for(int j=1;j<=m;j++) { read(a[i][j]); l[i][j]=r[i][j]=j; up[i][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>0;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(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=r[i][j]-l[i][j]+1; int b=min(a,up[i][j]); ans1=max(ans1,b); //ans2=max(ans2,a*up[i][j]); } } cout<<ans1; return 0; }
我认为是很好理解的,代码也很好写;