1. 程式人生 > 其它 >P6855「EZEC-4.5」走方格 TJ

P6855「EZEC-4.5」走方格 TJ

這是一篇還布吉島通沒通過的題解~

前言

題目傳送門

正解:動態規劃

挺 duliu 一道題,難度較大 qwq。

PS:因為此篇題解前後改動較多,如果有什麼錯誤請各位奆佬提出,本蒟蒻感激不盡 awa。

題意簡述

給你一個 \(n\times m\) 大小的方格陣,可以把方格中的任意一個數改為 \(0\),每次從 \((1,1)\)\((n,m)\) 的得分為路上所有數字的和。求每次改動數字後能得到的最大值的最小值。

法一:時間複雜度 \(Θ(m^2n^2)\) (TLE)

這但凡是個正常人都會想到吧……前兩層迴圈列舉變為 \(0\) 的方格座標,後兩層按照正常的方格取數做法求最大值。

結果,被校 OJ 卡了沒騙到分。

\(Code\)

#include<bits/stdc++.h>
#define ll long long
using namespace std;
int m,n,t,a[2005][2005];
ll ans=LONG_LONG_MAX,dp[2005][2005];
int main(){
	scanf("%d %d",&m,&n);
	for(int i=1;i<=m;i++){
		for(int j=1;j<=n;j++){
			scanf("%d",&a[i][j]);
		}
	}
	for(int x=1;x<=m;x++){
		for(int y=1;y<=n;y++){
			memset(dp,0,sizeof(dp));
			t=a[x][y];
			a[x][y]=0;
			for(int i=1;i<=m;i++){
				for(int j=1;j<=n;j++){
					dp[i][j]=max(dp[i][j-1],dp[i-1][j])+a[i][j];
				}
			}
			ans=min(ans,dp[m][n]);
			a[x][y]=t;
		}
	}
	printf("%lld",ans);
	return 0;
} 

法二:正解,時間複雜度 \(Θ(mn)\)

PS * 2:因為 me 習慣用 \(m\) 表示行 \(n\) 表示列,所以下列題解就會這麼寫。

有點麻煩。

如果每個點只能遍歷一次,那麼必須不變值和變為 \(0\) 這兩種情況需要同時考慮。

首先我們可以考慮先求出從 \((1,1)\) 點出發走到 \((i,j)\) 點和從 \((m,n)\)出發走到 \((i,j)\) 點能拿到的最大分數,分別存在 \(dp1\) 陣列和 \(dp2\) 數組裡。

這兩個陣列很好求,按照每個初學 DP 者都要打的取數板子就珂以了。至於為什麼要求這兩個陣列,我先賣個關子,待會兒就知道了(逃。

我們知道,如果你從 \((1,1)\)

出發,在走的時候不經過某個座標為 \((i,j)\) 的點(也就是繞過這個點),你有兩種情況可以繞開它:

  1. 從左邊繞。

  2. 從上邊繞。

給這兩種方法更嚴謹的定義 \((i>0)\)

  1. 從左邊繞:經過點 \((x,y-i)\)

  2. 從上邊繞:經過點 \((x-i,y)\)

對於這兩種情況,我們可以分別用兩個二維陣列 \(l\) (左邊繞)和 \(d\) (上邊繞)來存:

  • \(l_{i,j}\) 表示從左邊繞過 \((i,j)\) 點能獲得的最大值。

  • \(d_{i,j}\) 表示從上邊繞過 \((i,j)\) 點能獲得的最大值。

但是如何求這兩個陣列呢?

直接切入可能比較麻煩。這個時候我們可以先分析這種情況:

不管是往哪邊繞,也不管前面怎麼走,都緊貼著點 \((i,j)\) 過路方便分析,即,從左繞一定經過點 \((i,j-1)\),從上繞一定經過點 \((i-1,j)\)

首先分析從左邊繞的情況。

看圖,藍色區域表示從 \((1,1)\) 出發到點 \((i,j-1)\) 有可能會經過的區域,紅色區域表示從點 \((i+1,j-1)\)\((m,n)\) 有可能會經過的區域。至於為什麼選這兩個點呢,相信大家看圖也能明白,因為選擇這兩個點可以做到經過的格子不重不漏,考慮到每種情況。

如果這麼算,那麼從點 \((i,j-1)\) 繞過去能拿到的最大分數就是:

\[score=dp1_{i,j-1}+dp2_{i+1,j-1} \]

現在大家知道兩個 \(dp\) 陣列的意義了吧,就是用來求某個區域的最大分數的。因為如果每次迴圈到一個點就計算此點到終點的分數還需要兩層迴圈會超時,基於走方格的方向是可逆的,我們只需要計算終點到每個點的最大分數就可以啦 OvO。

那我們現在只求了最貼近點 \((i,j)\) 的繞法,那我們如何求出往左繞的所有情況的最大值呢?

我們之前不是用了一個數組來存往左邊繞的值嗎?因為迴圈的順序是從上到下,從左到右的,所以在求點 \((i,j)\) 的值時,我們已經把它左上方的所有值都求出來了。現在我們可以利用這些值,每一次,我們求當前 \(score\) 與之前最大分數的較大值,那每次都求最大值就是所有情況中的最大值。

那麼最後的結果就是:

\[l_{i,j}=\max(dp1_{i,j-1}+dp2_{i+1,j-1},i_{i,j-1}) \]

至於為什麼我們利用的是 \(l_{i,j-1}\),大家可以自己畫圖感知,這個格子就位於我們前面求的必須經過的那個格子 \((i,j-1)\),那麼繞過它我們就會求必須經過點 \((i,j-2)\),這樣我們就又需要考慮繞過這個格子的情況,就又必須經過點 \((i,j-3)\)……這麼一層一層往左推,最後可以推到點 \((i,1)\),從而把每種情況都考慮到。

從上面繞分析方法也差不多,這裡不多說畫張圖讓讀者感知一下。

(您看看這兩張圖多像,連大小都差不多26 KB)

最後求出 \(d_{i,j}\) 的式子為:

\[d_{i,j}=\max(dp1_{i-1,j}+dp2_{i-1,j+1},d_{i-1,j}) \]

最後求答案有些麻煩,因為題目要求的是變化後最能獲得的最大分數的最小值,所以 \(\max\)\(\min\) 是真的挺容易用混的,這裡需要特別注意。

首先每對一個格子進行操作,最後得到的答案會是下列三種情況中的一種:

  1. 從左邊繞過去得到的最大分數。

  2. 從上邊繞過去得到的最大分數。

  3. 經過這個格子得到的最大分數。

前兩個我們已經求解了,但其實第三種情況是灰常簡單的!因為要保證經過點 \((i,j)\),所以我們只需要求 \(dp1_{i,j}+dp2{i,j}\) 就可以了。但是上面那個式子算了兩次 \((i,j)\) 的值,而我們因為把它變成 \(0\) 了,就一次都不能算。所以還需要減去兩個 \(a_{i,j}\)

所以最後的答案終於可能被更新了 owo:

\[ans=\min\big(ans,\max(l_{i,j},d_{i,j},dp1_{i,j}+dp2_{i,j}-2\times a_{i,j})\big) \]

最後再強調一遍要注意最大值和最小值別用反了啊! 別問我為什麼知道(悲)。

\(Code\)

#include<bits/stdc++.h>
#define ll long long //記得要開long long哦!
using namespace std;
//溫馨提示細節:因為最後答案還是求的最小值,所以 ans 需要定義極大值
ll m,n,ans=LONG_LONG_MAX,a[2005][2005],dp1[2005][2005],dp2[2005][2005],l[2005][2005],d[2005][2005];
int main(){
	//輸入
	scanf("%lld %lld",&m,&n);
	for(int i=1;i<=m;i++){
		for(int j=1;j<=n;j++){
			scanf("%lld",&a[i][j]);
		}
	}
   	//求兩個 dp 陣列的值
	for(int i=1;i<=m;i++){
		for(int j=1;j<=n;j++){
			dp1[i][j]=max(dp1[i-1][j],dp1[i][j-1])+a[i][j];
		}
	}
	for(int i=m;i;i--){
		for(int j=n;j;j--){
			dp2[i][j]=max(dp2[i+1][j],dp2[i][j+1])+a[i][j];
		}
	}
   	//核心程式碼開始
	for(int i=1;i<=m;i++){
		for(int j=1;j<=n;j++){
			l[i][j]=max(l[i][j-1],dp1[i][j-1]+dp2[i+1][j-1]);
			d[i][j]=max(d[i-1][j],dp1[i-1][j]+dp2[i-1][j+1]);
			ans=min(ans,max(max(l[i][j],d[i][j]),dp1[i][j]+dp2[i][j]-2*a[i][j]));
		}
	}
   	//核心程式碼結束,輸出答案
	printf("%lld",ans);
	return 0;
}

寫在最後

這真的是一道很好的動態規劃題,很考驗思維,也有很多需要注意的細節。最後,看在本人寫了那麼久的份上,就請您隨手點一下左下角那個小小的贊吧 qwq。