(C++)P1216数字三角形(动态规划)⭐⭐⭐⭐

[USACO1.5] [IOI1994]数字三角形 Number Triangles - 洛谷

题目描述
观察下面的数字金字塔。

写一个程序来查找从最高点到底部任意处结束的路径,使路径经过数字的和最大。每一步可以走到左下方的点也可以到达右下方的点。

在上面的样例中,从 7 → 3 → 8 → 7 → 5  的路径产生了最大权值。

输入格式
第一个行一个正整数 r ,表示行的数目。

后面每行为这个数字金字塔特定行包含的整数。

输出格式
单独的一行,包含那个可能得到的最大的和。
样例输入 
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
样例输出 
30
【数据范围】
对于 100% 的数据,1 ≤ r ≤ 1000,所有输入在[0,100] 范围内。

递归好像套娃,一娃套一娃,娃娃都相似

代码一(dfs暴力,会超时)

#include <iostream>    // 输入输出流
#include <algorithm>   // 包含max等算法
#include <cstring>     // 包含字符串操作函数
using namespace std;   // 使用标准命名空间
using i64 = long long; // 定义一个别名,表示64位整数

const int N = 1010; // 定义最大规模,数字三角形的最大边长

int n;              // 数字三角形的边长
int g[N][N];        // 数字三角形的存储数组,g[i][j]表示第i行第j列的数字

// 深度优先搜索(DFS)函数,用于求解从(x1, y1)位置开始的最大路径和
int dfs(int x1, int y1) {
    if (x1 > n || y1 > n) return 0; // 如果超出三角形范围,返回0
    else {
        // 递归求解两个子问题:
        // 1. 向下移动一步:dfs(x1+1, y1)
        // 2. 向下右移动一步:dfs(x1+1, y1+1)
        // 取两者的最大值,并加上当前位置的值g[x1][y1]
        return max(dfs(x1 + 1, y1), dfs(x1 + 1, y1 + 1)) + g[x1][y1];
    }
}

int main() {
    scanf("%d", &n); // 输入数字三角形的边长n
    for (int i = 1; i <= n; i++) { // 逐行读取数字三角形的数据
        for (int j = 1; j <= i; j++) {
            scanf("%d", &g[i][j]); // 输入第i行第j列的数字
        }
    }

    int res = dfs(1, 1); // 从三角形的顶部(1,1)开始递归求解最大路径和
    printf("%d\n", res); // 输出结果
    return 0;
}

代码分析

  1. 输入部分

    • 数字三角形是一个直角三角形,第i行有i个数字。

    • 使用二维数组g[N][N]存储三角形的数字,g[i][j]表示第i行第j列的数字。

  2. DFS函数

    • dfs(x1, y1)表示从位置(x1, y1)开始的最大路径和。

    • 如果超出三角形范围(x1 > ny1 > n),返回0。

    • 递归求解两个子问题:

      • 向下移动一步:dfs(x1+1, y1)

      • 向下右移动一步:dfs(x1+1, y1+1)

    • 取两者的最大值,并加上当前位置的值g[x1][y1]

  3. 递归逻辑

    • 从三角形的顶部(1,1)开始递归。

    • 每次递归都会分解为两个子问题,直到超出三角形范围。

    • 递归返回时,会逐层累加路径和,最终返回从顶部到底部的最大路径和。

  4. 时间复杂度

    • 由于递归会重复计算很多子问题,时间复杂度较高,为指数级。

  5. 空间复杂度

    • 主要消耗在递归调用栈上,空间复杂度为O(n)

代码二 (dfs+记忆化数组,但还是超时了一个测试点)

#include <iostream>    // 输入输出流
#include <algorithm>   // 包含max等算法
#include <cstring>     // 包含字符串操作函数
using namespace std;
using i64 = long long;  // 定义一个别名,表示64位整数

const int N = 1010;     // 定义最大规模,数字三角形的最大边长

int n, sum;             // n表示数字三角形的边长,sum用于存储路径和
int g[N][N];            // 数字三角形的存储数组
int mem[N][N];          // 记忆化数组,用于存储已经计算过的子问题结果

// 深度优先搜索(DFS)函数,加入记忆化搜索
int dfs(int x1, int y1) {
    if (mem[x1][y1]) return mem[x1][y1]; // 如果已经计算过该子问题,直接返回结果

    if (x1 > n || y1 > n) sum = 0; // 如果超出三角形范围,路径和为0
    else {
        // 递归求解两个子问题:
        // 1. 向下移动一步:dfs(x1+1, y1)
        // 2. 向下右移动一步:dfs(x1+1, y1+1)
        // 取两者的最大值,并加上当前位置的值g[x1][y1]
        sum = max(dfs(x1 + 1, y1), dfs(x1 + 1, y1 + 1)) + g[x1][y1];
    }
    mem[x1][y1] = sum; // 将计算结果存储到记忆化数组中
    return sum;        // 返回当前子问题的结果
}

int main() {
    scanf("%d", &n); // 输入数字三角形的边长
    for (int i = 1; i <= n; i++) { // 逐行读取数字三角形的数据
        for (int j = 1; j <= i; j++) {
            scanf("%d", &g[i][j]); // 输入第i行第j列的数字
        }
    }

    int res = dfs(1, 1); // 从三角形的顶部(1,1)开始递归求解最大路径和
    printf("%d\n", res); // 输出结果
    return 0;
}

关键改进点

  1. 记忆化数组mem

    • mem[x1][y1]用于存储从位置(x1, y1)开始的最大路径和。

    • 如果mem[x1][y1]已经计算过(非零),直接返回结果,避免重复计算。

  2. 递归逻辑

    • 如果超出三角形范围(x1 > ny1 > n),路径和为0。

    • 否则,递归计算两个子问题:

      • 向下移动一步:dfs(x1+1, y1)

      • 向下右移动一步:dfs(x1+1, y1+1)

    • 取两者的最大值,并加上当前位置的值g[x1][y1]

    • 将结果存储到mem[x1][y1]中。

  3. 效率提升

    • 原始版本的DFS是指数级复杂度,因为会重复计算很多子问题。

    • 加入记忆化后,每个子问题只计算一次,时间复杂度降低到O(n²),与动态规划相当。

代码三(动态规划,通过)

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
using i64 = long long;

const int N = 1010; // 数字三角形的最大边长

int n;              // 数字三角形的边长
int g[N][N];        // 存储数字三角形的数组
int f[N][N];        // 动态规划数组,f[i][j]表示从(i, j)出发的最大路径和

int main() {
    scanf("%d", &n); // 输入数字三角形的边长
    for (int i = 1; i <= n; i++) { // 逐行读取数字三角形的数据
        for (int j = 1; j <= i; j++) {
            scanf("%d", &g[i][j]); // 输入第i行第j列的数字
        }
    }

    // 动态规划从底向上计算最大路径和
    for (int i = n; i >= 1; i--) { // 从倒数第一行开始
        for (int j = 1; j <= i; j++) {
            // 如果是最后一行,f[i][j]直接等于g[i][j]
            if (i == n) {
                f[i][j] = g[i][j];
            } else {
                // 否则,根据递推关系计算f[i][j]
                f[i][j] = max(f[i + 1][j], f[i + 1][j + 1]) + g[i][j];
            }
        }
    }

    printf("%d\n", f[1][1]); // 输出从顶部(1,1)出发的最大路径和
    return 0;
}
动态规划思路
  • 使用一个二维数组f[N][N]来存储动态规划的结果,其中f[i][j]表示从第i行第j列出发到达底部的最大路径和。

  • 动态规划的递推关系为:

    • f[i][j] = max(f[i+1][j], f[i+1][j+1]) + g[i][j]

    • 这表示从(i, j)出发的最大路径和等于其下方两个点的最大路径和加上当前点的值。

  • 边界条件

    • 最后一行的f[n][j]直接等于g[n][j],因为从最后一行出发的最大路径和就是它自身的值。

  • 最终结果

    • 从三角形顶部(1, 1)开始的最大路径和存储在f[1][1]中。

代码四(再次优化空间,使用滚动数组)(这个代码不是太好理解,多看几遍,模拟这个更新的过程)

这段代码是数字三角形问题的最终优化版本,使用了一维数组来实现动态规划,进一步减少了空间复杂度。这种优化方式称为滚动数组,它利用了动态规划的性质:每一行的计算只依赖于下一行的结果,因此可以只用一个一维数组来存储中间结果。

(滚动数组的优化:二维变一维,一维变成点) 

#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
using i64 = long long;

const int N = 1010; // 数字三角形的最大边长

int n;              // 数字三角形的边长
int g[N][N];        // 存储数字三角形的数组
int f[N];           // 一维动态规划数组,用于存储从当前行每个位置出发的最大路径和

int main() {
    scanf("%d", &n); // 输入数字三角形的边长
    for (int i = 1; i <= n; i++) { // 逐行读取数字三角形的数据
        for (int j = 1; j <= i; j++) {
            scanf("%d", &g[i][j]); // 输入第i行第j列的数字
        }
    }

    // 动态规划从底向上计算最大路径和
    for (int i = n; i >= 1; i--) { // 从最后一行开始向上计算
        for (int j = 1; j <= i; j++) {
            // 更新f[j],表示从(i, j)出发的最大路径和
            f[j] = max(f[j], f[j + 1]) + g[i][j];
        }
    }

    printf("%d\n", f[1]); // 输出从顶部(1,1)出发的最大路径和
    return 0;
}

关键点解析

  1. 一维数组f的作用

    • f[j]表示从当前行的第j个位置出发,到达底部的最大路径和。

    • 在每次迭代中,f数组会被更新为当前行的最优路径和。

  2. 动态规划逻辑

    • 从最后一行开始向上计算:

      • 对于每个位置(i, j),其最大路径和等于其下方两个位置的最大值加上当前值,左边的f[i]代表的是从(i, j)出发的最大路径和:

        f[j] = max(f[j], f[j + 1]) + g[i][j];
      • 括号里的f[j]f[j + 1]分别表示从(i+1, j)(i+1, j+1)出发的最大路径和。

  3. 滚动数组的优势

    • 通过只使用一个一维数组f,避免了二维数组的空间开销。

    • 空间复杂度从O(n²)优化到O(n),同时保持了时间复杂度为O(n²)

  4. 边界条件

    • i == n(最后一行)时,f[j]直接等于g[n][j],因为从最后一行出发的最大路径和就是它自身的值。

    • 代码中隐含了这一逻辑,因为最后一行的f[j]会被直接更新为g[n][j]


代码运行逻辑

  1. 输入数字三角形

    • 逐行读取数字三角形的值,存储到二维数组g中。

  2. 动态规划计算

    • 从最后一行开始,逐行向上计算每个位置的最大路径和。

    • 每次更新f[j]时,利用下一行的结果(f[j]f[j + 1])。

  3. 输出结果

    • 最终,f[1]存储了从顶部(1, 1)出发的最大路径和。

 

收藏加关注,观看不迷路

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值