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 @ 2024-11-11 20:38  ouyangxx  阅读(0)  评论(0编辑  收藏  举报