312. 戳气球

  1. 题目链接

  2. 解题思路

    • 暴力递归,一个很容易想到的方法就是,「先戳哪个气球」,process(L, R)[L, R]上,返回能得到的最大硬币数目。
      • 一个for循环,枚举,「先戳哪个气球」,但是有问题,假如先戳i号气球,接下来的递归怎么调用?process(L, i - 1) + process(i + 1, R)这样?这是错误的,因为你在递归调用process(L, i - 1)的时候,假设现在是先戳i - 1号球,那么i - 1号球得到的硬币是多少?你不知道!因为右边还有一个递归process(i + 1, R),你不知道右边的气球是怎么爆炸的。
    • 枚举的方法是,「最后戳爆哪个气球」,process(L, R),假如最后戳爆i号球,那么i号球得到的硬币是nums[i] * nums[L - 1] * nums[R - 1],然后递归调用process(L, i - 1) + process(i + 1, R),这次为什么可以这样递归调用了?因为最后戳爆i号球,你在调用process(L, i - 1)时,右边肯定有一个i号球,所以就不需要管右边的是怎么爆炸的了。
      • 有一个细节,process(L, R),先戳爆i号球,你怎么知道L-1还有球?你怎么知道R + 1还有球?所以为了避免这种情况,我们在原来的nums中,在开头加入一个「1」, 在末尾加一个「1」,这样就保证了,无论你何时戳爆一个气球,左右两边都一定有一个球,并且这些球不会影响结果。
    • 又是两个参数,所以,直接加缓存,就是动态规划了。
  3. 代码

    class Solution {
    public:
    int process(vector<int> &nums, int L, int R, vector<vector<int>> &dp) {
    if (L > R) {
    return 0;
    }
    if (L == R) {
    return nums[L] * nums[L - 1] * nums[R + 1];
    }
    if (dp[L][R] != -1) {
    return dp[L][R];
    }
    int ans = 0;
    for (int i = L; i <= R; ++i) {
    int next = process(nums, L, i - 1, dp) + process(nums, i + 1, R, dp) + nums[i] * nums[L - 1] * nums[R + 1];
    ans = max(ans, next);
    }
    dp[L][R] = ans;
    return ans;
    }
    int maxCoins(vector<int>& nums) {
    vector<int> newNums;
    newNums.push_back(1);
    for (auto &it : nums) {
    newNums.push_back(it);
    }
    newNums.push_back(1);
    int n = newNums.size();
    vector<vector<int>> dp(n, vector<int>(n, -1));
    return process(newNums, 1, n - 2, dp);
    }
    };
  4. 说明,「自顶向下」的动态规划,和「自底向上」的动态规划,本质上是没有区别的,「自底向上」的动态规划就是我们常说的dp,然后有状态转移方程,「自底向上」的动态规划,有时可以优化,为啥?因为「自顶向上」的动态规划,其中用了for循环来枚举,有些情况,这种「循环枚举」可以优化。

  5. 「自底向上」动态规划代码。状态转移方程怎么来的?其实也是暴力递归来的。dp[i][j]等于多少,其实就是等于process(i, j),也就是说「自底向上」的动态规划,本质上就是先写出暴力递归代码,然后再改成「自底向上」的。具体怎么改的,看下面代码中的注释。

    class Solution {
    public:
    int process(vector<int> &nums, int L, int R, vector<vector<int>> &dp) {
    if (L > R) {
    return 0;
    }
    if (L == R) {
    return nums[L] * nums[L - 1] * nums[R + 1];
    }
    if (dp[L][R] != -1) {
    return dp[L][R];
    }
    int ans = 0;
    for (int i = L; i <= R; ++i) {
    int next = process(nums, L, i - 1, dp) + process(nums, i + 1, R, dp) + nums[i] * nums[L - 1] * nums[R + 1];
    ans = max(ans, next);
    }
    dp[L][R] = ans;
    return ans;
    }
    int maxCoins(vector<int>& nums) {
    vector<int> newNums;
    newNums.push_back(1);
    for (auto &it : nums) {
    newNums.push_back(it);
    }
    newNums.push_back(1);
    int n = newNums.size();
    vector<vector<int>> dp(n, vector<int>(n, 0));
    // 其实dp就是一张二维表,把这张表填好,然后返回我们想要的dp[1][n-2]就行了
    // 为什么想要dp[1][n-2]?因为暴力递归返回的就是process(newNums, 1, n - 2, dp)
    // 也就是说 dp[1][n - 2] == process(newNums, 1, n - 2, dp)
    // 怎么开始填呢? 根据暴力递归的终止条件
    for (int i = 1; i <= n - 2; ++i) { // 暴力递归中的L == R
    dp[i][i] = newNums[i] * newNums[i - 1] * newNums[i + 1];
    }
    // 接下来怎么填这张表 我们看暴力递归是怎么展开的
    // int ans = 0;
    // for (int i = L; i <= R; ++i) {
    // int next = process(nums, L, i - 1, dp) + process(nums, i + 1, R, dp) + nums[i] * nums[L - 1] * nums[R + 1];
    // ans = max(ans, next);
    // }
    // 任意一个dp[L][R] =
    // for (int i = L; i <= R; ++i) {
    // int next = dp[L][i - 1] + dp[i + 1][R] + nums[i] * nums[L - 1] * nums[R + 1];
    // dp[L][R] = max(dp[L][R], next);
    // }
    // 一张二维表 我们要怎么填,因为我们的终止条件是对角线,然后我们需要的是右上角的位置
    // 所以我们填表的顺序是一条一条斜线填
    for (int k = 1; k < n - 1; ++k) {
    for (int j = 1; j + k < n - 1; ++j) {
    dp[j][j + k] = 0;
    for (int i = j; i <= j + k; ++i) {
    dp[j][j + k] = max(dp[j][j + k], dp[j][i - 1] + dp[i + 1][j + k] + newNums[i] * newNums[j - 1] * newNums[j + k + 1]);
    }
    }
    }
    //return process(newNums, 1, n - 2, dp);
    return dp[1][n - 2];
    }
    };
  6. 照这样说,「自底向上」这么难搞,每次都写「自顶向下」呗?不一定,「自底向上」的方法,有时候能根据依赖关系,把「循环枚举」去掉,这样就快了。

posted @   ouyangxx  阅读(10)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
点击右上角即可分享
微信分享提示