专题二十_动态规划_简单多状态dp问题_买卖股票系列问题_算法专题详细总结

时间:2024-11-18 15:48:57

目录

动态规划

1. 按摩师(easy)

解析:

1.状态表达式:

2.状态转移方程

3.初始化

4.填表方向

5.返回值:

代码编写:

总结:

2. 打家劫舍II (medium)

解析:

代码编写:

总结:

3. 删除并获得点数(medium)

解析:

代码编写:

总结:

4. 粉刷房⼦(medium)

解析:

代码编写:

总结:

下面就是买卖股票类型,也是我当年的噩梦了:

5. 买卖股票的最佳时机含冷冻期(medium)

解析:

1.状态表达式:

2.状态转移方程:

3.初始化:

4.填表顺序:

5.返回值,只用返回最后一天的最大利润即可;

代码编写:

总结:

6. 买卖股票的最佳时期含⼿续费(medium)

解析:

编写代码:

总结:

7. 买卖股票的最佳时机III(hard)

解析:

代码编写:

总结:

8. 买卖股票的最佳时机IV(hard)

 解析:

代码编写:

总结:

总结不易~本节对我的收获很大,希望对你也是~!!!


动态规划

在完成了前面动态规划相关内容的铺垫之后,我们已经对斐波那契模型以及路径问题有了清晰的认识。在此基础上,接下来我们将步入一个全新的章节 —— 简单多状态 dp 问题。

1. 按摩师(easy)

题目意思很简单,就是一个按摩师不能连续服务两个顾客,只能必须要有休息的间隔,给出一个方案能服务的最长时间

解析:

就是简单的dp问题,那我们来一步步分析清楚就能很快的解答:

1.状态表达式:

经验+题目意思

dp[i]表示:以i结尾的位置的最大服务时间

2.状态转移方程

3.初始化

初始f[i] 和 g[i] ,f[0] 就是初始化0位置要选的结果,即f[0]=nums[0]
g[0] 就是不选,不用进行初始化

4.填表方向

每次只有把i-1位置填了才能填当前位置,所填表方向从左往右填

5.返回值:

返回两种状态下的最大值即可

代码编写:

class Solution {
public:
    int massage(vector<int>& nums) {
        int n=nums.size();
        if(n==0) return 0;
        vector<int> f(n+1);
        vector<int> g(n+1);
        f[0]=nums[0];
        for(int i=1;i<n;i++)
        {
            f[i]=g[i-1]+nums[i];
            g[i]=max(f[i-1],g[i-1]);
        }
        return max(f[n-1],g[n-1]);
    }
};

总结:

本题不是很难,主要就是要学会分析当前问题的情况,学会分类讨论,讨论出当前位置是选还是不选比较合适,从而分出两个函数来一个单独代表选的情况,一个单独代表不选

2. 打家劫舍II (medium)

这是上一题的升级版,围成了一个圈,也就是说偷了0位置就不能偷n-1位置

解析:

1.状态表达式:

dp[i]表示:以i位置为结尾的最大利润

2.状态转移方程

仍然是设置f[i]表示当前位置选 跟 g[i]表示当前位置不选 两个表达式,这样就可以求出每一个位置选或者不选的不同的值,然后求出最后的最大值即可;

由于这一题是一个环形,所以就存在下面两种方案:

0位置选:0位置选了,1位置和n-1位置就不能被选了,那么这个数组的遍历范围就减小到[2,n-2]

0位置不选:那么1位置和n-1位置就有被选的资格,所以数组遍历范围就是[1,n-1];

那么为了多次创建数组,我们只需要封装一个函数rob1()每次传入数组的范围即可:

状态转移方程就是:

            f[i]=g[i-1]+nums[i];

            g[i]=max(g[i-1],f[i-1]);

3.初始化:

跟上一题一模一样的初始化,因为没有必要多开一个空间,又因为状态转移方程存在 n-1 所以每次只需要初始化传入的f[left]=nums[left] 然后从left+1开始遍历即可;

4.填表顺序:

依旧是从左往右填表

5.返回值

返回max(f[right],g[right]); 因为right位置是闭区间

代码编写:

class Solution {
public:
    int n;
    int rob(vector<int>& nums) {
        n=nums.size();
        return max(nums[0]+rob1(nums,2,n-2),rob1(nums,1,n-1));
    }

    int rob1(vector<int>& nums,int left,int right)
    {
        if(left>right) return 0;
        vector<int> f(n);
        auto g=f;
        f[left]=nums[left];
        for(int i=left+1;i<=right;i++)
        {
            f[i]=g[i-1]+nums[i];
            g[i]=max(g[i-1],f[i-1]);
        }
        return max(f[right],g[right]);
    }
};

总结:

这一题值得好好反思总结跟上一题的区别,原理是一样的,但是细节问题很多好好对比一下还是收获满满的;

3. 删除并获得点数(medium)

题目意思很简单,就是每次选择一个数后比这个数大一和小的全部不能选,求出最大总和

解析:

我刚开始想构建一个完美的数组vector<pair<int,int>> hash;只存入nums里面的数字对应的总和,但是构建出来后,发现如果用上面的动态规划,那么就会跳过比如两个数字之间并不是相差1的大小,而是跳过了本来不应该跳过的数字,也就是说,就算那个数字不存在,也应该是0来表示

错误编写:

class Solution {
public:
    int deleteAndEarn(vector<int>& nums) {
        vector<pair<int,int>> hash;
        sort(nums.begin(),nums.end());
        int n=nums.size();
        for(int i=0;i<n;i++)
        {
            if(hash.size()==0)
            {
                hash.push_back({nums[i],nums[i]});
                continue;
            }
            if(nums[i]>hash.back().first) hash.push_back({nums[i],nums[i]});
            else hash.back().second+=nums[i];
        }

        for(auto e : hash) cout<<e.first<<" "<<e.second<<endl;

        n=hash.size();
        vector<int> f(n);
        auto g=f;
        f[0]=hash[0].second;
        for(int i=1;i<n;i++)
        {
            f[i]=g[i-1]+hash[i].second;
            g[i]=max(g[i-1],f[i-1]);
        }
        return max(f[n-1],g[n-1]);
    }
};

所以还是只能创建一个unordered_map<int,int> 来构建这个hash

1.状态表达式

通过上面的思路可以看出这一题就是前面的打家劫舍I问题,只需要把对应的数字求出对应的总和即可:

dp[i] 表示:以i位置为结尾的最大总和数

2.状态转移方程:

还是跟打家劫舍问题一样,设置两个数组,分别记录当前位置选还是不选的最终结果,然后分别取最大值

f[i]=g[i-1]+nums[i];

g[i]=max(g[i-1],f[i-1]);

3.初始化:

任然是每次都要遍历i-1的位置,所以只需要初始化f[0]=hash[0],从1开始;

4.填表顺序:

从左往右

5.返回值

由于最后是[1,n]闭区间的位置,因为最后一个位置是hash里面最大值,所以开空间要开到ret的位置,也就是n+1个空间,最后返回max(f[n],g[n]);

代码编写:

class Solution {
public:
    int deleteAndEarn(vector<int>& nums) {
        unordered_map<int,int> hash;
        int n=nums.size();
        int ret=0;
        for(int i=0;i<n;i++)
        {
            hash[nums[i]]+=nums[i];
            ret=max(nums[i],ret);
        }
        n=ret;
        vector<int> f(n+1);
        auto g=f;
        f[0]=hash[0];
        for(int i=1;i<=n;i++)
        {
            f[i]=g[i-1]+hash[i];
            g[i]=max(g[i-1],f[i-1]);
        }
        return max(f[n],g[n]);
    }
};

总结:

这一题很细节,就是要学会问题的转换,能够把题目意思转换成我们所熟悉的打家劫舍问题,我记得我第一次做的时候就是暴力求解,删掉了当前位置的前一个值和后一个值,然后求最大值

4. 粉刷房⼦(medium)

题目意思很好理解,就是给了一个costs数组,每一行都代表这个i房子的不同染料的价格,并且两个相邻的房子颜色不能相同,所以求出最少的花费

解析:

1.状态表达式

dp问题就是为了表达所有的情况,求出最优解,所以设置一维dp只能表示每一行的最优解,而不能表示选取每一个格子后是否为最优解
设置二维:
dp[i][0]:表示在第i个房子下,用0号颜色情况下的所有粉刷最小费用
dp[i][1]:表示在第i个房子下,用1号颜色情况下的所有粉刷最小费用
dp[i][2]:表示在第i个房子下,用2号颜色情况下的所有粉刷最小费用

2.状态转移方程:

由于只有三个颜色,那么完全可以单独为每一个dp[i][0/1/2]单独求最小值:
即状态转移方程:是通过上一行能够挑出的两个颜色中选一个最小值在+当前房子需要的费用
            dp[i][0]=min(dp[i-1][1],dp[i-1][2])+costs[i-1][0];
            dp[i][1]=min(dp[i-1][0],dp[i-1][2])+costs[i-1][1];
            dp[i][2]=min(dp[i-1][0],dp[i-1][1])+costs[i-1][2];

3.初始化:

多加一行,初始化为0 即可,就是为了避免越界

4.填表顺序:

从上往下,从左往右填表

5.返回值:

返回最后一行的最小值即可

代码编写:

class Solution {
public:
    int minCost(vector<vector<int>>& costs) {
        int n=costs.size();
        vector<vector<int>> dp(n+1,vector<int>(3));
        int ret=INT_MAX;
        for(int i=1;i<=n;i++)
        {
            dp[i][0]=min(dp[i-1][1],dp[i-1][2])+costs[i-1][0];
            dp[i][1]=min(dp[i-1][0],dp[i-1][2])+costs[i-1][1];
            dp[i][2]=min(dp[i-1][0],dp[i-1][1])+costs[i-1][2];
            if(i==n) ret=min(min(dp[i][0],dp[i][1]),dp[i][2]);
        }
        return ret;
    }
};

总结:

这一题需要考虑的很细致,了解到怎么样才能将每一个房子的每一个颜色都考虑进来,很值得反复思考,让我更加深刻的理解到,动态规划是一个能够得到所有情况的最优解的办法

下面就是买卖股票类型,也是我当年的噩梦了:

5. 买卖股票的最佳时机含冷冻期(medium)

给了一个数组代表每天的股票价格,我可以多次进行买入和卖出,不过每次卖出时都会经过一天的冷冻期,然后才能再次买入

解析:

这一题买卖股票一共分为3个阶段,“买入”,“可交易”(手上没有股票,也不处于冷冻期),“冷冻期”

那么我们要考虑每一个阶段的dp,就要单独把每一个阶段单独拿出来进行考虑,给每一个阶段进行编号:

“买入” --> 0

“可交易” --> 1

“冷冻期” --> 2

1.状态表达式:

dp[i][0]表示:第i天结束后,处于“买入”状态,此时的最大利润

dp[i][1]表示:第i天结束后,处于“可交易”状态,此时的最大利润

dp[i][2]表示:第i天结束后,处于“冷冻期”状态,此时的最大利润

2.状态转移方程:

由于状态太多了,必须要画图来理清楚,不能盲目写代码:

从上面的状态机可以看到,每种状态都表示当天的状态能否发生对应的变换;

对照状态机来书写状态转移方程:

            dp[i][0]=max(dp[i-1][0],dp[i-1][1]-p[i]);
            dp[i][1]=max(dp[i-1][1],dp[i-1][2]);
            dp[i][2]=dp[i-1][0]+p[i];

3.初始化:

我最开始式多开了一行,就是为了避免越界的问题:就让p[i-1]

dp[i][0]=max(dp[i-1][0],dp[i-1][1]-p[i-1]);

初始化多开的第一行为0,但是这里我没有考虑到第1行取最大值会取到0,本应该在dp[1][0] 的位置是应该只买入的时候,当前的状态应该是负值,但是变成了0,最后影响了结果

所以这题初始化很简单,不用多开,只需要考虑第一天的所有状态即可:

dp[0][0]表示第0天只买入,dp[0][0]=-p[0];

dp[0][1]表示第0天为可交易,dp[0][1]=0;

dp[0][2]表示第0天为冷冻期,就是当天买入,当天卖出,dp[0][0]=0;

所以只用初始化dp[0][0]=-p[0]即可;

4.填表顺序:

从左往右,从上往下,依次填3个状态的dp表

5.返回值,只用返回最后一天的最大利润即可;

代码编写:

class Solution {
public:
    int maxProfit(vector<int>& p) {
        int n=p.size();
        int ret=0;
        vector<vector<int>> dp(n+1,vector<int>(3));
        dp[0][0]=-p[0];
        for(int i=1;i<n;i++)
        {
            dp[i][0]=max(dp[i-1][0],dp[i-1][1]-p[i]);
            dp[i][1]=max(dp[i-1][1],dp[i-1][2]);
            dp[i][2]=dp[i-1][0]+p[i];
            if(i==n-1) ret=max(max(dp[i][0],dp[i][1]),dp[i][2]);
        }
        return ret;
    }
};

总结:

这一题的状态机就能很好的表达出上面是多状态dp问题,多状态dp,就是要逐个分析每个状态会发生的每一种情况,才能得到最优解

6. 买卖股票的最佳时期含⼿续费(medium)

这一题上比较上一题就是没有冷冻期,但是换成了每一次完整的买卖都会有一个额外的手续费fee

解析:

有了上一题的经验,这一题就太好解决了

1.状态表达式:

这一题只有两个状态:

买入 -- > 0

可交易 --> 1

dp[i][0]表示:第i天时进行买入的最大利润

dp[i][1]表示:第i天时处于可交易状态的最大利润

2.状态转移方程:

根据上面的两个状态,来画出状态机,根据状态机来一步步秒杀状态转移方程

从买入到买入状态,就是当天什么也不干;从买入到卖出状态,就是卖掉手上的股票+p[i],这里我们规定,到卖出一只股票算是一个完整的操作,我们就要在这个时候交手续费-fee

从卖出到卖出状态,就是当天什么也不干;从卖出到买入状态,就是当天在买入一只股票-p[i]

dp[i][0]=max(dp[i-1][0],dp[i-1][1]-p[i]);
dp[i][1]=max(dp[i-1][1],dp[i-1][0]+p[i]-fee);

3.初始化:

还是只用考虑dp[0][0]第0天的时候处于买入状态:dp[0][0]=-p[i];处于可交易状态时dp[0][1]=0;就是第0天不买也不卖

4.填表顺序:

从左往右,从上往下,依次填买入和可交易两个表

5.返回值:

返回最后一天的最大值

编写代码:

class Solution {
public:
    int maxProfit(vector<int>& p, int fee) {
        int n=p.size();
        vector<vector<int>> dp(n,vector<int>(2));
        dp[0][0]=-p[0];
        for(int i=1;i<n;i++)
        {
            dp[i][0]=max(dp[i-1][0],dp[i-1][1]-p[i]);
            dp[i][1]=max(dp[i-1][1],dp[i-1][0]+p[i]-fee);
        }
        return max(dp[n-1][0],dp[n-1][1]);
    }
};

总结:

这类多状态dp问题只要画好状态机,就能迎刃而解,要耐心分析状态的每一种表示方式

7. 买卖股票的最佳时机III(hard)

最多可以进行两次交易,返回最大利润

解析:

1.状态表达式:

在分析dp状态的时候,考虑到每一个格子都有多种状态,当天可能会存在“买入” 和 “卖出”两种状态,在对每一种状态进行单独分析:

“买入” --> f[i][j]表示:第i天结束之后,完成了j次交易,此时处于“买入”状态下的,最大利润

“卖出” --> g[i][j]表示:第i天结束之后,完成了j次交易,此时处于“卖出”状态下的最大利润

这里就把每一种状态的每一天完成了几次交易分析的很透彻

2.状态转移方程:

首先就是画出状态机,然后再一步步分析每种状态的多种表示方式

这里初始化就一起顺带放在里面,为了避免初始化越界的问题把g[i][j]单独来出来考虑,当j-1>=0的时候再将g[i][j]进行比较取最大值

对于 f[i][j] ,我们有两种情况到这个状态:
i. i - 1 天的时候,交易了 j 次,处于「买⼊」状态,第 i 天啥也不⼲即可。此时最⼤利润为: f[i - 1][j]
ii. i - 1 天的时候,交易了 j 次,处于「卖出」状态,第 i 天的时候把股票买了。此时的最⼤利润为: g[i - 1][j] - prices[i]
综上,我们要的是「最⼤利润」,因此是两者的最⼤值: f[i][j] = max(f[i - 1][j], g[i - 1][j] - prices[i]) 
对于 g[i][j] ,我们也有两种情况可以到达这个状态:
i. i - 1 天的时候,交易了 j 次,处于「卖出」状态,第 i 天啥也不⼲即可。此时的最⼤利润为: g[i - 1][j]
ii. i - 1 天的时候,交易了 j - 1 次,处于「买⼊」状态,第 i 天把股票卖了,然后就完成了 j ⽐交易。此时的最⼤利润为: f[i - 1][j - 1] + prices[i] 。但是这个状态不⼀定存在,要先判断⼀下。
综上,我们要的是最⼤利润,因此状态转移⽅程为:
g[i][j] = g[i - 1][j];
if(j >= 1) g[i][j] = max(g[i][j], f[i - 1][j - 1] + prices[i]);
3. 初始化:
由于需要⽤到 i = 0 时的状态,因此我们初始化第⼀⾏即可。
当处于第 0 天的时候,只能处于「买⼊过⼀次」的状态,此时的收益为 -prices[0] ,因
f[0][0] = - prices[0]
为了取 max 的时候,⼀些不存在的状态「起不到⼲扰」的作⽤,我们统统将它们初始化为 -
INF (⽤ INT_MIN 在计算过程中会有「溢出」的⻛险,这⾥ INF 折半取
0x3f3f3f3f ,⾜够⼩即可)

4. 填表顺序:

从「上往下填」每⼀⾏,每⼀⾏「从左往右」,两个表「⼀起填」。

5. 返回值:
返回处于「卖出状态」的最⼤值,但是我们也「不知道是交易了⼏次」,因此返回 g 表最后⼀⾏的最⼤值。

代码编写:

class Solution {
public:
    int maxProfit(vector<int>& p) {
        int n=p.size();
        vector<vector<int>> f(n,vector<int>(3,-0x3f3f3f));
        auto g=f;
        f[0][0]=-p[0],g[0][0]=0;
        int ret=0;
        for(int i=1;i<n;i++)
        {
            for(int j=0;j<=2;j++)
            {
                f[i][j]=max(f[i-1][j],g[i-1][j]-p[i]);
                g[i][j]=g[i-1][j];
                if(j-1>=0) g[i][j]=max(g[i][j],f[i-1][j-1]+p[i]);
                if(i==n-1) ret=max(g[i][j],ret);
            }
        }
        return ret;
    }
};

总结:

这题很难,很细,还需要自己下去多总结,多思考~

8. 买卖股票的最佳时机IV(hard)

这一题就是上一题2次改成了k次

 解析:

1.状态表达式:

在分析dp状态的时候,考虑到每一个格子都有多种状态,当天可能会存在“买入” 和 “卖出”两种状态,在对每一种状态进行单独分析:

“买入” --> f[i][j]表示:第i天结束之后,完成了j次交易,此时处于“买入”状态下的,最大利润

“卖出” --> g[i][j]表示:第i天结束之后,完成了j次交易,此时处于“卖出”状态下的最大利润

这里就把每一种状态的每一天完成了几次交易分析的很透彻

2.状态转移方程:

首先就是画出状态机,然后再一步步分析每种状态的多种表示方式

这里初始化就一起顺带放在里面,为了避免初始化越界的问题把g[i][j]单独来出来考虑,当j-1>=0的时候再将g[i][j]进行比较取最大值

对于 f[i][j] ,我们有两种情况到这个状态:
i. i - 1 天的时候,交易了 j 次,处于「买⼊」状态,第 i 天啥也不⼲即可。此时最⼤利润为: f[i - 1][j]
ii. i - 1 天的时候,交易了 j 次,处于「卖出」状态,第 i 天的时候把股票买了。此时的最⼤利润为: g[i - 1][j] - prices[i]
综上,我们要的是「最⼤利润」,因此是两者的最⼤值: f[i][j] = max(f[i - 1][j], g[i - 1][j] - prices[i]) 
对于 g[i][j] ,我们也有两种情况可以到达这个状态:
i. i - 1 天的时候,交易了 j 次,处于「卖出」状态,第 i 天啥也不⼲即可。此时的最⼤利润为: g[i - 1][j]
ii. i - 1 天的时候,交易了 j - 1 次,处于「买⼊」状态,第 i 天把股票卖了,然后就完成了 j ⽐交易。此时的最⼤利润为: f[i - 1][j - 1] + prices[i] 。但是这个状态不⼀定存在,要先判断⼀下。
综上,我们要的是最⼤利润,因此状态转移⽅程为:
g[i][j] = g[i - 1][j];
if(j >= 1) g[i][j] = max(g[i][j], f[i - 1][j - 1] + prices[i]);
3. 初始化:
由于需要⽤到 i = 0 时的状态,因此我们初始化第⼀⾏即可。
当处于第 0 天的时候,只能处于「买⼊过⼀次」的状态,此时的收益为 -prices[0] ,因
f[0][0] = - prices[0]
为了取 max 的时候,⼀些不存在的状态「起不到⼲扰」的作⽤,我们统统将它们初始化为 -
INF (⽤ INT_MIN 在计算过程中会有「溢出」的⻛险,这⾥ INF 折半取
0x3f3f3f3f ,⾜够⼩即可)

4. 填表顺序:

从「上往下填」每⼀⾏,每⼀⾏「从左往右」,两个表「⼀起填」。

5. 返回值:
返回处于「卖出状态」的最⼤值,但是我们也「不知道是交易了⼏次」,因此返回 g 表最后⼀⾏的最⼤值。

代码编写:

class Solution {
public:
    int maxProfit(int k, vector<int>& p) {
        int n=p.size();
        vector<vector<int>> f(n,vector<int>(k+1,-0x3f3f3f));
        auto g=f;
        f[0][0]=-p[0],g[0][0]=0;
        int ret=0;
        for(int i=1;i<n;i++)
        {
            for(int j=0;j<=k;j++)
            {
                f[i][j]=max(f[i-1][j],g[i-1][j]-p[i]);
                g[i][j]=g[i-1][j];
                if(j-1>=0) g[i][j]=max(g[i][j],f[i-1][j-1]+p[i]);
                if(i==n-1) ret=max(ret,g[i][j]);
            }
        }
        return ret;
    }
};

总结:

这一题简直就是跟上一题思路一模一样,只需要改变一下j 的取值即可

总结不易~本节对我的收获很大,希望对你也是~!!!