找往期文章包括但不限于本期文章中不懂的知识点:
个人主页:我要学编程程(ಥ_ಥ)-CSDN博客
所属专栏:动态规划
目录
面试题17.16.按摩师
题目:

一个有名的按摩师会收到源源不断的预约请求,每个预约都可以选择接或不接。在每次预约服务之间要有休息时间,因此她不能接受相邻的预约。给定一个预约请求序列,替按摩师找到最优的预约集合(总预约时间最长),返回总的分钟数。
注意:本题相对原题稍作改动
示例 1:
输入: [1,2,3,1] 输出: 4 解释: 选择 1 号预约和 3 号预约,总时长 = 1 + 3 = 4。示例 2:
输入: [2,7,9,3,1] 输出: 12 解释: 选择 1 号预约、 3 号预约和 5 号预约,总时长 = 2 + 9 + 1 = 12。示例 3:
输入: [2,1,4,5,3,1,1,3] 输出: 12 解释: 选择 1 号预约、 3 号预约、 5 号预约和 8 号预约,总时长 = 2 + 4 + 3 + 3 = 12。
思路:
代码实现:
class Solution {
public int massage(int[] nums) {
// 排除特殊情况
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]);
}
}
可以看到本题与我们前面做的题目都不一样,本题的推导公式并不是只有一种,即状态并不是只有一种,而是多种状态,因此称为多状态dp问题。
198.打家劫舍
题目:
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1] 输出:4 解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。 偷窃到的最高金额 = 1 + 3 = 4 。示例 2:
输入:[2,7,9,3,1] 输出:12 解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。 偷窃到的最高金额 = 2 + 9 + 1 = 12 。提示:
1 <= nums.length <= 100
0 <= nums[i] <= 400
思路:
1、dp[i]的含义:
dp[i]表示到达i位置时,偷窃的最大金额
2、推导公式:
针对i位置,小偷有两种选择方式:偷或者不偷,因此本题也是一个多状态的dp问题,最终的推导公式也是有两种的。
f[i]:表示偷到i位置时,偷取i位置的钱,总共偷取的最大金额。
g[i]:表示偷到i位置时,不偷i位置的钱,总共偷取的最大金额。
f[i] = g[i-1] + nums[i](偷到i位置时,偷取i位置的钱,总共偷取的最大金额 = [0, i]区间内,不偷取i-1位置的值的最大金额 + nums[i])
g[i] = Math.max(f[i-1], g[i-1])(偷到i位置时,不偷i位置的钱,总共偷取的最大金额 = [0, i]区间内,不偷取i-1位置的值的最大金额 与 [0, i]区间内,偷取i-1位置的值的最大金额 的较大值)
3、初始化:
将0位置初始化即可,f[0] = nums[0],g[0] = 0
4、遍历顺序:
从左到右
5、打印dp数组
代码实现:
class Solution {
public int rob(int[] nums) {
int n = nums.length;
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]);
}
}
213.打家劫舍Ⅱ
题目:
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
示例 1:
输入:nums = [2,3,2] 输出:3 解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。示例 2:
输入:nums = [1,2,3,1] 输出:4 解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。 偷窃到的最高金额 = 1 + 3 = 4 。示例 3:
输入:nums = [1,2,3] 输出:3提示:
1 <= nums.length <= 100
0 <= nums[i] <= 1000
思路:本题和上一题有一点不一样,这里的房子是围成一个圈的,也就是环形数组的样子。我们可以想办法将环形数组转换为线性数组,这样就可以使用上一题的方法来解决了。对第一个位置进行分类讨论:当选择0下标时,1下标和n-1下标是不能选择的了,即对 [2, n-2] 进行一次打家劫舍的操作即可;当不选择0下标时,[1, n-1]可以进行一次打家劫舍的操作。最后只需要对两者的结果求最大值即可。
1、dp[i]的含义:
dp[i]表示到达i位置时,偷窃的最大金额
2、推导公式:
针对i位置,小偷有两种选择方式:偷或者不偷,因此本题也是一个多状态的dp问题,最终的推导公式也是有两种的。
f[i]:表示偷到i位置时,偷取i位置的钱,总共偷取的最大金额。
g[i]:表示偷到i位置时,不偷i位置的钱,总共偷取的最大金额。
f[i] = g[i-1] + nums[i](偷到i位置时,偷取i位置的钱,总共偷取的最大金额 = [0, i]区间内,不偷取i-1位置的值的最大金额 + nums[i])
g[i] = Math.max(f[i-1], g[i-1])(偷到i位置时,不偷i位置的钱,总共偷取的最大金额 = [0, i]区间内,不偷取i-1位置的值的最大金额 与 [0, i]区间内,偷取i-1位置的值的最大金额 的较大值)
3、初始化:
将0位置初始化即可,f[0] = nums[0],g[0] = 0
4、遍历顺序:
从左到右
5、打印dp数组
代码实现:
class Solution {
public int rob(int[] nums) {
// 分类讨论:是否选择偷窃0下标
// 偷窃0下标
int n = nums.length;
int ret1 = robIndex(nums, 2, n-2) + nums[0];
// 不偷窃0下标
int ret2 = robIndex(nums, 1, n-1);
return Math.max(ret1, ret2);
}
// 偷窃 [a,b] 内的钱
private int robIndex(int[] nums, int a, int b) {
// 排除特殊情况
if (a > b) {
return 0;
}
int n = b-a+1;
int[] f = new int[n];
int[] g = new int[n];
f[0] = nums[a];
for (int i = 1; i < n; i++) {
f[i] = g[i-1] + nums[a+i]; // 注意这里的下标映射关系(nums:a -> f:0)
g[i] = Math.max(f[i-1], g[i-1]);
}
return Math.max(f[n-1], g[n-1]);
}
}
740.删除并获得点数
题目:
给你一个整数数组
nums
,你可以对它进行一些操作。每次操作中,选择任意一个
nums[i]
,删除它并获得nums[i]
的点数。之后,你必须删除 所有 等于nums[i] - 1
和nums[i] + 1
的元素。开始你拥有
0
个点数。返回你能通过这些操作获得的最大点数。示例 1:
输入:nums = [3,4,2] 输出:6 解释: 删除 4 获得 4 个点数,因此 3 也被删除。 之后,删除 2 获得 2 个点数。总共获得 6 个点数。示例 2:
输入:nums = [2,2,3,3,3,4] 输出:9 解释: 删除 3 获得 3 个点数,接着要删除两个 2 和 4 。 之后,再次删除 3 获得 3 个点数,再次删除 3 获得 3 个点数。 总共获得 9 个点数。提示:
1 <= nums.length <= 2 * 104
1 <= nums[i] <= 104
思路:
题目说:选择任意一个 nums[i] ,删除它并获得 nums[i] 的点数。之后,你必须删除 所有 等于 nums[i] - 1 和 nums[i] + 1 的元素。这也就意味着:选择nums[i]之后,对应的nums[i]-1和nums[i]+1是不能选择的。这和我们前面刷的打家劫舍系列的题目是不是非常类似呢?也是在选择nums[i]之后,不能再选择相邻的元素了。因此这里我们可以将题目转换为打家劫舍的思路来解决。但要先预处理数组,打家劫舍的数组是i位置对应的钱,而这里的可能出现多个元素相同且无序的情况。我们可以使用一个数组来存储原始的数组,达到打家劫舍数组的情况。
剩余的部分可以参考上面的dp五部曲。
代码实现:
class Solution {
public int deleteAndEarn(int[] nums) {
// arr[i]表示i这个数出现的总和值
// 相当于是i位置有arr[i]的钱
int n = 10001; // 原数组值最大的值
int[] arr = new int[n];
// 将原数组转换为打家劫舍的数组
for (int x : nums) {
arr[x] += x;
}
// 对arr数组进行一次打家劫舍的操作
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]);
}
}
LCR091.粉刷房子
题目:
假如有一排房子,共
n
个,每个房子可以被粉刷成红色、蓝色或者绿色这三种颜色中的一种,你需要粉刷所有的房子并且使其相邻的两个房子颜色不能相同。当然,因为市场上不同颜色油漆的价格不同,所以房子粉刷成不同颜色的花费成本也是不同的。每个房子粉刷成不同颜色的花费是以一个
n x 3
的正整数矩阵costs
来表示的。例如,
costs[0][0]
表示第 0 号房子粉刷成红色的成本花费;costs[1][2]
表示第 1 号房子粉刷成绿色的花费,以此类推。请计算出粉刷完所有房子最少的花费成本。
示例 1:
输入: costs = [[17,2,17],[16,16,5],[14,3,19]] 输出: 10 解释: 将 0 号房子粉刷成蓝色,1 号房子粉刷成绿色,2 号房子粉刷成蓝色。 最少花费: 2 + 5 + 3 = 10。示例 2:
输入: costs = [[7,6,2]] 输出: 2提示:
costs.length == n
costs[i].length == 3
1 <= n <= 100
1 <= costs[i][j] <= 20
思路:
题目是让我们求出使用三种粉刷方式最终总的花费较少的
注意一下,costs数组的含义:横坐标表示几号房子,纵坐标表示该房子粉刷成啥样的颜色。
1、dp[i]的含义:粉刷到i位置时,总的花费最少。但粉刷到i位置时,i位置的颜色有好几种分类,因此我们也要对dp数组进行分类。这里可以参考题目中的costs数组,开辟一个二维数组dp[i][0]、dp[i][1]、dp[i][2]三者表示的含义分别是粉刷到i位置时,最后一个位置粉刷的颜色为红色、蓝色、绿色,总的花费最少。
2、推导公式:dp[i][0]表示粉刷到i位置时,最后一个位置粉刷为红色,总的花费最少。因此最后一个位置的颜色固定了,就是属于红色,最终就变为了在[0, i-1]区间内,求出最少花费,这里就需要我们往dp[i]的含义上联系了。在[0, i-1]区间内,求出最少花费,最后一个位置可以粉刷为蓝色或者绿色,因此我们应该要从两者中找出最小值,这不就符合dp[i][1]、dp[i][2]的定义了嘛,粉刷到i-1位置时,最后一个位置粉刷的颜色为蓝色、绿色,总的花费最少。
dp[i][0] = Math.min(dp[i][1], dp[i][2]) + costs[i][0],同理后面两种也是类似的。
3、初始化:为了确保下标不越界,应该将i==0的位置初始化为costs[0][0->2]。
4、遍历顺序:从左到右
5、打印dp数组
代码实现:
class Solution {
public int minCost(int[][] costs) {
int n = costs.length;
int[][] dp = new int[n][3];
dp[0][0] = costs[0][0];
dp[0][1] = costs[0][1];
dp[0][2] = costs[0][2];
for (int i = 1; i < n; i++) {
dp[i][0] = Math.min(dp[i-1][1], dp[i-1][2]) + costs[i][0];
dp[i][1] = Math.min(dp[i-1][0], dp[i-1][2]) + costs[i][1];
dp[i][2] = Math.min(dp[i-1][1], dp[i-1][0]) + costs[i][2];
}
return Math.min(Math.min(dp[n-1][0], dp[n-1][1]), dp[n-1][2]);
}
}
上述初始化也可以通过多申请一列来弥补。下标映射的话,只需要在新dp数组对应的横坐标-1即可。为了保证后续填表的正确性, 新增的一列必须满足 min(蓝色,绿色) + costs[红色] = cost[红色],即初始化为0即可。
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++) {
// 注意下标的映射关系
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][1], dp[i-1][0]) + costs[i-1][2];
}
return Math.min(Math.min(dp[n][0], dp[n][1]), dp[n][2]);
}
}
好啦!本期 动态规划 —— 简单多状态 dp 问题 的刷题之旅 就到此结束啦!我们下一期再一起学习吧!