打家劫舍问题
文章目录
- 【动态规划】简单多状态dp问题(1)打家劫舍问题
- 1. 按摩师(打家劫舍Ⅰ)
- 1.1 题目解析
- 1.2 算法原理
- 1.2.1 状态表示
- 1.2.2 状态转移方程
- 1.2.3 初始化
- 1.2.4 填表顺序
- 1.2.5 返回值
- 1.3 编写代码
- 2. 打家劫舍Ⅱ
- 2.1 题目解析
- 2.2 算法原理
- 2.2.1 状态表示
- 2.2.2 状态转移方程
- 2.2.3 初始化
- 2.2.4 填表顺序
- 2.2.5 返回值
- 2.3 编写代码
- 3. 删除并获得点数
- 3.1 题目解析
- 3.2 算法原理
- 3.2.1 状态表示
- 3.2.2 状态转移方程
- 3.2.3 初始化
- 3.2.4 填表顺序
- 3.2.5 返回值
- 3.3 编写代码
- 4. 粉刷房子
- 4.1 题目解析
- 4.2 算法原理
- 4.2.1 状态表示
- 4.2.2 状态转移方程
- 4.2.3 初始化
- 4.2.4 填表顺序
- 4.2.5 返回值
- 4.3 编写代码
传送门:面试题 17.16. 按摩师
题目:
越难的dp问题,看示例只能起到了解题目的效果,一般推不出啥普遍的规律,所以接下来就是我们的算法原理,通过动归的思想去理解,才会豁然开朗!
我们需要通过经验 + 题目要求去决定状态表示:
再根据经验,一般dp表的其中一值就应该是答案!
综合得到状态表示:dp[i]
表示就是起点到坐标为 i 的位置这些天以来的最长预约时长
而这道题,与之前做过的题不一样的是,一个坐标的状态有两种情况,需要我们继续细化
在之前的题目里,我们到达一个坐标并无其他选择,而现在有~
所以,最终的状态表示为:
f[i]表示的是,从起点到 i 坐标的这些天内,i这一天接单的情况下的最长预约时间
g[i]表示的是,从起点到 i 坐标的这些天内,i这一天不接单的情况下的最长预约时间
同样的套路,我们需要根据已确定的dp表的值来推导 f[i] 和 g[i] 的值,并且牢记他们的状态表示!
“最近一步”可以理解为“必然事件”
而1代表着f表怎么填,2代表着g表怎么填
所以得出状态转移方程:
f[i] = nums[i] + g[i - 1];
g[i] = max{f[i - 1], g[i - 1]};
这道题可以用假数据的方法,但是没有必要,因为我们只需要初始化第一个值就行了~
从左往右两个表一起填
返回的就是起点到终点,即所有天以来的最长预约时长,即f[n - 1]或者g[n - 1]
class Solution {
public int massage(int[] nums) {
//1. 创建dp表
//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] = nums[i] + g[i - 1];
g[i] = Math.max(f[i - 1], g[i - 1]);
}
return Math.max(f[n - 1], g[n - 1]);
}
}
时空复杂度都为:O(N)
传送门:力扣213. 打家劫舍 II
题目:
越难的dp问题,看示例只能起到了解题目的效果,一般推不出啥普遍的规律,所以接下来就是我们的算法原理,通过动归的思想去理解,才会豁然开朗!
算法原理基本跟上一道题一致,只不过这一次成环了~
而并不想在状态转移方程中去判断这种情况,因为这是不现实的,因为f 表和g 表到达n - 2下标的时候,我们并不能确定n - 2坐标对应的情况,起点是否“偷”与“不偷”,并且我们也不能人为规定(影响最终结果)
所以我们需要一个**预处理**的阶段:(往已知的解法靠拢,化未知为已知)
我们细化第一天的状态:(重点理解)
而我们则需要去封装,“一次打家劫舍”的方法!
我们需要通过经验 + 题目要求去决定状态表示:
再根据经验,一般dp表的其中一值就应该是答案!
综合得到状态表示:dp[i]
表示就是起点到坐标为 i 的位置偷到的最大钱数
而这道题,与之前做过的题不一样的是,一个坐标的状态有两种情况,需要我们继续细化
所以,最终的状态表示为:
f[i]表示的是,从起点到 i 坐标的这些房间,i这个房间偷的情况下的最大钱数
g[i]表示的是,从起点到 i 坐标的这些房间,i这个房间不偷的情况下的最大钱数
同样的套路,我们需要根据已确定的dp表的值来推导 f[i] 和 g[i] 的值,并且牢记他们的状态表示!
“最近一步”可以理解为“必然事件”
而1代表着f表怎么填,2代表着g表怎么填
所以得出状态转移方程:
f[i] = nums[i] + g[i - 1];
g[i] = max{f[i - 1], g[i - 1]};
从左往右两个表一起填
一次打家劫舍操作的方法封装:
public int oneRob(int[] nums, int left, int right) {
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] = nums[i] + g[i - 1];
g[i] = Math.max(f[i - 1], g[i - 1]);
}
return Math.max(f[right], g[right]);
}
核心方法:
class Solution {
public int rob(int[] nums) {
int n = nums.length;
if(n == 1) {
return nums[0];
}
if(n == 2) {
return Math.max(nums[0], nums[1]);
}
int yes = nums[0] + oneRob(nums, 2, n - 2);
int no = oneRob(nums, 1, n - 1);
return Math.max(yes, no);
}
}
传送门:力扣740. 删除并获得点数
题目:
越难的dp问题,看示例只能起到了解题目的效果,一般推不出啥普遍的规律,所以接下来就是我们的算法原理,通过动归的思想去理解,才会豁然开朗!
首先,我们需要将这些数据进行一个排序,这是很容易想到的,这样可以很好的观察到数据的发布情况(值为x的数的个数也明显)
而这样也不够,我们怎么往动态规划的方向去靠拢呢
所以很快就能想到以下操作:
猛地一看!
问题:这样岂不是会选到不存在的数字?
即,不限制其选择不存在的数字,是完全考虑选择在不存在数字的情况下的强大的最优解
我们需要通过经验 + 题目要求去决定状态表示:
再根据经验,一般dp表的其中一值就应该是答案!
综合得到状态表示:dp[i]
表示就是起点到坐标为 i 的位置能获得的最大点数
而这道题,与之前做过的题不一样的是,一个坐标的状态有两种情况,需要我们继续细化
所以,最终的状态表示为:
f[i]表示的是,从起点到 i 坐标的这些数内,i这一个数不选情况下能获得的最大点数
g[i]表示的是,从起点到 i 坐标的这些数内,i这一个数不选的情况下能获得的最大点数
同样的套路,我们需要根据已确定的dp表的值来推导 f[i] 和 g[i] 的值,并且牢记他们的状态表示!
“最近一步”可以理解为“必然事件”
而1代表着f表怎么填,2代表着g表怎么填
所以得出状态转移方程:
f[i] = arr[i] + g[i - 1];
g[i] = max{f[i - 1], g[i - 1]};
0这个数是不存在的树,所以选和不选f[0]和g[0]都为0
从左往右两个表一起填
f[1e4] 和 g[1e4]的较大值
预处理
创建dp表
填表
返回值
class Solution {
public int deleteAndEarn(int[] nums) {
//1. 预处理
int max = (int)1e4;
int[] arr = new int[max + 1];
for(int i = 0; i < nums.length; i++) {
int index = nums[i];
arr[index] += index;
}
//2. 创建dp表
//3. 填表
//4. 返回值
int[] f = new int[max + 1];
int[] g = new int[max + 1];
for(int i = 1; i < max + 1; i++) {
f[i] = arr[i] + g[i - 1];
g[i] = Math.max(f[i - 1], g[i - 1]);
}
return Math.max(f[max], g[max]);
}
}
时空复杂度都为:O(N)
传送门:力扣剑指 Offer II 091. 粉刷房子
题目:
越难的dp问题,看示例只能起到了解题目的效果,一般推不出啥普遍的规律,所以接下来就是我们的算法原理,通过动归的思想去理解,才会豁然开朗!
我们需要通过经验 + 题目要求去决定状态表示:
再根据经验,一般dp表的其中一值就应该是答案!
综合得到状态表示:dp[i]
表示就是起点到坐标为 i 的位置时的这些房子粉刷完的最小花费
而这道题,与之前做过的题不一样的是,一个坐标的状态有三种情况,需要我们继续细化
所以,最终的状态表示为:
dp[i] [0]表示的是,从起点到 i 坐标的这些房子内,i这一个房子为红色情况下的最小花费
dp[i] [1]表示的是,从起点到 i 坐标的这些房子内,i这一个房子为蓝色情况下的最小花费
dp[i] [2]表示的是,从起点到 i 坐标的这些房子内,i这一个房子为绿色情况下的最小花费
同样的套路,我们需要根据已确定的dp表的值来推导 f[i] 和 g[i] 的值,并且牢记他们的状态表示!
“最近一步”可以理解为“必然事件”
而1代表着dp[i] [0]表怎么填,2代表着dp[i] [1]表怎么填,3代表着dp[i] [2]表怎么填
所以得出状态转移方程:
dp[i][0] = min{dp[i][1], dp[i][2]} + costs[i][0];
dp[i][1] = min{dp[i][0], dp[i][2]} + costs[i][1];
dp[i][2] = min{dp[i][0], dp[i][1]} + costs[i][2];
dp[0] [j] = costs[0] [j];
最后一个房间刷红刷蓝刷绿三种情况中的较小值~
class Solution {
public int minCost(int[][] costs) {
//1. 创建dp表
//2. 初始化
//3. 填表
//4. 返回值
int n = costs.length;
int[][] dp = new int[n][3];
for(int i = 0; i < 3; i++) {
dp[0][i] = costs[0][i];
}
for(int i = 1; i < n; i++) {
dp[i][0] = costs[i][0] + Math.min(dp[i - 1][1], dp[i - 1][2]);
dp[i][1] = costs[i][1] + Math.min(dp[i - 1][0], dp[i - 1][2]);
dp[i][2] = costs[i][2] + Math.min(dp[i - 1][0], dp[i - 1][1]);
}
return Math.min(dp[n - 1][0], Math.min(dp[n - 1][1], dp[n - 1][2]));
}
}
时空复杂度都为:O(N)
由于此类问题题目较多,分两篇文章讲述,下一篇文章的题目链接(买卖股票问题):
传送门:力扣309. 最佳买卖股票时机含冷冻期
传送门:力扣714. 买卖股票的最佳时机含手续费
传送门:力扣123.买卖股票的最佳时机 III
传送门:力扣188. 买卖股票的最佳时机 IV
文章到此结束!谢谢观看
可以叫我 小马,我可能写的不好或者有错误,但是一起加油鸭!本文代码链接:动态规划03/src/Main.java · 游离态/马拉圈2023年6月 - 码云 - 开源中国 (gitee.com)