最大子矩形问题(学习笔记)
总结了几个求最大子矩形(最大正方形)的方法:DP,枚举障碍点,单调栈,悬线法
题意简述:在一个有障碍点的矩形中找到一个最大正方形
对于数据范围小,直接\(n^2\)算法DP
int bj[1005][1005],f[1005][1005];
int n,m,ans;
int main(){
n=read();m=read();
for(int i=1;i<=m;i++){
int x=read(),y=read();
bj[x][y]=1;
//将障碍点标记为1
}
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++){
if(bj[i][j]==0)//如果这个点不是障碍点
f[i][j]=min(min(f[i][j-1],f[i-1][j]),f[i-1][j-1])+1;
//状态转移方程是本题精髓
//为什么求最大面积会是比较最小值呢?+1是什么意思呢?
//首先明确这里f[i][j]表示的是:
//以第i行第j列为右下角顶点所能构成的最大正方形的边长;
//所以[i][j]这个点一定先要满足不是障碍点这个条件
//那么对于f[i][j]=x,意思就是:
//[i][j]向上x个节点,向左x个节点构成的正方形中无障碍点
//我们要同时满足向上和向左两个条件,所以是取最小值
ans=max(ans,f[i][j]);//更新最大边长
}
printf("%d\n",ans);
return 0;
}
进阶篇 传送门
题意简述:在一个有障碍点的矩形中找到一个最大子矩形
当矩形边长很大时,我们不能再像上面一样\(n^2\)(n:边长)处理了,此时我们可以考虑从障碍点入手,因为本题中障碍点数较小,所以可以\(n^2\)(n:障碍点数)处理
我是基于一个平面直角坐标系中的矩形来讨论的(每次这种矩形的题目,手动模拟的时候傻傻分不清长和宽)
还是在这里大致讲一下吧(下面也讲得比较详细):
1 按横坐标从小到大枚举障碍点,以每个障碍点为子矩形的一个顶点,分别向右,向左扫描其它的障碍点,同时不断更新上下界.
2 上面这样讨论可能会漏掉左右边界恰与矩形边界重合的子矩形,所以还要考虑到这种情况,可以直接按纵坐标从小到大排序预处理完成.
int L,W,n,ans;
struct cow{
int x,y;
}a[5001];
//结构体来存障碍点,方便对障碍点排序
bool cmp1(cow b,cow c){
return b.x<c.x;
}
bool cmp2(cow b,cow c){
return b.y<c.y;
}
int main(){
L=read();W=read();n=read();
for(int i=1;i<=n;i++){
a[i].x=read();
a[i].y=read();
}
a[++n].x=0;a[n].y=0;
a[++n].x=L;a[n].y=0;
a[++n].x=0;a[n].y=W;
a[++n].x=L;a[n].y=W;
//枚举的时候,可能有些子矩形直接与矩形边界重合
//所以我们不妨直接把矩形四个顶点也当做障碍点
sort(a+1,a+n+1,cmp2);
for(int i=1;i<=n-1;i++){
ans=max(ans,(a[i+1].y-a[i].y)*L);
}
//先对障碍点按纵坐标从小到大排序
//这里我们枚举的是以上下相邻两个点为上下边界,
//左右边界直接与矩形边界重合的子矩形
sort(a+1,a+n+1,cmp1);
//障碍点按横坐标从小到大排序
//以a[i]这个障碍点为边界:
for(int i=1;i<=n;i++){
int up=W,down=0,wid=L-a[i].x;
//up,down,wid分别是最大子矩形的上界,下界,最大宽度
//初始都赋为(理论上的)最大值
//从左往右扫描每个障碍点:
for(int j=i+1;j<=n;j++){
if(ans>=wid*(up-down))break;
//ans是当前面积,wid*(up-down)是理论最大面积
ans=max(ans,(up-down)*(a[j].x-a[i].x));
//更新最大面积
if(a[i].y==a[j].y)break;
//如果两个点在同一条线上,则构不成矩形
if(a[j].y>a[i].y)
up=min(up,a[j].y);
if(a[j].y<a[i].y)
down=max(down,a[j].y);
//不断调整子矩形上下边界
}
//重置上下界和最大宽度,从右往左扫描(只修改一点点):
up=W,down=0,wid=a[i].x;
for(int j=i-1;j>=1;j--){
if(ans>=wid*(up-down))break;
ans=max(ans,(up-down)*(a[i].x-a[j].x));
if(a[i].y==a[j].y)break;
if(a[j].y>a[i].y)
up=min(up,a[j].y);
if(a[j].y<a[i].y)
down=max(down,a[j].y);
}
}
printf("%d\n",ans);
return 0;
}
进阶篇 传送门
题意:在一个有障碍的矩形土地上找到一个最大的子矩形.
本题的核心思想是单调栈,是否记得单调栈的经典例题,不会这道题的戳这里.
在那道经典例题中,题目给我们了一条水平线上的很多矩形,矩形的宽度都是1,要求它们构成的最大子矩形的面积.而这道题中,我们首先拿在手上的是一个\(n*m\)的二维平面,因此我们可以将其视作在n条水平线上,每条水平线上有m个矩形.这样问题就变得和经典例题一模一样了.显而易见,这也是个\(n^2\)算法.
struct A{
int lon,wid;
}st[1005];
int n,m,len[1005][1005];
long long ans;
//单调栈求最大子矩形(这里不讲了)
void B(int x){
int top=0,width,maxn=0;
st[++top].lon=len[x][1];
st[top].wid=1;
for(int i=2;i<=m;i++){
width=0;
while(st[top].lon>=len[x][i]&&top>0){
width+=st[top].wid;
maxn=max(maxn,st[top--].lon*width);
}
st[++top].lon=len[x][i];
st[top].wid=width+1;
}
width=0;
while(top>0){
width+=st[top].wid;
maxn=max(maxn,st[top--].lon*width);
}
if(maxn>ans)ans=maxn;
}
int main(){
n=read();m=read();
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++){
char ch;cin>>ch;
if(ch=='F')len[i][j]=len[i-1][j]+1;
//转化的关键,相当于转化成了:
//每条水平线上有m个长度不等,宽度为1的矩形
}
for(int i=1;i<=n;i++)B(i);
//现在就可以直接开始对每一行分别单调栈求最大子矩形了
printf("%lld\n",ans*3);
return 0;
}
进阶篇
还是就着上面那道题,谈谈悬线法吧.悬线法,我们可以简单地理解为一根上端点是边界或者障碍点,下端点可以自由伸缩的悬线,不断左右扫描求得最大子矩形的方法.想好好研究的话,推荐上面那篇论文,真的要很耐心地看.
\(a[i][j]=1\)表示该点不是障碍点
\(l,r[i][j]\)表示该点水平方向上所能拓展到的最大的纵坐标处
\(up[i][j]\)表示该点向上所能拓展到的最远距离
理解了这三个数组,下面的代码就很好理解了.
int n,m,ans;
int a[1005][1005],l[1005][1005],r[1005][1005],up[1005][1005];
int main(){
n=read();m=read();
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++){
char ch;cin>>ch;
if(ch=='F'){
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]==1&&a[i][j-1]==1)
l[i][j]=l[i][j-1];
}
//预处理l数组
for(int i=1;i<=n;i++)
for(int j=m-1;j>=1;j--){
if(a[i][j]==1&&a[i][j+1]==1)
r[i][j]=r[i][j+1];
}
//预处理r数组
for(int i=2;i<=n;i++)
for(int j=1;j<=m;j++){
if(a[i][j]==1&&a[i-1][j]==1){
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;
}
ans=max(ans,up[i][j]*(r[i][j]-l[i][j]+1));
}
//一个max,一个min,根据数组定义来理解即可
printf("%d\n",ans*3);
return 0;
}