312. 戳气球
-
解题思路
- 暴力递归,一个很容易想到的方法就是,「先戳哪个气球」,
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)
,你不知道右边的气球是怎么爆炸的。
- 一个for循环,枚举,「先戳哪个气球」,但是有问题,假如先戳i号气球,接下来的递归怎么调用?
- 枚举的方法是,「最后戳爆哪个气球」,
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」,这样就保证了,无论你何时戳爆一个气球,左右两边都一定有一个球,并且这些球不会影响结果。
- 有一个细节,
- 又是两个参数,所以,直接加缓存,就是动态规划了。
- 暴力递归,一个很容易想到的方法就是,「先戳哪个气球」,
-
代码
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); } };
-
说明,「自顶向下」的动态规划,和「自底向上」的动态规划,本质上是没有区别的,「自底向上」的动态规划就是我们常说的dp,然后有状态转移方程,「自底向上」的动态规划,有时可以优化,为啥?因为「自顶向上」的动态规划,其中用了for循环来枚举,有些情况,这种「循环枚举」可以优化。
-
「自底向上」动态规划代码。状态转移方程怎么来的?其实也是暴力递归来的。
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]; } };
-
照这样说,「自底向上」这么难搞,每次都写「自顶向下」呗?不一定,「自底向上」的方法,有时候能根据依赖关系,把「循环枚举」去掉,这样就快了。