【小结】最大子矩形问题

P4147 玉蟾宫

先预处理出 \((i,j)\) 点最多可以往上延伸的数量。记为 \(up_{i,j}\)

枚举每一行,然后对于这一行,这个矩阵的高度肯定是这一行的某个点的 \(up\) 值。

所以对于每个点单调栈跑出前面第一个碰到的比它小的点和后面第一个碰到的比它小的点。也就是从左往右扫一次,从右往左扫一次。

然后就可以枚举这一行上面的点,因为处理出了左边的范围和右边的范围,所以可以 \(S=\max(S,up_{i,j}\times (R_j-L_j+1))\),其中 \(L_i\)\(R_i\) 分别表示左边延伸的范围和右边延伸的范围。

注意细节。单调栈从左往右扫的时候推一个 \(0\) 进去。从右往左扫的时候推一个 \(m+1\) 进去。因为边界限制是 \([1,m]\)

时间复杂度:\(\mathcal O(nm)\)

代码很简洁。

#include<bits/stdc++.h>
using namespace std;
/*

*/
struct FSI{
	template<typename T>
	FSI& operator >> (T &res){
		res=0;T f=1;char ch=getchar();
		while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
		while (isdigit(ch)){res=res*10+ch-'0';ch=getchar();}
		res*=f;
		return *this;
	}
}scan;
const int N=1010;
int n,m,i,j;
char a[N][N];
int up[N][N];
int st[N],tp;
int L[N],R[N],S;
int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;
	for (i=1;i<=n;i++)
	{
		for (j=1;j<=m;j++) cin>>a[i][j];
	}
	for (i=1;i<=n;i++)
	{
		for (j=1;j<=m;j++)
		{
			if (a[i][j]=='F') up[i][j]=up[i-1][j]+1;
			else up[i][j]=0;
		}
	}
	for (i=1;i<=n;i++)
	{
		tp=0;
		st[++tp]=0;
		for (j=1;j<=m;j++)
		{
			while (tp&&up[i][st[tp]]>=up[i][j]) tp--;
			L[j]=st[tp]+1;
			st[++tp]=j;
		}
		tp=0;
		st[++tp]=m+1;
		for (j=m;j>=1;j--)
		{
			while (tp&&up[i][st[tp]]>=up[i][j]) tp--;
			R[j]=st[tp]-1;
			st[++tp]=j;
		}
		for (j=1;j<=m;j++) S=max(S,up[i][j]*(R[j]-L[j]+1));
	}
	cout<<3*S;
	return 0;
}

P1387 最大正方形

典。暴力可以过。但是是一道经典 DP。

首先 \(dp_{i,j}\) 表示以 \((i,j)\) 作为右下角的正方形的边长最大值。

就是说一个 \((i,j)\) 的正方形可以从三个点转移过来。

转移方程就是 \(dp_{i,j}=\min(dp_{i-1,j-1},dp_{i,j-1},dp_{i-1,j})+1\)

答案为 \(dp_{i,j}\) 的最大值。

没了。

时间复杂度:\(\mathcal O(nm)\)

#include<bits/stdc++.h>
using namespace std;
/*

*/
struct FSI{
	template<typename T>
	FSI& operator >> (T &res){
		res=0;T f=1;char ch=getchar();
		while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
		while (isdigit(ch)){res=res*10+ch-'0';ch=getchar();}
		res*=f;
		return *this;
	}
}scan;
const int N=110;
int n,m,i,j,a[N][N],dp[N][N],ans;
int main()
{
	scan>>n>>m;
	for (i=1;i<=n;i++)
	{
		for (j=1;j<=m;j++) scan>>a[i][j];
	}
	for (i=1;i<=n;i++)
	{
		for (j=1;j<=m;j++) 
		{
			if (a[i][j]) dp[i][j]=min(dp[i-1][j-1],min(dp[i-1][j],dp[i][j-1]))+1;
		}
	}
	for (i=1;i<=n;i++)
	{
		for (j=1;j<=m;j++) ans=max(ans,dp[i][j]);
	}
	printf("%d\n",ans);
	return 0;
}

最大正方形II

那这道题就是求交错正方形的边长最大值。

\(1 \leq n,m \leq 1500\),必须 DP 了。

那还是 \(dp_{i,j}\) 表示右下角为 \((i,j)\) 的正方形的边长的最大值。

\(a_{i,j}=a_{i,j-1}\) 或者 \(a_{i,j}=a_{i-1,j}\),那不完了,只要拓展到 \(2\) 的边长,就不满足要求。所以根据单调性,大于等于 \(2\) 的都不满足。而 \(1\) 又满足。所以这种情况 \(dp_{i,j}\) 显然为 \(1\)

那另一种情况,还是一样的转移方程。

答案也一样。

注意状态的初始化。

时间复杂度:\(\mathcal O(nm)\)

#include<bits/stdc++.h>
using namespace std;
/*

*/
struct FSI{
	template<typename T>
	FSI& operator >> (T &res){
		res=0;T f=1;char ch=getchar();
		while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
		while (isdigit(ch)){res=res*10+ch-'0';ch=getchar();}
		res*=f;
		return *this;
	}
}scan;
const int N=1510;
int n,m,i,j,a[N][N],dp[N][N],ans;
int main()
{
	scan>>n>>m;
	for (i=1;i<=n;i++)
	{
		for (j=1;j<=m;j++) scan>>a[i][j];
	}
	for (i=1;i<=n;i++) dp[i][1]=1;
	for (j=1;j<=m;j++) dp[1][j]=1;
	for (i=2;i<=n;i++)
	{
		for (j=2;j<=m;j++) 
		{
			if (a[i][j]!=a[i][j-1]&&a[i][j]!=a[i-1][j]) dp[i][j]=min(dp[i-1][j-1],min(dp[i-1][j],dp[i][j-1]))+1;
			else dp[i][j]=1;
		}
	}
	for (i=1;i<=n;i++)
	{
		for (j=1;j<=m;j++) ans=max(ans,dp[i][j]);
	}
	printf("%d\n",ans);
	return 0;
}

P1169 [ZJOI2007] 棋盘制作

建议升蓝。

第一问就是最大正方形 II。

诶然后考虑第二问。

第二问的话就是说你要求一个相邻格子颜色不同的最大矩形的面积。

然后先自己做。

WA。WA。WA。WA。

哇。

就是 \(up\) 随便转移转移。

然后枚举行,枚举行上的所有点,单调栈处理。

但是非常难的一个点在于,它有可能行都是不连续的,它不像前面那种问题,如果不合法,\(up=0\)。如果不合法,\(up=1\),但实际上需要结合行的情况来判断是否可行。

所以如果相邻两个位置相等了,那么清空栈,重置起点。这一段与上一段毫无关系。

这样把所有段算完之后,可算 AC 了。

#include<bits/stdc++.h>
using namespace std;
/*

*/
struct FSI{
	template<typename T>
	FSI& operator >> (T &res){
		res=0;T f=1;char ch=getchar();
		while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
		while (isdigit(ch)){res=res*10+ch-'0';ch=getchar();}
		res*=f;
		return *this;
	}
}scan;
const int N=2010;
int n,m,i,j,a[N][N],dp[N][N],ans,up[N][N];
int st[N],tp,L[N],R[N],tmp;
int main()
{
	scan>>n>>m;
	for (i=1;i<=n;i++)
	{
		for (j=1;j<=m;j++) scan>>a[i][j];
	}
	for (i=1;i<=n;i++) dp[i][1]=1;
	for (j=1;j<=m;j++) dp[1][j]=1;
	for (i=2;i<=n;i++)
	{
		for (j=2;j<=m;j++)
		{
			if (a[i][j]!=a[i][j-1]&&a[i][j]!=a[i-1][j]) dp[i][j]=min(dp[i-1][j-1],min(dp[i][j-1],dp[i-1][j]))+1;
			else dp[i][j]=1;
		}
	}
	for (i=1;i<=n;i++)
	{
		for (j=1;j<=m;j++) ans=max(ans,dp[i][j]*dp[i][j]);
	}
	printf("%d\n",ans);
	for (j=1;j<=m;j++) up[1][j]=1;
	for (i=2;i<=n;i++)
	{
		for (j=1;j<=m;j++)
		{
			if (a[i][j]!=a[i-1][j]) up[i][j]=up[i-1][j]+1;
			else up[i][j]=1;
		}
	}
	ans=0;
	for (i=1;i<=n;i++)
	{
		tp=0;
		tmp=0;
		for (j=1;j<=m;j++)
		{
			if (j!=1&&a[i][j]==a[i][j-1])
			{
				tp=0;
				tmp=j-1;
			}
			while (tp&&up[i][st[tp]]>=up[i][j]) tp--;
			if (tp) L[j]=st[tp]+1;
			else L[j]=tmp+1;
			st[++tp]=j;
		}
		tp=0;
		st[++tp]=m+1;
		for (j=m;j>=1;j--)
		{
			if (a[i][j]==a[i][j+1]&&j!=m)
			{
				tp=0;
				tmp=j+1;
			}
			while (tp&&up[i][st[tp]]>=up[i][j]) tp--;
			if (tp) R[j]=st[tp]-1;
			else R[j]=tmp-1;
			st[++tp]=j;
		}
		for (j=1;j<=m;j++) ans=max(ans,up[i][j]*(R[j]-L[j]+1));
	}
	printf("%d\n",ans);
	return 0;
}

但是我两道题都用的是单调栈。

看题解。

题解里果然全是悬线法。

喽一眼。

悬线法是不是更加简单?

好把悬线法弄懂了,再做下。

就是说,还是枚举每一行,但是对于这一行的每一个点,贪心的去取最高的那个矩形,然后算出这个最高的矩形的左右限制,最后取最大值。

难点在于理解这个玩意为啥取最高的一定是对的。

核心点就在于,这个最大的矩形,一定会被其中一个点计入到答案中。

反证:设这个矩形是最大矩形。如果底层上的每一个点的高度都大于这个矩形的高度,那么将会形成一个更大的矩形。与假设矛盾。故最大矩形一定会被底层的某个点所计入。

如上图。最终答案是一定正确的。补个代码。

\(5\) 分钟写完。

时间复杂度:\(\mathcal O(nm)\)

#include<bits/stdc++.h>
using namespace std;
/*

*/
struct FSI{
	template<typename T>
	FSI& operator >> (T &res){
		res=0;T f=1;char ch=getchar();
		while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
		while (isdigit(ch)){res=res*10+ch-'0';ch=getchar();}
		res*=f;
		return *this;
	}
}scan;
const int N=2010;
int n,m,i,j,a[N][N],x,y,up[N][N];
int L[N][N],R[N][N],t;
int main()
{
	scan>>n>>m;
	for (i=1;i<=n;i++)
	{
		for (j=1;j<=m;j++) scan>>a[i][j];
	}
	for (i=1;i<=n;i++)
	{
		for (j=1;j<=m;j++)
		{
			if (a[i][j]!=a[i-1][j]) up[i][j]=up[i-1][j]+1;
			else up[i][j]=1;
		}
	}
	for (i=1;i<=n;i++)
	{
		L[i][1]=1;
		for (j=2;j<=m;j++)
		{
			if (a[i][j]!=a[i][j-1]) L[i][j]=L[i][j-1];
			else L[i][j]=j;
		}
		R[i][m]=m;
		for (j=m-1;j>=1;j--)
		{
			if (a[i][j]!=a[i][j+1]) R[i][j]=R[i][j+1];
			else R[i][j]=j;
		}
	}
	for (i=1;i<=n;i++)
	{
		for (j=1;j<=m;j++)
		{
			if (a[i][j]!=a[i-1][j]&&i!=1)
			{
				L[i][j]=max(L[i][j],L[i-1][j]);
				R[i][j]=min(R[i][j],R[i-1][j]);
			}
			t=min(R[i][j]-L[i][j]+1,up[i][j]);
			x=max(x,t*t);
			y=max(y,up[i][j]*(R[i][j]-L[i][j]+1));
		}
	}
	printf("%d\n%d",x,y);
	return 0;
}

P2701 [USACO5.3] 巨大的牛棚 Big Barn

基本上就是最大正方形换了个壳。然后扩大了数据范围。

没啥好说的。

时间复杂度:\(\mathcal O(n^2)\)

#include<bits/stdc++.h>
using namespace std;
/*

*/
struct FSI{
	template<typename T>
	FSI& operator >> (T &res){
		res=0;T f=1;char ch=getchar();
		while (!isdigit(ch)){if (ch=='-') f=-1;ch=getchar();}
		while (isdigit(ch)){res=res*10+ch-'0';ch=getchar();}
		res*=f;
		return *this;
	}
}scan;
const int N=1010;
int n,m,i,j,a[N][N],dp[N][N],ans,x,y;
int main()
{
	scan>>n>>m;
	for (i=1;i<=m;i++)
	{
		scan>>x>>y;
		a[x][y]=1;
	}
	for (i=1;i<=n;i++)
	{
		for (j=1;j<=n;j++) 
		{
			if (!a[i][j]) dp[i][j]=min(dp[i-1][j-1],min(dp[i-1][j],dp[i][j-1]))+1;
		}
	}
	for (i=1;i<=n;i++)
	{
		for (j=1;j<=n;j++) ans=max(ans,dp[i][j]);
	}
	printf("%d\n",ans);
	return 0;
}

P1578 [WC2002] 奶牛浴场

posted @ 2026-08-21 16:46  GUO120822  阅读(2)  评论(0)    收藏  举报