一本通1258.數字金字塔 題解 動態規劃
題目連結:http://ybt.ssoier.cn:8088/problem_show.php?pid=1258
題目大意:
給你一個數字金字塔,每次可以從當前點走到左下方或右下方的點。查詢從最高點到底部任意處結束的路徑,使路徑經過數字的和最大。
解題思路:
設 \(a_i,j\) 表示數字金字塔上第 \(i\) 行從左往右數第 \(j\) 個點,則我們的目的就是從 \(a_{1,1}\) 走到第 \(n\) 行的 \(a_{n,i}\)(此處 \(i\) 可能是 \(1 \sim n\) 範圍內的任何一個點)。
很明顯這道題目可以用動態規劃(DP)求解,但是根據求解的方向不同,對應的狀態也不相同。
這裡,我們可以以兩種思路來解決這個問題:
- 第1種思路:自頂向下(從上往下推);
- 第2種思路:自底向上(從下往上推)。
下面,我們來依次分析兩種思路。
自頂向下(從上往下推)
定義狀態 \(f_{i,j}\) 表示從 \(a_{1,1}\) 走到 \(a_{i,j}\) 的所有路徑數字和的最大值。則,可以推匯出狀態轉移方程為:
- 當 \(i =1\) 時(\(i=1\) 時只有一個狀態 \(f_{1,1}\)),\(f_{1,1} = a_{1,1}\);
- 當 \(i \gt 1\) 時,
- 當 \(j = 1\) 時,\(f_{i,1} = f{i-1,1} + a_{i,1}\)(最左邊的點只能從右上方走過來);
- 當 \(j = i\)
- 當 \(1 \lt j \lt i\) 時,\(f_{i,j} = \max\{f_{i-1,j-1}f_{i-1,j}\} + a_{i,j}\)(中間的點可以選擇從左上角或右上角的點走過來,所以選擇兩者的較大值)
令 \(i = 1 \rightarrow n\) 的順序推匯出所有的 \(f_{i,j}\),則最終的答案就是:
- \(f_{n,1}\)(從 \(a_{1,1}\) 走到 \(a_{n,1}\) 的最大數字和)、
- \(f_{n,2}\)(從 \(a_{1,1}\)
- \(f_{n,3}\)(從 \(a_{1,1}\) 走到 \(a_{n,3}\) 的最大數字和)、
- …… ……
- \(f_{n,n}\)(從 \(a_{1,1}\) 走到 \(a_{n,n}\) 的最大數字和)
的最大值,即答案為 \(\max\{ f_{n,i} \}\)(其中 \(i \in [1,n]\))
說明:\(\in\) 是“屬於”符號,\(i \in [1,n]\) 表示 \(i\) 屬於 \([1,n]\) 範圍內,即 \(i\) 是 \(1\) 到 \(n\) 範圍內(包括 \(1\) 和 \(n\))的一個整數。
按照自頂向下實現的程式碼如下:
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1010;
int n, a[maxn][maxn], f[maxn][maxn], ans;
int main()
{
scanf("%d", &n); // 雖然題目裡用R表示行的數量,但是我還是比較習慣用n表示行數
for (int i = 1; i <= n; i ++)
for (int j = 1; j <= i; j ++)
scanf("%d", &a[i][j]);
for (int i = 1; i <= n; i ++)
{
for (int j = 1; j <= n; j ++)
{
if (i == 1) f[i][j] = a[i][j];
else if (j == 1) f[i][j] = f[i-1][j] + a[i][j];
else if (j == i) f[i][j] = f[i-1][j-1] + a[i][j];
else f[i][j] = max(f[i-1][j-1], f[i-1][j]) + a[i][j];
}
}
for (int i = 1; i <= n; i ++)
ans = max(ans, f[n][i]);
printf("%d\n", ans);
return 0;
}
自底向上(從下往上推)
從第 \(1\) 行的格子(\(a_{1,1}\))走到第 \(n\) 行的路徑的最大數字和,等價於從第 \(n\) 行選一個點作為起點從下往上走到 \(a_{1,1}\) 的路徑最大數字和。所以我們可以把問題看成從下往上走找一條最大路徑的數字和。
那麼我們可以定義狀態 \(f_{i,j}\) 表示:從第 \(n\) 行找一個點作為起點走到 \(a_{i,j}\) 的路徑的最大數字和,則可以推匯出轉檯轉移方程為:
- 當 \(i = n\) 時,\(f_{i,j} = a_{i,j}\)(因為時最下面一行,起點出發能夠得到的數字就是本身);
- 當 \(i \lt n\) 時,\(f_{i,j} = \max\{ f_{i+1,j} , f_{i+1,j+1} \} + a_{i,j}\)。
注意這裡要按照 \(i = n \rightarrow 1\) 的方向推匯出所有的狀態。
則最終的狀態為 \(f_{1,1}\)(因為從小往上推的話,所有路徑對應的重點只有一個就是 \(a_{1,1}\))。
按照自底向上實現的程式碼如下(可以發現,按照這種方法實現的程式碼更簡潔一些):
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1010;
int n, a[maxn][maxn], f[maxn][maxn];
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i ++)
for (int j = 1; j <= i; j ++)
scanf("%d", &a[i][j]);
for (int i = n; i >= 1; i --)
{
for (int j = 1; j <= i; j ++)
{
if (i == n) f[i][j] = a[i][j];
else f[i][j] = max(f[i+1][j], f[i+1][j+1]) + a[i][j];
}
}
printf("%d\n", f[1][1]);
return 0;
}