当前位置: 首页 > news >正文

动态规划之多状态 dp 问题

题型概述

什么时候该用多状态解决动态规划问题

有些情况下,用一个 dp 表就足以充分表示每一个位置上代表的含义,也可以保证在每一种情况下,都能用 dp 表求出正确答案。

但有些情况下,一个 dp 表只能表示一种情况下的答案,并不能涵盖所有可能性,这时就需要借助多个 dp 表以确保答案正确。

例题

力扣 面试题 17.16 按摩师

在这道题中,如果按照以往的习惯,dp[i] 表示到 i 位置时,最长的预约时间,那么还有一个信息无法表示----该位置的前一个位置有没有被加到总的预约时间里?

所以,针对这个问题,可以采用两个 dp 表,分别用来表示某一个位置被加入到了总的预约时间,以及没有被加入到总的预约时间时,到该位置时可以达到的最长预约时间。假设两个 dp 表分别为 f 和 g。

解题步骤:

创建 dp 表以及确定 dp 表中所要填写位置的含义:

首先,根据写题经验,先确定出这道题应该使用的解题思路是 “以某一个位置为结尾进行分析”。

f[i] 表示,到 i 位置时,并且将该位置加入到总的预约时间中,这时可以达到的最长预约时间。

g[i] 表示,到 i 位置时,并且不将该位置加入到总的预约时间中,这时可以达到的最长预约时间。

确定状态转移方程:

对于 f[i],由于该位置要被加入到总的预约时间,所以需要保证前一个位置一定不能加入总的时间中,所以 f[i] = g[i - 1] + nums[i]。

对于 g[i],由于该位置没有加入到总的预约时间,所以前一个位置可以加入到总的预约时间,也可以不加入,所以取它们的最大值即可,即 g[i] = max(g[i - 1], f[i - 1])。

初始化:

只需要将 f[0] = nums[0] 即可,表示第一个位置被加入到总的预约时间的情况。

确定填表顺序:

根据状态转移方程可以看出应该从从左到右依次填入数据。

确定返回值:

该题要求返回针对整个数组,可以达到的最长的总的预约时间,最长预约时间可能包含最后一个位置,也可能不包含,所以应该返回 f 和 g 表中最后一个位置的最大值。

代码:

class Solution 
{
public:int massage(vector<int>& nums) {// 创建 dp 表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> f(n), g(n);// 初始化f[0] = nums[0];// 填表for (int i = 1; i < n; i++){f[i] = g[i - 1] + nums[i];g[i] = max(g[i - 1], f[i - 1]);}return max(f[n - 1], g[n - 1]);}
};

练习题

力扣 213. 打家劫舍(二)

力扣 213. 打家劫舍(二)

这道题只需要针对第一个和最后一个位置是否被占用,分成两种情况考虑即可。

如果第一个房子被占用,则最后一个房子不能被占用。

所以,可以针对第一个房子到倒数第二个房子,以及第二个房子到最后一个房子,分别做一次类似于上一道题的操作即可。

最后应该返回两种情况得到的最大值,具体原理不再赘述。

代码:

class Solution 
{
public:int Rob(vector<int>& nums, int left, int right){if (left > right) return 0;int n = nums.size();vector<int> f(n), g(n);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]);}int rob(vector<int>& nums) {int n = nums.size();if (n == 0) return 0;if (n == 1) return nums[0];return max(Rob(nums, 0, n - 2), Rob(nums, 1, n - 1));}
};

力扣 740. 删除并获得点数

力扣 740. 删除并获得点数

这道题求的是满足条件的最大数字和, 其实跟打家劫舍问题很像。

只是需要把这道题中的数组转化为一个更接近打家劫舍的数组以供操作。

所以考虑创建一个数组arr, arr[i] 表示 nums[i] 中所有出现的数字 i 的总和。

所以该问题就转化为 "选了一个数之后, 不能再选择与它相邻的数", 标准的打家劫舍问题 。

class Solution 
{
public:int deleteAndEarn(vector<int>& nums) {vector<int> arr(10001);for (int num : nums) arr[num] += num;vector<int> f(10001), g(10001);for (int i = 1; i <= 10000; i++){f[i] = g[i - 1] + arr[i];g[i] = max(g[i - 1], f[i - 1]);}return max(f[10000], g[10000]);}
};

力扣 91. 粉刷房子

力扣 91. 粉刷房子

这道题可以创建 3 个 dp 表,也可以直接创建一个二维的 dp 表,在其中包含每个房子分别粉刷成 3 种颜色的情况。

我这里为了不用自己初始化第一个房子,直接用 costs 数组创建一个 dp 表。

dp 表中的每一行有 3 个数据,分别对应 3 种颜色,针对每一种颜色,只需要在 dp 表原有的基础上, 再加上上一行中另外两列的最小值即可(dp 表中每一个位置已经有每一种颜色本身的花费了,因为是由 costs 初始化而来的,如果这个位置选了某一个颜色,则上一个位置一定不是这个颜色,所以在另外两列找最小值)

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

力扣 309. 买卖股票的最佳时期含冷冻期

力扣 309. 买卖股票的最佳时期含冷冻期

这道题针对每一天可以细分为三种状态:

买入(在当天买入股票) 可交易(当天没有股票, 可以买入) 冷冻期(当天没有股票, 也不能买入)。

创建一个 dp 表, dp[i][0] dp[i][1] dp[i][2] 分别表示第 i 天结束之后, 上述三种状态下的最大利润。

针对这三种状态分别分析, 确定每一种状态能不能以及怎么做才能转化为另外两种状态。

买入: 前一天买入股票, 这一天什么都不干, 可以继续处于买入状态。

前一天买入股票, 这一天卖出股票, 可以转化为冷冻期。

前一天买入股票, 今天不能转化为可交易状态, 因为需要提前卖出股票, 还要经过冷冻期。

可交易状态: 前一天没有股票, 今天买入, 就可以转化为买入状态。

前一天没有股票, 今天什么都不干, 依然是可交易状态。

前一天没有股票, 无法到达冷冻期, 因为需要经过买入和卖出。

冷冻期: 前一天冷冻期, 今天不可能还是冷冻期。

前一天冷冻期, 今天什么都不干, 就转化为了可交易状态。

前一天冷冻期, 今天不能买入股票, 无法转化为买入状态。

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

力扣 714. 买卖股票的最佳时期含手续费

力扣 714. 买卖股票的最佳时期含手续费

没有冷冻期, 每天要么是买入(手里有股票), 要么是卖出(手里没有股票)。

并且这两种状态都可以相互转换, 也都可以保持该状态不变。

代码:

class Solution 
{
public:int maxProfit(vector<int>& prices, int fee) {int n = prices.size();vector<int> f(n), g(n);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];}
};

力扣 123. 买卖股票的最佳时机(三)

力扣 123. 买卖股票的最佳时机(三)

这道题不同之处在于, 它限制了交易的次数。

所以, 除了要记录每一天是买入还是卖出状态之外, 还要记录当前是第几次交易。

f[i][j] 表示第 i 天处于买入状态下, 且已经完成了 j 笔交易时, 最大获利。

g[i][j] 表示第 i 天处于卖出状态下, 且已经完成了 j 笔交易时, 最大获利。

代码:

class Solution 
{const int INF = -0x3f3f3f3f;
public:int maxProfit(vector<int>& prices) {int n = prices.size();vector<vector<int>> f(n, vector<int>(3, INF)), g(n, vector<int>(3, INF));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]);g[i][j] = g[i - 1][j];if (j - 1 >= 0) g[i][j] = max(g[i][j], f[i - 1][j - 1] + prices[i]);}}return *max_element(g[n - 1].begin(), g[n - 1].end());}
};

力扣 188. 买卖股票的最佳时机(四)

力扣 188. 买卖股票的最佳时机(四)

这道题跟上面的一模一样,只是需要把第二层循环中的 j 控制为 <= k 即可。原理不再赘述。

代码:

class Solution 
{const int INF = -0x3f3f3f3f;
public:int maxProfit(int k, vector<int>& prices) {int n = prices.size();vector<vector<int>> f(n, vector<int>(k + 1, INF)), g(n, vector<int>(k + 1, INF));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]);g[i][j] = g[i - 1][j];if (j - 1 >= 0) g[i][j] = max(g[i][j], f[i - 1][j - 1] + prices[i]);}}return *max_element(g[n - 1].begin(), g[n - 1].end());}
};

总结

多状态 dp 问题要注意以下几点:

  1. 创建 dp 表的个数取决于,需要多少个 dp 表才能完全表示出所需要的每一种状态。

  2. 对 dp 表的初始化要确保不会影响后面填表的正确性。

  3. 对于解题思路中的每一种状态,要分析它经过某种操作是否能到达其他状态或保持不变,这是推导状态转移方程的关键所在。

  4. 最后的返回值一定要再次分析题目,确定返回的是符合题目要求的答案。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 十三、Kafka(系列)-Kafka入门(重试机制)
  • springboot系列教程(三十一):springboot整合Nacos组件,环境搭建和入门案例详解
  • 【Qt】为什么Qt是你选择的理由?
  • Android渠道配置不同依赖性
  • 小程序商品图片有什么要求
  • 使用 OpenCV 进行轮廓处理和图像保存
  • flink 1.17 测试
  • VSCode上安装C#环境教程
  • springboot+vue+mybatis音乐网站的设计+PPT+论文+讲解+售后
  • kafka cmd
  • 酸性蓄电池的结构与工作原理是什么?
  • 日常进度提醒
  • 【轨物推荐】经济长波:创新周期的历史
  • Python:第三课:重要API - 集合类
  • 注册或购买的谷歌账号的辅助邮箱是否需要设置?有什么用?设置的要点是什么?
  • android图片蒙层
  • Centos6.8 使用rpm安装mysql5.7
  • ComponentOne 2017 V2版本正式发布
  • ES2017异步函数现已正式可用
  • JS学习笔记——闭包
  • Linux编程学习笔记 | Linux IO学习[1] - 文件IO
  • SpringBoot 实战 (三) | 配置文件详解
  • 翻译:Hystrix - How To Use
  • 观察者模式实现非直接耦合
  • 后端_MYSQL
  • 漂亮刷新控件-iOS
  • 前端工程化(Gulp、Webpack)-webpack
  • 如何用vue打造一个移动端音乐播放器
  • 如何在GitHub上创建个人博客
  • 原生Ajax
  • ​1:1公有云能力整体输出,腾讯云“七剑”下云端
  • ​iOS安全加固方法及实现
  • ​Redis 实现计数器和限速器的
  • ​补​充​经​纬​恒​润​一​面​
  • # Redis 入门到精通(七)-- redis 删除策略
  • # 利刃出鞘_Tomcat 核心原理解析(二)
  • # 透过事物看本质的能力怎么培养?
  • ###C语言程序设计-----C语言学习(3)#
  • #android不同版本废弃api,新api。
  • $(document).ready(function(){}), $().ready(function(){})和$(function(){})三者区别
  • (LLM) 很笨
  • (附源码)spring boot校园拼车微信小程序 毕业设计 091617
  • (黑马C++)L06 重载与继承
  • (六)什么是Vite——热更新时vite、webpack做了什么
  • (三分钟了解debug)SLAM研究方向-Debug总结
  • (算法)硬币问题
  • (转)Scala的“=”符号简介
  • .bat批处理(二):%0 %1——给批处理脚本传递参数
  • .java 指数平滑_转载:二次指数平滑法求预测值的Java代码
  • .md即markdown文件的基本常用编写语法
  • .net core 管理用户机密
  • .NET Framework杂记
  • .net refrector
  • .net 生成二级域名
  • .Net 中的反射(动态创建类型实例) - Part.4(转自http://www.tracefact.net/CLR-and-Framework/Reflection-Part4.aspx)...