http://codevs.cn/problem/2491/
求最大子矩形面积,方法很多。
首先介绍一个cgold的O(n^3)暴力做法:
http://blog.csdn.net/qq_36312502/article/details/77334624
除此之外这里还收集了三种方法,但首先要明确如下几点:
本题的统计思路:分别计算以每一行为底所能得到的最大子矩形面积 。
无论什么方法,我们都是通过确定一个已知高度的一列F向左向右扩展的边界来确定一个矩形的。
方法一:单调栈
我们建立栈内元素高度严格递增的单调栈s,用于存储遍历到的每一列F的高度,每次遍历到新的一列则判断:
1.若当前列的高度大于栈顶元素则加入栈中,符合栈中元素高度严格递增的条件。
2.若当前列的高度小于栈顶元素则依次弹栈,至当前一列的高度为栈中最高 ,维护栈中元素高度严格递增的条件。
3.遍历完成后再将栈内元素依次弹出。
在弹栈时计算将要弹出的一列向左向右(高度不变)扩展出的矩形面积,答案对每个矩形面积取max即可。
如何计算呢?
我们另外开一个对应的栈l,储存s中对应列向左扩展的长度。
确定一列的l值时有如下两种情况
1.符合单调性的一列加入,可向左扩展的距离只有其本身1,l中记为1。
2.高度小于栈顶元素的一列加入,对于栈内高度大于等于当前列的元素来说,其向右可扩展的长度为其后加入的列的个数,我们记为len。那么我们就可以在弹栈时得到栈中每一个高度大于等于当前列的元素对应的这个len去计算他们扩展出的最大子矩形面积。对于当前列,我们在弹栈完成后将其加入栈中,其在l中对应的值为len+1。
为什么是len+1呢?
因为虽然我们将高度大于等于当前列的元素弹出栈了,但事实上在原图中他们依旧是存在的,仍可以为当前列贡献向左扩展的长度,然后加上其本身的距离1,就是len+1了。
代码
#include<iostream>//栈内元素高度严格递增的单调栈
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;
int n,m,top,len,ans;
int mp[2010][2010],s[100010],l[100010];
char c;
void find(int h[])
{
for(int j=1;j<=m;j++)
{
if(h[j]>s[top])//若高度大于栈顶元素则加入栈中
{
s[++top]=h[j];
l[top]=1;//记录新加入了一列
}
else
{
len=0;
while(top&&h[j]<=s[top])//弹栈至当前一列的高度为栈中最高
{
len+=l[top];//记录将要弹出栈的一列,其在图中向左可扩展的长度+当前向右可扩展的长度---也是向右可扩展的最长长度
ans=max(ans,len*s[top]);//取其面积更新答案
top--;
}
s[++top]=h[j];
l[top]=len+1;//记录当前一列可向左扩展的大小
}
}
len=0;
while(top!=0)//将剩余列依次弹栈,处理其可扩展的答案的方法同上
{
len+=l[top];
ans=max(ans,len*s[top]);
top--;
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
c=getchar();
while(c<'F'||c>'R')
c=getchar();
if(c=='F')
mp[i][j]=mp[i-1][j]+1;//mp[i][j]记录第i行第j列F的高度
}
}
for(int i=1;i<=n;i++)//计算以每一行为底所能得到的最大子矩形面积
find(mp[i]);
printf("%d",ans*3);
return 0;
}
方法二:悬线法
首先处理出每一个点对应列(悬线)向左和向右可扩展到的边界记为l[i][j]和r[i][j]。
然后对于每一个合法的点,处理其上方的点的边界对其边界的影响,即对两点共同所处的矩形左右边界确定的影响,具体操作为两点边界值取min。
这样实际上来说,是将此点作为其上方的点所处矩形向下的扩展,而以此点原有边界为边界的矩形,则由与此点同行的其他不受上方点限制(上方点不合法或边界更宽松)的点统计,因为我们会处理每个点扩展到的矩形,故不会存在遗漏情况,可以保证正确性。
代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
using namespace std;
const int maxn=1000+5;
int h[maxn][maxn],l[maxn][maxn],r[maxn][maxn];
bool mmp[maxn][maxn];
int n,m;
int main()
{
char ch[2];
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
scanf("%s",ch);
if(ch[0]=='F') mmp[i][j]=1;
else if(ch[0]=='R') mmp[i][j]=0;
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(mmp[i][j]) l[i][j]=l[i][j-1]+1;//处理每一个点向左可扩展到的边界
}
for(int j=m;j>=1;j--)
{
if(mmp[i][j]) r[i][j]=r[i][j+1]+1;//处理每一个点向右可扩展到的边界
}
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(mmp[i][j])
{
h[i][j]=h[i-1][j]+1;
if(mmp[i-1][j])
{
l[i][j]=min(l[i][j],l[i-1][j]);
//处理其上方的点的边界对两点共同所处的矩形的影响
r[i][j]=min(r[i][j],r[i-1][j]);
//将此点作为其上方的点所处矩形向下的扩展
}
}
}
}
int ans=0;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
ans=max(ans,h[i][j]*(l[i][j]+r[i][j]-1));//处理每个点扩展到的矩形
printf("%d\n",ans*3);
return 0;
}
但要注意这个算法的空间复杂度的是比较高的。
方法三:单调栈+悬线法
我们通过维护单调栈确定栈顶元素向左,向右扩展的的边界,但并非在弹栈过程中计算面积,而是在每一列的边界确定后遍历一遍计算面积。
对于某一行中的各列我们先需要从左向右进行一遍单调栈的操作:
1.若当前列的高度大于栈顶元素则加入栈中。
2.若不然则可确定栈顶元素不可能再向右扩展,则其右边界为当前列的编号j-1即栈顶元素对应编号。
3.遍历完成后,栈内剩余元素的右边边界都为列数m,这是显而易见的。
同理我们再从右向左进行一遍单调栈的操作就可以确定每一列的左边界了。
之后再遍历各列计算矩形面积即可。
代码
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int n,m,mmp[1010][1010],r[1010],l[1010],s[1010],top,ans;
string x;
inline void push(int x)
{
s[++top]=x;
return;
}
inline void pop()
{
s[top--]=0;
return;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
cin>>x;
if(x=="F")
mmp[i][j]=mmp[i-1][j]+1;//mp[i][j]记录第i行第j列F的高度
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
while(mmp[i][j]<mmp[i][s[top]])
{
r[s[top]]=j-1;//栈顶元素可向右扩展的边界确定
pop();
}
push(j);
}
while(top>0)
{
r[s[top]]=m;//右边界即为原图最右端
pop();
}
for(int j=m;j>=1;j--)
{
while(mmp[i][j]<mmp[i][s[top]])
{
l[s[top]]=j+1;//栈顶元素可向左扩展的边界确定
pop();
}
push(j);
}
while(top>0)
{
l[s[top]]=1;//左边界即为原图最左端
pop();
}
for(int j=1;j<=m;j++)
ans=max(ans,(r[j]-l[j]+1)*mmp[i][j]);//对于每一个列考虑可以扩展成多大的矩形,其高度至高为当前列的高度
}
printf("%d",3*ans);
}
正如开篇所说,“无论什么方法,我们都是通过确定一个已知高度的一列F向左向右扩展的边界来确定一个矩形的”,三种方法本质上并无区别,差别来着实现方法。对于一种解题思路不妨尝试考虑不同的实现,同时,也要综合考量算法对时间复杂度,空间复杂度和代码复杂度的影响。