动态规划
世界还是这么小啊,又遇见了我们最亲爱的朋友 ———— 动态规划,怎么说也见过两三次了,回忆起来了我们的这位老朋友了吗?
我来带你回忆回忆我们的动态四部曲!
动态四部曲
1. 确定dp
的状态 : 定义为一维二维还是三维?表示什么含义?
2. 确定状态转移方程
3. 初始化,也就是dp
数组可以取得的元素的值
4. 自底向上的方式计算dp
,并得出最优值(遍历的顺序)
戳气球
题目
有 n 个气球,编号为0 到 n - 1,每个气球上都标有一个数字,这些数字存在数组 nums 中。
现在要求你戳破所有的气球。戳破第 i 个气球,你可以获得 nums[i - 1] * nums[i] * nums[i + 1] 枚硬币。 这里的 i - 1 和 i + 1 代表和 i 相邻的两个气球的序号。如果 i - 1或 i + 1 超出了数组的边界,那么就当它是一个数字为 1 的气球。
求所能获得硬币的最大数量。
下面是一个示例
示例 1:
输入:nums = [3,1,5,8]
输出:167
解释:
nums = [3,1,5,8] --> [3,5,8] --> [3,8] --> [8] --> []
coins = 3*1*5 + 3*5*8 + 1*3*8 + 1*8*1 = 167
复制代码
思路解析
那我们就根据我们的动态四部曲,来解析一下这个示例
先确定dp
数组的状态
这里我们所要求的问题的解是戳破气球所能获得的硬币的最大数量,那么这个问题的子问题也就是戳破了nums
数组中从i
到j
的气球获得的硬币最大数量,所以我们定义dp
数组为二维数组,而dp[i][j]
的含义就为i
到j
这个区间所获得的最大硬币数量。
确定状态转移方程
那么这个最大硬币数量又是怎么来的呢?
我们看到示例当中的计算方式,为被戳破的那个气球所表示的数字与其左右两边数字的乘积,我们假定nums
数组的三个数字下标i``k``j
,被戳爆的气球下标索引为k
,那么戳爆k
所获得的硬币的这个计算表达式就可以表示为 nums[i] * nums[k] * nums[j]
。这样子仅仅就是三个气球的情况,要是左右两边气球更多呢?
那么同样的,假设在 [i, j]区间内戳破一个索引为k
的气球,我们就可以推导出 dp[i][j]
的值就是戳破k
左边的所有气球得到的dp[i][k]
加上戳破 k
右边边的所有气球得到的dp[k][j]
,再加上 k
对应的气球得到的 nums[i] * nums[k ] * nums[j]
,那么相应的状态转移方程就为dp[i][j] = max( dp[i][j],(dp[i][k] + dp[k][j] + nums[i] * nums[k] * nums[j]))
;
初始化,也就是dp
数组可以取得的元素的值
这里dp
数组初始化,能取得值只有0了,取不取其实也无关紧要。
4. 自底向上的方式计算dp
,并得出最优值(遍历的顺序)
这里很显然是倒序递推了,假设这里是先求解 dp[2][6],那么就需要先求解对应的“dp[4][6]”,如果采用顺序数组循环,那么求解 dp[4][6] 会在求解 dp[2][6] 之前进行,显然就不符合逻辑了。所以这里排成倒序递推更好求解
代码
class Solution {
public:
int maxCoins(vector<int>& nums) {
int n = nums.size();
vector<vector<int>> dp(n + 2, vector<int>(n + 2, 0));
nums.insert(nums.begin(), 1);
nums.push_back(1);
for (int i = n - 1; i >= 0; i--) {
for (int j = i + 2; j <= n + 1; j++) {
for (int k = i + 1; k < j; k++) {
dp[i][j] = max(
dp[i][j],
(dp[i][k] + dp[k][j] + nums[i] * nums[k] * nums[j]));
}
}
}
return dp[0][n + 1];
}
};
复制代码
代码解析
我们首先需要扩大数组,两边都加上1,因为题目中说明若是右边或者左边没有气球就用1替代,所以这里我们从下标为n - 1
处开始倒序递推,而我们在右边元素j
从至少间隔i
一个位置的下标开始遍历,中间元素k
则是要被戳破的气球,也就是i``j
之间的元素,并利用状态转移方程,求解,最后求得的dp[0][n + 1]
就是我们最终的结果
总结
动态规划题目种类还是很多的,题型也是防不胜防,还是需要多学习。
我是小白,我们一起在学习代码的路上不折不扣,屡战屡败,屡败屡战!