最大子矩形問題的解決方法:懸線法
給出一道板子題
洛谷4147 玉蟾宮
題目背景
有一天,小貓rainbow和freda來到了湘西張家界的天門山玉蟾宮,玉蟾宮宮主藍兔盛情地款待了它們,並賜予它們一片土地。
題目描述
這片土地被分成N*M個格子,每個格子裡寫著'R'或者'F',R代表這塊土地被賜予了rainbow,F代表這塊土地被賜予了freda。
現在freda要在這裡賣萌。。。它要找一塊矩形土地,要求這片土地都標著'F'並且面積最大。
但是rainbow和freda的OI水平都弱爆了,找不出這塊土地,而藍兔也想看freda賣萌(她顯然是不會程式設計的……),所以它們決定,如果你找到的土地面積為S,它們每人給你S兩銀子。
輸入輸出格式
輸入格式:
第一行兩個整數N,M,表示矩形土地有N行M列。
接下來N行,每行M個用空格隔開的字元'F'或'R',描述了矩形土地。
輸出格式:
輸出一個整數,表示你能得到多少銀子,即(3*最大'F'矩形土地面積)的值。
輸入輸出樣例
輸入樣例#1: 複製5 6
R F F F F F
F F F F F F
R R R F F F
F F F F F F
F F F F F F
輸出樣例#1: 複製
45
說明
對於50%的資料,1<=N,M<=200
對於100%的資料,1<=N,M<=1000
//花有重開日,人有少年時!致敬砍下50分的羅斯
讓我們像羅斯一樣,永不言棄。這道題雖然有著提高加的難度,我們仍要解決它!
這種以O(NM)的時間複雜度解決它的特殊方法,叫做懸線法。
轉載一波:
定義
有效豎線:除了兩個端點外,不覆蓋任何一個障礙點的豎直線段。
懸線:上端覆蓋了一個障礙點或者到達整個矩形上邊界的有效線段。
每個懸線都與它底部的點一一對應,矩形中的每一個點(矩形頂部的點除外)都對應了一個懸線。
懸線的個數=(N-1)*M;
如果把一個極大子矩形按照橫座標的不同切割成多個與y軸平行的線段,那麼其中至少有一個懸線。
如果把一個懸線向左右兩個方向儘可能的移動,那麼就得到了一個矩形,我們稱它為懸線對應的矩形。
懸線對應的矩形不一定是極大子矩形,因為下邊界可能還可以向下擴充套件。
設計演算法:
原理:所有懸線對應矩形的集合一定包含了極大子矩形的集合。 通過列舉所有的懸線,找出所有的極大子矩形。 演算法規模: 懸線個數=(N-1)×M 極大子矩形個數≤懸線個數
具體方法:
設 H[i,j]為點(i,j)對應的懸線的長度。 L[i,j]為點(i,j)對應的懸線向左最多能夠移動到的位置。 R[i,j]為點(i,j)對應的懸線向右最多能夠移動到的位置。 考慮點(i,j)對應的懸線與點(i-1,j)對應的懸線的關係(遞推思想): 如果(i-1,j)為障礙點,那麼,如圖所示,(i,j)對應的懸線長度1,左右能移動到的位置是整個矩形的左右邊界。 即 H[i,j]=1, L[i,j]=0,R[i,j]=m如果(i-1,j)不是障礙點,那麼,如圖所示,(i,j)對應的懸線長度為(i-1,j)對應的懸線長度+1。 即 H[i,j]=H[i-1,j]+1 •如果(i-1,j)不是障礙點,那麼,如圖所示,(i,j)對應的懸線左右能移動的位置要在(i-1,j)的基礎上變化。 L[i-1,j] L[i,j]=max (i-1,j)左邊第一個障礙點的位置 •同理,也可以得到R[i,j]的遞推式 R[i-1,j] R[i,j]=min (i-1,j)右邊第一個障礙點的位置 以此思想,我們得出瞭解決此題的方法。 但我的第一次提交是這樣寫的:
#include<iostream>
#include<cstdio>
#include<cstring>
#define ll long long
using namespace std;
int n,m;
int map[1003][1003],xuxi[1003][1003],l[1003][1003],r[1003][1003];
char c;
int read()
{
char c=getchar();
while(c!='F'&&c!='R')c=getchar();
if(c=='F')return 1;
else return 0;
}
int main()
{
memset(map,0,sizeof(map));
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
map[i][j]=read();
l[i][j]=r[i][j]=j;xuxi[i][j]=1;
}
for(int i=1;i<=n;i++)
for(int j=2;j<=m;j++)
if(map[i][j]&&map[i][j-1])
l[i][j]=l[i][j-1];
for(int i=1;i<=n;i++)
for(int j=m-1;j>=1;j--)
if(map[i][j]&&map[i][j+1])
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(map[i][j]&&map[i-1][j])
{
xuxi[i][j]=xuxi[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]);
ans=max(ans,xuxi[i][j]*(r[i][j]-l[i][j]+1));//這句話的位置不對
}
printf("%d",ans*3);
return 0;
}
看似沒有錯誤,實則一交只得90······
原因是,構造這樣一組資料即可卡死這個程式:
5 5
R R R R R
R R R R R
R R R F R
R R R R R
R R R R R
本該輸出3,卻輸出了0.
所以,應該這樣寫,以下才是標準程式:
#include<iostream>
#include<cstdio>
#include<cstring>
#define ll long long
using namespace std;
int n,m;
int map[1003][1003],xuxi[1003][1003],l[1003][1003],r[1003][1003];
char c;
int read()
{
char c=getchar();
while(c!='F'&&c!='R')c=getchar();
if(c=='F')return 1;
else return 0;
}
int main()
{
memset(map,0,sizeof(map));
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
map[i][j]=read();
l[i][j]=r[i][j]=j;xuxi[i][j]=1;
}
for(int i=1;i<=n;i++)
for(int j=2;j<=m;j++)
if(map[i][j]&&map[i][j-1])
l[i][j]=l[i][j-1];
for(int i=1;i<=n;i++)
for(int j=m-1;j>=1;j--)
if(map[i][j]&&map[i][j+1])
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(map[i][j]&&map[i-1][j])
{
xuxi[i][j]=xuxi[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]);
}
ans=max(ans,xuxi[i][j]*(r[i][j]-l[i][j]+1));
}
printf("%d",ans*3);
return 0;
}
即可輕鬆AC。
總結:最大子矩形,求法精妙,細節注意。