一本通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;
}
posted @ 2021-08-12 19:10  quanjun  阅读(334)  评论(0编辑  收藏  举报