这是一道easy的题目,但是今天我重做的时候,用了一个不一样的方法,对动态规划的理解又深刻了一点。。。主要是将其和背包问题的状态定义的区别更加理解了。
之前我对动态规划的理解,就是从最小的一个问题,递推,最终得到最大问题的解。
具体我又给其分成两种问题,一种是问题图形画出来是一棵二叉树,一种画出来是多叉树。
二叉树问题诸如:
斐波那契数列以及其变种(如爬楼梯 / 在一个网格中只能向两个方向移动,让求其最小路径)
如这一道问题:
其画出来的就是这样的一棵二叉树
可以找到其递归结构:
状态定义:设 F(n)表示第n层往下所能得到的最短路径
V(n) 表示该层的元素
状态转移方程:F(0) = V(0) + min( F(1左), F(1右) )
由于发现在这个二叉树中存在重复的元素,即我们处理的问题中存在重叠子问题,所以可以用动态规划的方式来做:
即从倒数第二行的每一个元素开始计算,在该点处的最小路径是多少,并用这个位置保存该路径,依次往上推,最终[0][0]位置处就是我们想要得到的最小路径。
//动态规划写法(我其实是先写画出递归树,看是否有重叠子问题,
然后用递归写出来,最后改成记忆化搜索的方式,
最后就比较好改写成递推的动态规划)
class Solution {
public:
int minimumTotal(vector<vector<int>>& triangle) {
int m = triangle.size();
if( m == 0) return 0;
if( m == 1) return *min_element(triangle[0].begin(), triangle[0].end());
for(int i = m-2; i >=0; i -- ){ //从下往上推
for(int j = 0; j < triangle[i].size(); j ++){
triangle[i][j] += min(triangle[i+1][j], triangle[i+1][j+1]);
}
}
return triangle[0][0];
}
};
多叉树问题,其实和二叉树求解是一样的,但是在循环外多了一个变量,用来求出每一轮的最大/小值
如这一道问题
其画出来的树结构为:
递归结构是:
状态定义:设F(n)为n这个数字所能得到的最大乘积
状态转移方程: F(n) = max( 1 * F(n-1), 2 * F(n-2)… (n-1) * F(1) );
所以动态规划的方法即为:
先计算F(1),然后计算F(2),这样层层递推到F(n)
//动态规划写法(这道题需要注意,每次还要将其 i * (n-i) 即不继续分割了 来一起放入比较)
class Solution {
private:
int maxThree(int a, int b, int c){
return max( a, max(b,c) );
}
public:
//求n分成数字的最大乘积
int integerBreak(int n) {
vector<int> memo = vector<int>(n+1, -1); //这里开辟空间又忘记了是n+1
memo[1] = 1;
for(int i = 2; i <= n; i ++ ){
for(int j = 1; j < i; j ++){
memo[i] = maxThree(memo[i], j*(i-j), j * memo[i-j]);
}
}
return memo[n];
}
};
以上就是我总结的动态规划思路,画出递归树,然后定义好状态,写出状态转移方程,然后写出递推的动态规划方法。
这里状态的定义可以有很多种,不同的状态定义,就会写出来不同的动态规划代码。
比如这一道题leetcode198
我读完题后,画出了这样的一个问题结构树
发现了其为一棵多叉树,符合递归结构,并且也发现了重叠子问题,所以也就可以用动态规划来解。
按照这个图,状态定义和状态转移方程如下:
这里基础问题就是 x = n-1 的时候 即[n-1,n-1]
代码就如下:
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
if(n == 0)
return 0;
//memo[i] 存储的是偷[i...n-1]能获得的最大金额
vector<int> memo(n, -1);
memo[n - 1] = nums[n - 1]; //从最基础的[n-1,n-1]开始逐渐向左边扩充
for(int i = n - 2 ; i >= 0 ; i --)
for(int j = i; j < n; j ++)
memo[i] = max( memo[i], nums[j] + (j + 2 < n ? memo[j+2] : 0) );
return memo[0]; //最终memo[0]就是要求的解(重点在于memo[i]的定义)
}
};
或者 状态定义为: 考虑偷取 [0...x] 的房子
状态转移方程:F(n-1) = max{ v(n-1) + F(n-3), v(n-2) + F(n-4) … v(1) + F(0), v(0) }
这时候动态规划中基础问题就是F(0) 即[0,0]的
代码如下:
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
if( n == 0 )
return 0;
//memo[i] 存储的是偷[0...i]能获得的最大金额
vector<int> memo(n, -1);
memo[0] = nums[0];
for(int i = 1; i < n ; i ++){
//每一次都是在求解memo[i]
for(int j = i; j >= 0 ; j --){
memo[i] = max( memo[i], nums[j] + (j-2 >= 0 ? memo[j-2] : 0 ) );
}
}
return memo[n-1];
}
};
虽然我图画的是这个图,但是我写动态规划的时候,思路还是之前递推的模式,所以我的定义和图一点关系都没有。。。但是我的思路和状态定义为: 考虑偷取 [0...x] 的房子
这个非常不容易区分来开(这里x号房子不一定被偷)
状态定义:memo[i] 表示i为最后偷到的房子(i肯定被偷),其所能得到的最大价值
状态转移方程:偷n家房子的最大价值 = max(memo[ n-2 ] ,memo[n-1]) (即最大价值 肯定是在以【倒数第一个或 倒数第二个 】为最后一个偷取的房子)
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
if( n == 0 ) return 0;
if( n == 1 ) return nums[0];
if( n == 2 ) return max( nums[0], nums[1] );
vector<int> memo (n, -1);
memo[0] = nums[0]; memo[1] = nums[1];
//这个memo[i]定义为 只抢劫到i的时候 能得到的最大金额 所以最后求memo中memo[n-1]和memo[n-2]的最大值
for(int i = 2; i < n ; i ++)
for( int j = 2; j <= i; j ++)
memo[i] = max(memo[i], memo[i-j] + nums[i]);
return max(memo[n-2],memo[n-1]);
}
};
这两个需要好好思考一下我才能分辨开来。。。
基于上面的几个思路,我又想到了之前在看0-1背包问题的时候,可以根据该元素是否放入背包来构建状态
这道题也可以,对于第一种状态,考虑偷取 [x…n-1] 的房子,如果用是否偷取x的房子来构建状态,需要更改的代码为:
//原来的
for(int i = n - 2 ; i >= 0 ; i --)
for(int j = i; j < n; j ++)
memo[i] = max( memo[i], nums[j] + (j + 2 < n ? memo[j+2] : 0) );
//变化后
for(int i = n - 2 ; i >= 0 ; i --)
memo[i] = max( memo[i + 1], nums[i] + (i + 2 < n ? memo[i + 2] : 0));
对于第二种状态,考虑偷取[0…x] 的房子
//原来的
for(int i = 1; i < n ; i ++){
//每一次都是在求解memo[i]
for(int j = i; j >= 0 ; j --){
memo[i] = max( memo[i], nums[j] + (j-2 >= 0 ? memo[j-2] : 0 ) );
}
}
//变化后
for(int i = 1 ; i < n ; i ++)
memo[i] = max(memo[i - 1], nums[i] + (i - 2 >= 0 ? memo[i - 2] : 0));
memo[i] 的值等于 不偷取i 和 偷取i 中获得利益的最大值
可以看到用这样的一种思路,其时间复杂度从O(n^2)变成了O(n)