1. 程式人生 > 其它 >一本通1258.數字金字塔 題解 動態規劃

一本通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\)
      時,\(f_{i,i} = f{i-1,i-1} + a_{i,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}\)
    走到 \(a_{n,2}\) 的最大數字和)、
  • \(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;
}