由leetcode198来思考动态规划

在这里插入图片描述
这是一道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)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

五月的天气

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值