【动态规划】简单多状态dp问题

一、经验总结

在分析dp问题的状态表示时,发现当前阶段的状态可以继续细分为多个状态,且多个状态之间可以通过某种方式进行转换,这就是动态规划的多状态问题。

多状态问题的关键有以下几点:

  1. 找出dp问题的多个状态表示:经验+题目要求,在之前经验的基础上(以i位置为起点或终点)继续对当前阶段的状态进行细分。为每种状态各自创建出一个dp表。每种状态可能还有若干子状态,此时应该为每种状态的dp表增加维度,以表示出所有的子状态。

  2. 确定多个状态间的相互转换关系:对于简单题目可以直接得到转换关系(前4题);但是如果每个阶段的状态数量较多,状态间的相互转换较为复杂,可以借助状态机模型分析各状态间的转换关系(后4题),其中:

    1. 各顶点表示的是每个阶段的各种状态(在第一步确定状态表示时得出)
    2. 箭头的起点是前一阶段的状态
    3. 箭头的终点是当前阶段的状态
    4. 箭头上的动作是状态转换时需要进行的活动(当前阶段)

    如何绘制状态机?

    1. 先画出每个阶段的各种状态
    2. 对于每种状态,先考虑自己能不能转换为自己(状态不改变)
    3. 再考虑其他状态能不能转换为该状态
    4. 标出状态转换时需要进行的活动(也可以啥也不干)
  3. 之后就可以依据转换关系写出各种状态的状态转移方程了。最好依据绘制状态机的顺序写方程,做到不重不漏的计算出所有状态的结果。

  4. 填表顺序:要按照顺序将创建的多个dp表一起填

  5. 返回值:要考虑到所有的状态


二、相关编程题

2.1 按摩师

题目链接

面试题 17.16. 按摩师 - 力扣(LeetCode)

题目描述

在这里插入图片描述

算法原理

在这里插入图片描述

编写代码

class Solution {
public:
    int massage(vector<int>& nums) {
        //1.创建dp表
        //2.初始化
        //3.填表
        //4.返回值
        int n = nums.size();
        if(n == 0) return 0;
        vector<int> dp1(n), dp2(n);
        dp1[0] = nums[0];
        for(int i = 1; i < n; ++i)
        {
            dp1[i] = dp2[i-1] + nums[i];
            dp2[i] = max(dp1[i-1], dp2[i-1]);
        }
        return max(dp1[n-1], dp2[n-1]);
    }
};

2.2 打家劫舍Ⅱ

题目链接

LCR 090. 打家劫舍 II - 力扣(LeetCode)

题目描述

在这里插入图片描述

算法原理

在这里插入图片描述

编写代码

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

    int rob1(vector<int>& nums, int begin, int end) {
        if(begin >= end) return 0;
        int n = end-begin; //左闭右开
        vector<int> dp1(n), dp2(n); //dp1偷/dp2不偷
        dp1[0] = nums[begin];
        for(int i = 1; i < n; ++i)
        {
            dp1[i] = dp2[i-1] + nums[begin+i]; //注意下标映射关系
            dp2[i] = max(dp1[i-1], dp2[i-1]);
        }
        return max(dp1[n-1], dp2[n-1]);
    }
};

2.3 删除并获得点数

题目链接

740. 删除并获得点数 - 力扣(LeetCode)

题目描述

在这里插入图片描述

算法原理

在这里插入图片描述

编写代码

class Solution {
public:
    int deleteAndEarn(vector<int>& nums) {
        const int N = 10001;
        int arr[N] = {0}; //该数组用于统计每个数字出现的总和
        for(auto e : nums)
            arr[e]+=e;
        vector<int> dp1(N), dp2(N); //dp1获取,dp2不获取
        dp1[1] = arr[1];
        for(int i = 2; i < N; ++i)
        {
            dp1[i] = dp2[i-1]+arr[i];
            dp2[i] = max(dp1[i-1], dp2[i-1]);
        }
        return max(dp1[N-1], dp2[N-1]);
    }
};

2.4 粉刷房子

题目链接

LCR 091. 粉刷房子 - 力扣(LeetCode)

题目描述

在这里插入图片描述

算法原理

在这里插入图片描述

编写代码

class Solution {
public:
    int minCost(vector<vector<int>>& costs) {
        int n = costs.size();
        vector<vector<int>> dp(n+1, vector<int>(3));
        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][1], dp[i-1][0])+costs[i-1][2];
        }
        return min(dp[n][0], min(dp[n][1], dp[n][2]));
    }
};

2.5 买卖股票的最佳时机含冷冻期

题目链接

309. 买卖股票的最佳时机含冷冻期 - 力扣(LeetCode)

题目描述

在这里插入图片描述

算法原理

在这里插入图片描述

如果每个阶段的状态数量较多,状态间的相互转换较为复杂,可以借助状态机模型分析各状态间的转换关系,其中:

  1. 各顶点表示的是每个阶段的各种状态(在第一步确定状态表示时得出)
  2. 箭头的起点是前一阶段的状态
  3. 箭头的终点是当前阶段的状态
  4. 箭头上的动作是状态转换时需要进行的活动(当前阶段)

如何绘制状态机?

  1. 先画出每个阶段的各种状态
  2. 对于每种状态,先考虑自己能不能转换为自己(状态不改变)
  3. 再考虑其他状态能不能转换为该状态
  4. 标出状态转换时需要进行的活动

编写代码

class Solution {
public:
    int maxProfit(vector<int>& p) {
        int n = p.size();
        vector<vector<int>> dp(n, 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];
        }
        return max(dp[n-1][1], dp[n-1][2]);
    }
};

2.6 买卖股票的最佳时机含手续费

题目链接

714. 买卖股票的最佳时机含手续费 - 力扣(LeetCode)

题目描述

在这里插入图片描述

算法原理

在这里插入图片描述

编写代码

class Solution {
public:
    int maxProfit(vector<int>& prices, int fee) {
        int n = prices.size();
        vector<int> f(n), g(n); //f是买入状态,g是可交易状态
        f[0] = -prices[0];
        for(int i = 1; i < n; ++i)
        {
            f[i] = max(f[i-1], g[i-1]-prices[i]);
            g[i] = max(g[i-1], f[i-1]+prices[i]-fee);
        }
        return g[n-1]; //f[n-1]最后一天还有股票没有卖出,一定不是最大利润,不考虑
    }
};

2.7 买卖股票的最佳时机III

题目链接

123. 买卖股票的最佳时机 III - 力扣(LeetCode)

题目描述

在这里插入图片描述

算法原理

在这里插入图片描述

为什么INF(无穷大)用0x3f3f3f3f表示?请阅读文章:0x3f3f3f3f是什么意思-CSDN博客

用0x3f3f3f3f表示32-bit int最大值的好处:

  1. 足够大:0x3f3f3f3f是10^9级别的,比一般场合下的数据都要大,所以它可以作为无穷大使用,而不至于出现数据大于无穷大的情况。
  2. 加数据不会溢出:0x3f3f3f3f加上一个数据时并不会溢出,也就是说“无穷大加上一个有穷的数依然是无穷大”。甚至0x3f3f3f3f + 0x3f3f3f3f依然没有超过32-bit int的表示范围,也就满足了我们“无穷大加无穷大还是无穷大的需求”。
  3. 可以使用memset设置内存:之前如果我们要将某个int数组全部赋值为INT_MAX,需要自己写循环,不能使用memset,因为memset是以字节为单位进行赋值的。现在好了,如果我们将无穷大设为0x3f3f3f3f,由于它的每个字节都是0x3f,我们可以通过memset(arr, 0x3f, sizeof(arr));将数组全部置为无穷大。

编写代码

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        int INF = 0x3f3f3f3f; //无穷大
        vector<vector<int>> f(n, vector<int>(3, -INF)), g(n, vector<int>(3, -INF));
        //边界1:第0天第0次交易,其余交易次数的收益初始化为-∞
        f[0][0] = -prices[0];
        g[0][0] = 0;
        for(int i = 1; i < n; ++i)
        {
            for(int j = 0; j < 3; ++j)
            {
                f[i][j] = max(f[i-1][j], g[i-1][j]-prices[i]);
                //边界2:第0次交易时卖出状态
                if(j == 0) 
                    g[i][j] = 0;
                else
                    g[i][j] = max(g[i-1][j], f[i-1][j-1]+prices[i]);
            }
        }   
        return max(g[n-1][0], max(g[n-1][1], g[n-1][2]));
    }
};

2.8 买卖股票的最佳时机Ⅳ

题目链接

188. 买卖股票的最佳时机 IV - 力扣(LeetCode)

题目描述

在这里插入图片描述

算法原理

在这里插入图片描述

编写代码

class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {
        int n = prices.size();
        k = min(k, n/2); //n天最多进行n/2次有效交易
        const int INF = 0x3f3f3f3f;
        vector<vector<int>> f(n, vector<int>(k+1, -INF));
        auto g = f;
        f[0][0] = -prices[0];
        g[0][0] = 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]-prices[i]);
                if(j == 0)
                    g[i][j] = 0;
                else
                    g[i][j] = max(g[i-1][j], f[i-1][j-1]+prices[i]);
            }
        }
        int ret = -INF;
        for(int j = 0; j <= k; ++j)
        {
            ret = max(ret, g[n-1][j]);
        }
        return ret;
    }
};
  • 22
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

芥末虾

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

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

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

打赏作者

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

抵扣说明:

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

余额充值