动态规划之线性dp

1、打家劫舍

图片

解法一:循环迭代法
class Solution {
public:
    int rob(vector<int>& nums) {
        int len = nums.size();
        vector<int> dp(len);
        //返回最基本的两种情况:dp [ 0 ] 与 dp [ 1 ]
        if (len == 1) return nums[0];
        if (len == 2) return max(nums[0],nums[1]);
        //dp数组的初始化
        dp[0] = nums[0];
        dp[1] = max(nums[0],nums[1]);

        for (int i = 2;i < nums.size();i++){//遍历顺序由下及上,相对数组来说即从左向右遍历
            dp[i] = max(dp[i-1],dp[i-2]+nums[i]);//通过递推公式计算出数组dp区间 [ 2 , len - 1 ] 中的所有数据
        }
        return dp[len - 1];//注意数组长度 = 数组最大下标值 + 1,返回时要长度 - 1
    }
};
五步法分析:
1、确定dp数组以及下标的含义

dp[i] :表示从第一个房屋出发开始偷,到第i个房屋,所能窃取的最大金额数为dp[i]。

2、确定递推公式

想要求dp[i],只能比较选择出来,而比较选择最少需要两个对象,即dp[i - 1] 和 dp[i - 2] 。

此时在回顾一下 dp[i - 1] 表示啥,是从第一个房屋出发开始偷,到第i - 1个房屋,所能窃取的最大金额数为dp[ i - 1 ],dp[ i - 2 ]同理,只不过dp[ i - 2 ]和dp[ i ]间距了一个房间,可以在偷完后继续偷 i 房间,即dp [ i - 2 ] + nums[ i ]。

那么很自然,dp[ i ] = max( dp[ i - 1 ] , dp[ i - 2 ] + nums[ i ] ) 。

3、dp数组的初始化

如何初始化呢,首先至少要有两个初始值,才能推导出后面的dp数组。

所以初始化代码为:

dp[0] = nums[0]; dp[1] = nums[1];

4、确定遍历顺序

这里要看一下递推公式dp[ i ] = max( dp[ i - 1 ] , dp[ i - 2 ] + nums[ i ] ) ,dp[ i ]都是从其左方推导而来,那么从左到右遍历就可以了。

5、循环代入数据推导dp数组
解法二:带“备忘录”的递归法(由下及上)
class Solution {
public:
    int rob(vector<int>& nums) {
        int len = nums.size();
        if (len == 0) return 0;//如果数组长度为0,直接返回0
        vector<int> memo(len+7,-1);//递归调用传入的参数i + 1和i + 2都有越界的风险,所以实际开辟的空间要大一些
        
        return helper(nums,memo,0);//递归函数调用
    }
    int helper(vector<int>& nums,vector<int>& memo,int i){
        int n = nums.size();

        if (i >= n) return 0;//递归结束条件和返回值
    

        if (memo[i] != -1) return memo[i];//判断“备忘录”里是否有这次的结果,若有则直接返回
        memo[i] = max(helper(nums,memo,i+1),helper(nums,memo,i+2)+nums[i]);//状态传递函数,计算后存入“备忘录”中
        
        return memo[i];//返回
    }
};
解法二的特点:

不同于平常的递归,都是从右向左,由上及下的拆分法,这也是我们所熟知的递归特性。但这道题的递归采用了由左到右,由下及上的推导法。

实际上我们就是换了个方向,好比我们想要数数从五楼到一楼有多少个台阶,从五楼下到一楼和从一楼上到五楼没有本质的区别,只是在实际的代码中有些细节需要注意:不同的方向需要对数组的边界、递归的结束条件以及状态传递函数的下标值进行修改。

解法二另一个版本:带“备忘录”的递归法(由上及下)
class Solution {
public:
    int rob(vector<int>& nums) {
        int len = nums.size();
        if (len == 0) return 0;//如果数组长度为0,直接返回0
        //新的数组边界限制
        vector<int> memo(len + 1,-1);//由于备忘录的索引从1开始,而不是从0开始,最终输出的是下标要等于len,所以开辟的空间要额外+1
        
        return helper(nums,memo,len);//递归函数调用
    }
    int helper(vector<int>& nums,vector<int>& memo,int i){
        //新的递归结束条件
        if (i <= 0) return 0;
        if (i == 1) return nums[0];//递归结束条件和返回值
        if (i == 2) return max(nums[0] , nums[1]);


        if (memo[i] != -1) return memo[i];//判断“备忘录”里是否有这次的结果,若有则直接返回
        /*新的状态传递函数的下标值变化,例如:i - 1 , i - 2 , nums[ i - 1 ]
          备忘录的索引从1开始,而不是从0开始,相对于索引从0开始的nums数组memo的下标值总要多1,
          所以实际传参时要 - 1
        */
        memo[i] = max(helper(nums,memo,i-1),helper(nums,memo,i-2)+nums[i-1]);//状态传递函数,计算后存入“备忘录”中
        
        return memo[i];//返回
    }
};
动态规划三要素分析:

首先说下这道线性DP涉及的状态,“打家劫舍”的状态只涉及一种,那就是“偷到房屋i的最大金额数”,这是动态规划的第一要素:状态;接下来是动态规划的第二要素:阶段,这道题是线性DP,那么它就属于动态规划的线性阶段,是如何看出它是线性DP的呢?显然我们要根据题目给出的房屋的顺序依次进行计算才能求出答案,如果把房屋的顺序进行改变,那么最终的答案就可能会改变,我们把这种需要按照特定的顺序对子问题进行求解的动态规划划分为线性阶段DP;最后是动态规划的第三要素:决策,状态转移方程的最终形式实际上是在状态“偷到房屋i的最大金额数”下的选择——每次选择状态最大的子问题,这样最终得到的状态才是最大的。

2、股票的最大利润

图片

解法一:循环迭代法
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int len = prices.size();
        if (len == 0) return 0;
        vector<int> dp(len,0);
        int minPrice = prices[0];//最小子问题
        for (int i = 1;i < len;i++){
            minPrice = min(minPrice,prices[i - 1]);
            dp[i] = max(dp[i - 1] , prices[i] - minPrice);
        }
        return dp[len - 1];
    }
};
解法二:带“备忘录”的递归法
class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int len = prices.size();
        if (len == 0) return 0;
    int minP = prices[0];
        vector<int> memo(len + 5,-1);
        return helper(prices,memo,1,minP);
    }
    int helper(vector<int>& prices,vector<int>& memo,int i,int minPrice){
        int n = prices.size();
        if (i >= n) return 0;
       
        if (memo[i] != -1) return memo[i];
    
        minPrice = min(minPrice, prices[i-1]);
        
        memo[i] = max(helper(prices,memo,i+1,minPrice) , prices[i] - minPrice);
        
        return memo[i];
    }
};
动态规划三要素分析:

1、状态:“第i天所能获得的最大利润”;

2、阶段:股票的价格无规律排列,子问题的状态取决于价格的排列顺序,例如[ 1 , 2 , 3 , 4 ],最小子问题即为第一天所能获得的最大利润为0,次小子问题为第二天所能获得的最大利润为1......则这道题需要按照特定的顺序对子问题进行求解,因此阶段划分为线性阶段;

3、决策:每次取子问题的最大状态。

转移方程的推导:

由状态分析作为切入点,状态“第i天所能获得的最大利润”在没有经过“决策”之前实际上是两种状态的叠加态,一种是它本身可以求出的最大利润,也就是在第i天把股票卖了;另一种是它所能继承的最大利润,也就是第i天股票卖不出去,那就继承“第i - 1天所能获得的最大利润”,因此得出下面的文字表达式:

第i天所能获得的最大利润 = max(未卖出:第i - 1天所能获得的最大利润,卖出:第i天所能获得的最大利润);

将它符号抽象化得:

dp[ i ] = max( dp[ i - 1 ] , prices[ i ] - min( prices[ 0 : i - 1 ] ) );

其中min( prices[ 0 : i - 1 ] )表示第i天之前最低的股票价格。

Summarize:

线性DP实际上是对动态规划阶段的划分,现在我了解并学过的有线性DP,数位和DP等,对他们的划分都有对应的依据,是可以从题目的描述中分析出来的。就比如线性DP,他的特点就是我们最终需要按照特定的顺序对子问题进行代入求解,像上面的“打家劫舍”和“股票的最大利润”问题,我们都需要按照题目给出的数据顺序代入转移方程中求出最终的答案,如果把题目给出的数据的排序进行改变,即使是一模一样的数据,我们最终也可能的道与原先截然不同的答案,因此做这类题时一定要严格按照题目所给出的数据顺序进行遍历求解。

众所周知求解DP问题最重要的就是推出它的转移方程,笔者自己总结了一套方法放在下面供大家参考:

对状态转移方程进行定义和推导:

①确定状态:dp[i]表示XXXXX;

②状态方程 = 决策(子状态,本身状态);决策可以是取最大值=>max(子状态,本身状态);取最小值=>min(子状态,本身状态),相加=>子状态+本身状态等

③由①、②确定状态转移方程。

在之后的文章中会具体的应用这种方法进行解题,

以上,如果有帮助的话欢迎分享给他人。

更好的阅读体验欢迎关注公众号“三七的碎碎念”,期待和你的一起成长φ(゜▽゜*)♪

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值