动态规划 之 简单多状态 dp 问题 算法专题
一. 按摩师
按摩师
- 状态表示
根据经验 + 题目要求
dp[i] 表示: 选择到i位置时, 此时的最长预约时长
但是根据题目又分成两种情况:
f[i] : 选择到 i 位置的时候, nums[i] 必选, 此时的最长预约时长
g[i] : 选择到 i 位置的时候, nums[i] 不选, 此时的最长预约时长 - 状态转移方程
f[i] 如果i 位置必选, 那么f[i - 1] 位置必不选, 就等于g[i - 1] + nums[i]
g[i] 如果i位置不选, 那么g[i - 1] 位置有两种情况, 如果[i - 1] 选, 那么就=f[i - 1] ,如果[i - 1] 不选, 那么就=g[i - 1] , 但是我们要的是最长时长, 所以
- f[i] = g[i - 1] + nums[i]
- g[i] = max(f[i - 1], g[i - 1])
- 初始化
f[0] = nums[0] g[0] = 0; - 填表顺序
从左往右 两个表一起填 - 返回值
返回max(f[n], g[n])
class Solution {
public int massage(int[] nums) {
//1. 建表
//2. 初始化
//3. 填表
//4. 返回值
int n = nums.length;
if(n == 0) return 0;
int[] f = new int[n];
int[] g = new int[n];
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]);
}
}
二. 打家劫舍II
打家劫舍II
分析: 通过分类讨论, 可以将环形的问题转化为线性问题
1.如果第0家偷, 那么第一家和最后一家就不能偷, 所以第二家和倒数第二家之间就可以按照正常的情况考虑
2.如果第0家不偷, 那么剩下的都可以正常进行了
- 状态表示
根据经验 + 题目要求
dp[i] 表示: 选择到i位置时, 此时偷的最大金额
但是根据题目又分成两种情况:
f[i] : 选择到 i 位置的时候, nums[i] 必选, 此时偷的最大金额
g[i] : 选择到 i 位置的时候, nums[i] 不选, 此时偷的最大金额 - 状态转移方程
f[i] 如果i 位置必选, 那么f[i - 1] 位置必不选, 就等于g[i - 1] + nums[i]
g[i] 如果i位置不选, 那么g[i - 1] 位置有两种情况, 如果[i - 1] 选, 那么就=f[i - 1] ,如果[i - 1] 不选, 那么就=g[i - 1] , 但是我们要的是偷的最大金额, 所以
- f[i] = g[i - 1] + nums[i]
- g[i] = max(f[i - 1], g[i - 1])
- 初始化
f[0] = nums[0] g[0] = 0; - 填表顺序
从左往右 两个表一起填 - 返回值
返回第0家偷和不偷的最大值
class Solution {
int[] f;
int[] g;
int[] nums;
public int rob(int[] _nums) {
// 1. 建表
// 2. 初始化
// 3. 填表
// 4. 返回值
nums = _nums;
int n = _nums.length;
int x = rob1(2, n - 2) + nums[0];
int y = rob1(1, n - 1);
return Math.max(x, y);
}
public int rob1(int left, int right) {
if(left > right) return 0;
int n = nums.length;
f = new int[n];
g = new int[n];
f[left] = nums[left];
for (int i = left; 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]);
}
}
三. 删除并获得点数
删除并获得点数
分析: 由于是删除大于或和小于的数, 也就是说相邻的数是不能再选的, 那么和打家劫舍问题是类似的
但是这些数并不是相邻的并且无序, 所以我们需要重新定义一个数组arr来存储
点数代表下标, 对应的值就是该点数的总和
- 状态表示
根据经验 + 题目要求
dp[i] 表示: 选择到i位置时, 此时获得的最大点数
但是根据题目又分成两种情况:
f[i] : 选择到 i 位置的时候, nums[i] 必选, 此时获得的最大点数
g[i] : 选择到 i 位置的时候, nums[i] 不选, 此时获得的最大点数 - 状态转移方程
f[i] 如果i 位置必选, 那么f[i - 1] 位置必不选, 就等于g[i - 1] + arr[i]
g[i] 如果i位置不选, 那么g[i - 1] 位置有两种情况, 如果[i - 1] 选, 那么就=f[i - 1] ,如果[i - 1] 不选, 那么就=g[i - 1] , 但是我们要的是获得的最大点数, 所以
- f[i] = g[i - 1] + nums[i]
- g[i] = max(f[i - 1], g[i - 1])
- 初始化
f[0] = arr[0] g[0] = 0; - 填表顺序
从左往右 两个表一起填 - 返回值
返回max(f[n - 1], g[n - 1])
class Solution {
public int deleteAndEarn(int[] nums) {
//预处理
int n = 10001;
int[] arr = new int[n];
for(int x: nums) arr[x] += x;
//建表
//初始化
//填表
//返回值
int[] f = new int[n];
int[] g = new int[n];
f[0] = arr[0];
for(int i = 1; i < n; i++){
f[i] = g[i - 1] + arr[i];
g[i] = Math.max(f[i - 1], g[i - 1]);
}
return Math.max(f[n - 1], g[n - 1]);
}
}
四, 粉刷房子
粉刷房子
- 状态表示
根据经验 + 题目要求
dp[i] 表示: 选择到i位置时, 此时粉刷的最小花费
但是根据题目又分成两种情况:
如果第i个房子, 粉刷的是红色的, 那么前一个房子只能是蓝色或绿色, 同理其他情况
所以dp可以弄成二维数组, dp[i][j] 表示最小费用, j表示颜色 0红色,1蓝色,2绿色 - 状态转移方程
1.如果i选择红色, dp[i][0], 那么dp[i - 1]只能选择[1][2], 要的是最小花费, 所以
- dp[i][0] = Math.min(dp[i - 1][1], dp[i - 1][2]) + costs[i - 1][0]
2.如果i选择蓝色, dp[i][0], 那么dp[i - 1]只能选择[1][2], 要的是最小花费, 所以
- dp[i][1] = Math.min(dp[i - 1][0], dp[i - 1][2]) + costs[i - 1][1]
3.如果i选择红色, dp[i][0], 那么dp[i - 1]只能选择[1][2], 要的是最小花费, 所以
- dp[i][2] = Math.min(dp[i - 1][0], dp[i - 1][1]) + costs[i - 1][2]
- 初始化
本道题初始化较为复杂, 所以使用虚拟节点来帮助我们初始化, 要注意两个注意事项: 初始化与对应关系
最开始是0的位置最小花费应该初始化为0, 因为还没有花费 - 填表顺序
从左往右 两个表一起填 - 返回值
返回min(dp[n][0], dp[n][1], dp[n][2])
class Solution {
public int minCost(int[][] costs) {
int n = costs.length;
int[][] dp = new int[n + 1][3];
for(int i = 1; i <= n; i++){//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]);
}
}
五. 买卖股票的最佳时机含冷冻期
买卖股票的最佳时机含冷冻期
- 状态表示
根据经验 + 题目要求
dp[i] 表示: 选择到第i天位置时, 此时的最大利润
但是到第i天又分成三种情况:
可能处于"买入"状态, "可交易"状态, "冷冻期"状态
所以用二维数组[i][j]状态表示 0"买入"状态, 1"可交易"状态, 2"冷冻期"状态 - 状态转移方程
1.如果处于"买入"状态, 那么第i - 1天, 可能处于"买入"状态(i - 1天买完后第i天没卖, 等于啥也没干), 可能处于"可交易"状态(i - 1天不是冷冻期, 可以进行购买), 此时是需要-价格, 进行购买, 由于要最大利润, 对两次状态的结果取最大值
- dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i])
2.如果处于"可交易"状态, 那么第i - 1天, 可能处于"可交易"状态(i - 1天之前就卖了, 第i天还没买, 等于啥也没干), 可能处于"冷冻期"状态(i - 1天卖了, 第i天还没买, 等于啥也没干), 由于要最大利润, 对两次状态的结果取最大值
- dp[i][1] = max(dp[i - 1][1], dp[i - 1][2])
3.如果处于"冷冻期"状态, 那么第i - 1天, 可能处于"买入"状态(i - 1天买了, 第i天卖了), , 此时是需要+价格, 进行购买,
- dp[i][2] =dp[i - 1][0] + price[i]
- 初始化
只需对0位置进行初始化
dp[0][0] 说明买了股票, 利润为-prices[0]
dp[0][1] 说明可以买, 利润为0
dp[0][2] 说明冷冻期, 利润为0 - 填表顺序
从左往右 三个表一起填 - 返回值
返回max(dp[n - 1][0], dp[n - 1][1], dp[n - 1][2])
class Solution {
public int maxProfit(int[] prices) {
int n = prices.length;
int[][] dp = new int[n][3];
dp[0][0] = -prices[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(Math.max(dp[n - 1][0], dp[n - 1][1]), dp[n - 1][2]);
}
}
六. 买卖股票的最佳时机含手续费
买卖股票的最佳时机含手续费
- 状态表示
根据经验 + 题目要求
dp[i] 表示: 选择到第i天位置时, 此时的最大利润 - 状态转移方程
第i天位置时, 有两种情况
1.可能处于"买入"状态
那么i - 1 位置可能是"买入"状态(啥也不干), 可能是"可交易"状态(买股票)
f[i] = max(f[i - 1], g[i - 1] - p[i])
2.可能处于"可交易"状态
那么i - 1 位置可能是"买入"状态(卖股票, 再加上手续费), 可能是"可交易"状态(啥也不干)
g[i] = max(f[i - 1] + p[i] - fee, g[i - 1]) - 初始化
f[0] = -p[0] g[0] = 0 - 填表顺序
从左往右 两个表一起填 - 返回值
max(g[n - 1], f[n - 1])
class Solution {
public int maxProfit(int[] prices, int fee) {
int n = prices.length;
int[] f = new int[n];
int[] g = new int[n];
f[0] = -prices[0];
for(int i = 1; i < n; i++){
f[i] = Math.max(f[i - 1], g[i - 1] - prices[i]);
g[i] = Math.max(f[i - 1] + prices[i] - fee, g[i - 1]);
}
return Math.max(g[n - 1], f[n - 1]);
}
}
七. 买卖股票的最佳时机III
买卖股票的最佳时机III
- 状态表示
根据经验 + 题目要求
dp[i] 表示: 选择到第i天结束之后, 此时的最大利润
第i天位置时, 有两种情况, "买入"状态 和 "可交易"状态, 但是还要记录交易次数, 所以我们要使用二维数组 - 状态转移方程
1.第i天可能处于"买入"状态, 此时完成了j次交易, i为最大利润
那么i - 1 位置可能是"买入"状态(啥也不干), 可能是"可交易"状态(买股票)
f[i][j] = max(f[i - 1][j], g[i - 1][j] - p[i])
2.第i天可能处于"可交易"状态, 此时完成了j次交易, i为最大利润
那么i - 1 位置可能是"买入"状态(卖股票, 交易次数 + 1), 可能是"可交易"状态(啥也不干)
g[i][j] = max(f[i - 1][j - 1] + p[i], g[i - 1][j]) - 初始化
f需要初始化第一行, g需要初始化第一行第一列
所以我们可以修改一下状态转移方程, 只初始化第一行即可
g[i][j] = g[i - 1][j];
if(j - 1 >= 0) max(f[i - 1][j - 1] + p[i], g[i][j])
f[0][0] = -p[0] 第0天不可能完成多笔交易, 所以第一行其余填-0x3f3f3f3f (INT_MIN的一半, 不初始化为INT_MIN, 防止溢出
g[0][0] = 0 第一行其余填-0x3f3f3f3f
4. 填表顺序
从左往右 从上往下 两个表一起填
5. 返回值
返回可交易表最后一行的最大值
class Solution {
public int maxProfit(int[] prices) {
int n = prices.length;
int INF = 0x3f3f3f3f;
int[][] f = new int[n][3];
int[][] g = new int[n][3];
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][j], f[i - 1][j - 1] + prices[i]);
}
}
int ret = 0;
for(int j = 0; j < 3; j++){
ret = Math.max(ret, g[n - 1][j]);
}
return ret;
}
}
八. 买卖股票的最佳时机IV
买卖股票的最佳时机IV
- 状态表示
根据经验 + 题目要求
dp[i] 表示: 选择到第i天结束之后, 此时的最大利润
第i天位置时, 有两种情况, "买入"状态 和 "可交易"状态, 但是还要记录交易次数, 所以我们要使用二维数组 - 状态转移方程
1.第i天可能处于"买入"状态, 此时完成了j次交易, i为最大利润
那么i - 1 位置可能是"买入"状态(啥也不干), 可能是"可交易"状态(买股票)
f[i][j] = max(f[i - 1][j], g[i - 1][j] - p[i])
2.第i天可能处于"可交易"状态, 此时完成了j次交易, i为最大利润
那么i - 1 位置可能是"买入"状态(卖股票, 交易次数 + 1), 可能是"可交易"状态(啥也不干)
g[i][j] = max(f[i - 1][j - 1] + p[i], g[i - 1][j]) - 初始化
f需要初始化第一行, g需要初始化第一行第一列
所以我们可以修改一下状态转移方程, 只初始化第一行即可
g[i][j] = g[i - 1][j];
if(j - 1 >= 0) max(f[i - 1][j - 1] + p[i], g[i][j])
f[0][0] = -p[0] 第0天不可能完成多笔交易, 所以第一行其余填-0x3f3f3f3f (INT_MIN的一半, 不初始化为INT_MIN, 防止溢出
g[0][0] = 0 第一行其余填-0x3f3f3f3f
4. 填表顺序
从左往右 从上往下 两个表一起填
5. 返回值
返回可交易表最后一行的最大值
class Solution {
public int maxProfit(int k, int[] prices) {
int n = prices.length;
k = Math.min(k, n / 2);
int INF = 0x3f3f3f3f;
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][j], f[i - 1][j - 1] + prices[i]);
}
}
int ret = 0;
for(int j = 0; j <= k; j++){
ret = Math.max(ret, g[n - 1][j]);
}
return ret;
}
}