动态规划——简单多状态 dp 问题
1. 按摩师
题目链接:面试题 17.16. 按摩师
思路一:单一状态表示
题目解析:
在数组nums中,可以选择从0位置或者1位置开始,每次可选择跳2步或者3步,最后结束点在数组倒数第一个或者倒数第二个
算法流程:
- 状态表示:dp[i]表示到达i位置时,预约的最大时间
- 状态转移方程:dp[i] = max(dp[i-2][i-3]) + nums[i]
- 初始化:dp表在前面多创建3个虚拟节点,都初始化为0,防止数组越界,注意这时nums下标与dp表下标的映射关系改变,因此,状态方程为dp[i] = max(dp[i-2][i-3]) + nums[i-3]
- 填表顺序:从左往右
- 返回值:max(dp[m+1], dp[m+2])
实现代码:
class Solution {
public int massage(int[] nums) {
//1.创建dp表
int m = nums.length;
int[] dp = new int[m+3];
//2.初始化
dp[0] = dp[1] = dp[2] = 0;
//3.填表
for(int i = 3; i < m+3; i++) {
dp[i] = Math.max(dp[i-2], dp[i-3]) + nums[i-3];
}
//4.返回值
return Math.max(dp[m+2], dp[m+1]);
}
}
思路二:多状态表示
算法流程:
- 状态表示:思路一中dp[i]表示到达i位置时,预约的最大时间。其实,这个状态还可以细化为两个状态:选择i位置的值,不选择i位置的值。因此,f[i]表示到达i位置时,选择nums[i],此时的最长预约时长;g[i]表示到达i位置时,不选择nums[i],此时的最长预约时长
- 状态转移方程:对于f[i],到达i位置选择nums[i],因此i-1位置必没有选(也就是g[i-1]),所以f[i] = g[i-1] + nums[i];对于g[i],到达i位置不选择nums[i],因此i-1位置可选(即f[i-1]),也可不选(即g[i-1]),取两者的最大值,即g[i] = max(f[i-1], g[i-1])
- 初始化:f[0] = nums[0],g[0] = 0
- 填表顺序:从左往右
- 返回值:max(f[n-1], g[n-1])
实现代码:
class Solution {
public int massage(int[] nums) {
//1.创建dp表
//2.初始化
//3.填表
//4.返回值
int n = nums.length;
int[] f = new int[n];
int[] g = new int[n];
if(n == 0) return 0;//处理边界情况
f[0] = nums[0];
for(int i = 1; i < n; i++) {
f[i] = g[i-1] + nums[i];
g[i] = Math.max(f[i-1], g[i-1]);
}
return Math.max(f[n-1], g[n-1]);
}
}
2. 打家劫舍II
题目链接:213. 打家劫舍 II
解题思路:
上一道题是单排的模式,这道题是环形的模式,即首尾相连的。但是我们可以把这个环形问题转换为两个单排问题
- 偷[0, n-2]区间的房子
- 偷[1, n-1]区间的房子
最后返回两个单排问题所得结果的最大值即可,每个单排问题和上一个问题是一样的
实现代码:
class Solution {
public int rob(int[] nums) {
int n = nums.length;
if(n == 1) return nums[0];//处理边界情况
return Math.max(rob1(nums, 0, n - 2), rob1(nums, 1, n - 1));
}
private int rob1(int[] nums, int left, int right) {
//1.创建dp表
//2.初始化
//3.填表
//4.返回值
int n = nums.length;
int[] f = new int[n];
int[] g = new int[n];
f[left] = nums[left];
for(int i = left + 1; i <= right; i++) {
f[i] = g[i-1] + nums[i];
g[i] = Math.max(g[i-1], f[i-1]);
}
return Math.max(f[right], g[right]);
}
}
3. 删除并获得点数
题目链接: 740. 删除并获得点数
算法思路:
注意题目描述,选择数字x后,就不能选择x+1 和 x-1了,这是不是很像打家劫舍问题,选择了i位置的金额,就不能选择i+1和i-1位置的金额了。因此,我们可以试着把这道题的解题方式往打家劫舍去靠
对于[1,2,3,4,5,6]这种连续且数字唯一的序列,数字值可以直接对应于打家劫舍的位置值,可以直接使用打家劫舍去做
对于[2,4,5,6,1]这种不连续或乱序的序列,就不能直接去用打家劫舍去做了,我们可以去创建一个数组[0,1,2,0,4,5,6],使其数字值对应于打家劫舍的位置值,原数组中不存在的数字值用0代替,再去用打家劫舍去做
对于[1,1,2,2,3,4,5,5,5,6]这种每个数字具有多个的情况,又该怎么办呢?当我们需要选择一个数字时,不如把所有的数字全部选上,将他们的和放入arr数组中该数字下标的位置,即[0,2,4,3,4,15,6],再去用打家劫舍去做
实现代码:
class Solution {
public int deleteAndEarn(int[] nums) {
//1. 预处理
int n = 10001;
int[] arr = new int[n];
for(int x : nums) arr[x] += x;
//2.创建dp表
int[] f = new int[n];
int[] g = new int[n];
//3.初始化
f[0] = arr[0];
//4.填表
for(int i = 1; i < n; i++) {
f[i] = g[i-1] + arr[i];
g[i] = Math.max(f[i-1], g[i-1]);
}
//5.返回值
return Math.max(f[n-1], g[n-1]);
}
}
注: 本来是要根据nums中的最大值去创建arr数组的大小,但是题目中给出了nums数组里元素的取值范围,于是就取10001作为arr数组的大小,这样就不需要将数组nums排序,找出最大值了,也是一种解题的小技巧
4. 粉刷房子
题目链接:LCR 091. 粉刷房子
算法流程:
- 状态表示:创建一个二维dp表,dp[i][0],dp[i][1],dp[i][2]分别表示粉刷到第i个房子时,将其粉刷成红色,蓝色,绿色时,此时的最小花费
- 状态转移方程:dp[i][0] = Math.min(dp[i-1][1], dp[i-1][2]) + costs[i-1][0];
dp[i][1] = Math.min(dp[i-1][0], dp[i-1][2]) + costs[i-1][1];
dp[i][2] = Math.min(dp[i-1][0], dp[i-1][1]) + costs[i-1][2]; - 初始化:创建表时,可以多创建一列(第0列),初始化为0
- 填表顺序:三个表同时从左往右填
- 返回值:返回三个表的最后一个节点的最小值,即dp[n][0],dp[n][1],dp[n][2]三者的最小值
实现代码:
class Solution {
public int minCost(int[][] costs) {
//1. 建表
//2.初始化
//3.填表
//4.返回值
int n = costs.length;
int[][] dp = new int[n+1][3];
for(int i = 1; i < n+1; i++) {
dp[i][0] = Math.min(dp[i-1][1], dp[i-1][2]) + costs[i-1][0];
dp[i][1] = Math.min(dp[i-1][0], dp[i-1][2]) + costs[i-1][1];
dp[i][2] = Math.min(dp[i-1][0], dp[i-1][1]) + costs[i-1][2];
}
return Math.min(Math.min(dp[n][0], dp[n][1]), dp[n][2]);
}
}
5.买卖股票的最佳时机含冷冻期
题目链接:309. 买卖股票的最佳时机含冷冻期
算法流程:
- 状态表示:dp[i]表示当第i天结束时,所得的最大利润。而dp可以划分为几个状态:(一)第i天结束时,处于手中有股票的状态,即后面一天可进行卖出操作。(二)第i天结束时,处于冷冻期的状态即当天卖出股票,后一天不能进行买入操作。(三)第i天结束时,处于手中无股票的状态,即后面一天可进行买入操作。注意:手中有股票的状态到达手中无股票的状态,必须经过冷冻期状态
因此,用dp[i][0]表示第i天结束后,处于手中有股票的状态,所得的最大利润,dp[i][1]表示第i天结束后,处于手中无股票的状态,所得的最大利润,dp[i][2]表示第i天结束后,处于冷冻期的状态,所得的最大利润 - 状态转移方程:
- dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - price[i])
- dp[i][1] = max(dp[i - 1][1], dp[i-1][2])
- dp[i][2] = dp[i-1][0] + price[i]
- 初始化:
- dp[0][0]:第1天结束后,手中有股票,因此第一天买入了股票,因此利润为-price[0]
- dp[0][1]:第1天结束后,手中还是没有股票,因此第一天什么都没干,因此利润为0
- dp[0][2]:第1天结束后,处于冷冻期,因此第一天买入后又卖出了,因此利润为0
- 填表顺序:三个表同时从左往右填
- 返回值:最大利润不可能是最后一天结束后手里还有股票,因此最后返回max(dp[n-1][1], dp[n-1][2])。(返回三者的最大值也是可以的)
实现代码:
class Solution {
public int maxProfit(int[] prices) {
//1.创建dp表
//2.初始化
//3.填表
//4.返回值
int n =prices.length;
int[][] dp = new int[n][3];
dp[0][0] = -prices[0];
dp[0][1] = 0;
dp[0][2] = 0;
for(int i = 1; i < n; i++) {
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1]-prices[i]);
dp[i][1] = Math.max(dp[i-1][1], dp[i-1][2]);
dp[i][2] = dp[i-1][0] + prices[i];
}
return Math.max(dp[n-1][1], dp[n-1][2]);
}
}
6. 买卖股票的最佳时机含手续费
题目链接:714. 买卖股票的最佳时机含手续费
算法流程:
- 状态表示:p[i][0]表示第i天结束后,处于手中有股票的状态,所得的最大利润;dp[i][1]表示第i天结束后,处于手中无股票的状态,所得的最大利润
画出状态机
我们不妨将买入股票并卖出股票要收取的手续费,放在买入时收取一次,当卖出时不收取即可
- 状态转移方程:dp[i][0] = max(dp[i-1][0], dp[i-1][1] - price[i] - fee); dp[i][1] = max(dp[i][0] + price[i], dp[i][1])
- 初始化:dp[0][0] = -price[0] - fee; dp[0][1] = 0;
- 填表顺序:两张表同时从左往右填
- 返回值:dp[n-1][1]
实现代码:
class Solution {
public int maxProfit(int[] prices, int fee) {
int n = prices.length;
int[][] dp = new int[n][2];
dp[0][0] = -prices[0]-fee;
for(int i = 1; i < n; i++) {
dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1]-prices[i]-fee);
dp[i][1] = Math.max(dp[i-1][0]+prices[i], dp[i-1][1]);
}
return dp[n-1][1];
}
}
7. 买卖股票的最佳时机 III
题目链接:123. 买卖股票的最佳时机 III
算法流程:
- 状态表示:f[i][j]表示第i天结束后,完成了j笔交易,这时手中有股票,此时的最大利润;g[i][j]表示第i天结束后,完成了j笔交易,这时手中无股票,此时的最大利润
画出状态机:
-
状态转移方程:f[i][j] = max(f[i-1][j], g[i-1][j]-price[i]); g[i][j] = max(g[i-1][j], f[i-1][j-1]+price[i])
-
初始化:需要初始化f的第一行和第一列,初始化g的第一行
对于f,除了可以多创建一行和一列外,还有一个技巧:只初始化第一行,当j-1存在时,才进行状态转移方程计算
但是,如果将负无穷设置为int类型的最小值时,g[i-1][j]-price[i]会超出int类型的范围,从而变成一个很大的一个值,即溢出,因此我们可以规定无穷大为0x3f3f3f3f,正好是int类型最大值的一半,足够小即可 -
填表顺序:从左往右,从上往下,两张表同时填
-
返回值:g[n-1][0],g[n-1][1],g[n-1][2]三者的最大值
代码实现:
class Solution {
public int maxProfit(int[] prices) {
//1.创建dp表
//2.初始化
//3.填表
//4.返回值
int n = prices.length;
int[][] f = new int[n][3];
int[][] g = new int[n][3];
int INF = 0x3f3f3f3f; //无穷大
for(int j = 0; j < 3; j++) f[0][j] = g[0][j] = -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] = Math.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] = Math.max(g[i-1][j], f[i-1][j-1] + prices[i]);
}
}
}
return Math.max(Math.max(g[n-1][0], g[n-1][1]), g[n-1][2]);
}
}
8. 买卖股票的最佳时机 IV
思路和上题是一致的,故不再赘述
代码如下:
class Solution {
public int maxProfit(int k, int[] prices) {
//1. 创建dp表
//2. 初始化
//3. 填表
//4. 返回值
int INF = 0x3f3f3f3f;
int n = prices.length;
int[][] f = new int[n][k+1];
int[][] g = new int[n][k+1];
for(int j = 0; j <= k; j++) f[0][j] = g[0][j] = -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] = Math.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] = Math.max(g[i-1][j], f[i-1][j-1] + prices[i]);
}
}
int ret = 0;
for(int j = 0; j <= k; j++) {
if(ret < g[n-1][j]) ret = g[n-1][j];
}
return ret;
}
}