Codevs2491玉蟾宮

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向左向右擴展的邊界來確定一個矩形的”,三種方法本質上並無區別,差別來着實現方法。對於一種解題思路不妨嘗試考慮不同的實現,同時,也要綜合考量算法對時間複雜度,空間複雜度和代碼複雜度的影響。

發佈了65 篇原創文章 · 獲贊 72 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章