Luogu P4147 玉蟾宮 題解
P4147 玉蟾宮
舊版:2019/7/5
Rewrite on 2022/2/10 增加講解,增加程式碼精簡度
(本題所有圖中,黃色表示 \(C\) ,藍色表示 \(F\))
原題 傳送門
主體思路:預處理+懸線法
First things first
既然題目中的圖按照 \(S/F\) 進行輸入,首先不必多說需要用一個數組mp
儲存
樣例儲存:
0 | 1 | 1 | 1 | 1 | 1 |
---|---|---|---|---|---|
1 | 1 | 1 | 1 | 1 | 1 |
0 | 0 | 0 | 1 | 1 | 1 |
1 | 1 | 1 | 1 | 1 | 1 |
1 | 1 | 1 | 1 | 1 | 1 |
主要思路:
Step 1:初始化整張圖
我們使用兩個陣列 \(l[i][j]\) 和 \(r[i][j]\) 來儲存該點所在行向左邊/右邊擴充套件最近處的障礙
之後,列舉mp
從左到右的每一個點,使用變數t
來儲存上一個找到的節點:
-
\(First\space things\space First:\) 定義
t
為最左端-1,也就是 \(0\) -
$Case 1: $
1
(也就是 \(F\))
那麼,左邊的障礙就不變,同樣為
t
- $Case 2: $
0
(也就是 \(C\))
那麼很不幸,它本身就是障礙,
t
的值需要更新,同時該點在 \(l[i][j]\) 陣列中的值就變為一個不存在的值(比如序列最前端-1,也就是0)(實測不進行更改,得分為46 QwQ)
下圖是完成後,樣例中每一個點的 \(L[i][j]\)
同樣的,我們再從右到左列舉,思路同上,注意:
t
需要重新初始化
Step1的最後一步:因為第 \(0\) 行不存在,然而我們在之後的處理中會用到 l[i-1][j]
和 r[i-1][j]
,因此需要排除這個bug
下圖是完成後,樣例中每一個點的 \(R[i][j]\)
Step 2:步入正題:懸線法求解
什麼是懸線法?
OI Wiki:對於一條懸線,我們在這條上端點不超過當前位置的矩形高度且不移出邊界的前提下,將這條懸線左右移動,求出其最多能向左和向右擴充套件到何處,此時這條懸線掃過的面積就是包含這條懸線的儘可能大的矩形。容易發現,最大子矩形必定是包含一條初始位置為 \(i\) ,高度為 \(h_i\) 的懸線。列舉實現這個過程的時間複雜度為 \(O(n^2)\)
,但是我們可以用懸線法將其優化到 \(O(n)\) 。
(個人認為講解還是蠻詳細的)
因此,最後的答案計算,我們可以用懸線法解決
程式碼片段:
if(mp[i][j] != 0 /*mp[i][j]不是障礙物*/)
{
h[i][j] = h[i-1][j]+1;//判斷,如果這個點是合法的,對應的懸線長度應當比它下面的點對應的懸線長大1
l[i][j] = max(l[i][j]+1, l[i-1][j]);
r[i][j] = min(r[i][j]-1, r[i-1][j]);
ans = max((r[i][j]-l[i][j]+1)*h[i][j], ans); //求出最大的面積
}
其中的h
陣列就是用於計算懸線法,\(h[i][j]\) 實質上就是計算對於點 \((i,j)\) 適用的懸線長度,容易想出,會有以下兩種情況:
- $Case 1: $ \(h[i-1][j]=0\),也就是現在列舉到的點下面的點要麼是 \(C\) 而不滿足條件,要麼是最後一行
此時的 \(h[i-1][j]\) 值加1等於1,也就是第一行,符合題意
- $Case 1: $ \(h[i-1][j]\neq 0\),也就是下一行同樣符合條件
此時的 \(h[i-1][j]\) 值加1,符合題意
真正的最後一步: 計算懸線法計算出的矩形的長
舉個例子:
根據上面的圖,我們知道最後矩形的長實際上是第 \(i\) 行和第 \(i-1\) 行取交集
因為 l
陣列是從左到右遞增或不變的,所以需要取 max
,同理,r
陣列需要取 min
那麼,為什麼還要 $-1 $ 或 \(+1\) ?
剛才在計算
l
和r
陣列時,我們計算的是最近的障礙,那麼邊界就是在l
陣列中 \(+1\) ,在r
陣列中 \(-1\)
於是,我們就可以愉快的輸出答案了!
AC程式碼:
#include<bits/stdc++.h>
#define maxn 1010
using namespace std;
int mp[maxn][maxn],n,m,h[maxn][maxn],ans,t;char c;
//mp負責儲存輸入的簡化(0/1),n和m代表n行m列,h是點離下面的垂線,ans是結果,t是上一個障礙的位置 ,c也負責輸入
int l[maxn][maxn],r[maxn][maxn];
//l[i,j]和 r[i,j]分別表示點(i,j)能擴充套件到的左邊和右邊的最近的障礙。
int main()
{
cin >> n >> m;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
cin>>c;
mp[i][j] = (c=='F'?1:0); //三目運算子,儲存地圖
}
//按行列舉
for(int i=1;i<=n;i++)
{
t=0; //初始化t,預設上個障礙不存在
for(int j=1;j<=m;j++) //列舉每個點
if(mp[i][j]) l[i][j]=t; //如果此點為F,那麼左邊的障礙位置不變
else l[i][j]=0;t = j; //反之,我們把障礙預設到最左端-1,意思是他本身就是障礙
t=m+1; //重新初始化t,讓他成為序列最右端+1
for(int j=m;j>=1;j--)
{
//列舉右邊的障礙,思路同上
if(mp[i][j]) r[i][j]=t;
else r[i][j]=m+1,t=j;
}
}
//因為第0行不存在,所以需要初始化第0行的資料為序列端點+/-1
//l陣列不需要初始化,因為初始化內容一定是 l[0][i] = 0; 而l陣列中預設值就是0
for(int i=1;i<=m+1;i++)
r[0][i]=m+1;
for(int i = 1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(mp[i][j] != 0 /*mp[i][j]不是障礙物*/)
{
h[i][j] = h[i-1][j]+1;//判斷,如果這個點是合法的,對應的懸線長度應當比它下面的點對應的懸線長大1
l[i][j] = max(l[i][j]+1, l[i-1][j]); //計算左邊界
r[i][j] = min(r[i][j]-1, r[i-1][j]); //計算右邊界
ans = max((r[i][j]-l[i][j]+1)*h[i][j], ans); //求出最大的面積
}
}
}
cout<<3*ans<<endl; //切莫忘記*3
return 0;
}
提交記錄:URL
本題提升:P1950
完成此題可以一試