打家劫舍等题目—一维动态规划
打家劫舍相关题目
打家劫舍
198. 打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
理解题意,相当于不能连续连着偷两家。那么假设用dp进行状态转移,那么dp[i]就是小偷在0~i这些房屋中能够偷盗的最大金额,那么对于i有两种选择,①偷,那么i-1肯定不能偷,但是0~i-2是随便偷的;②不偷,那么能0~i-1都是随便偷的。因此,dp更新过程:dp[i] = max(dp[i-1], dp[i-2] + nums[i])
。
如果只有一个房子,那就直接偷;如果只有两个房子,偷二者中金钱更多的那个;>=3时,用上面的状态转移方法,进行更新~ java代码如下:
class Solution {
public int rob(int[] nums) {
int n = nums.length;
if(n == 1)
return nums[0];
int[] dp = new int[n];
dp[0] = nums[0];
dp[1] = Math.max(nums[0],nums[1]);
for(int i = 2; i < n; i++){
dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i]);
}
return dp[n-1];
}
}
上面的状态转移,我们看到dp[i]其实只跟dp[i-1]和dp[i-1]相关,因此我们可以对上面的dp数组进行状态转移,即我们只用两个变量分别表示dp[i-1]和dp[i-2]。java代码如下:
class Solution {
public int rob(int[] nums) {
int n = nums.length;
if(n == 1)
return nums[0];
// 用两个变量存储dp[i-2]和dp[i-1]
int money1 = 0, money2 = nums[0];
for(int i = 1; i < n; i++){
int curMoney = Math.max(money2, money1 + nums[i]);
money1 = money2;
money2 = curMoney;
}
return money2;
}
}
变形:740. 删除并获得点数
给你一个整数数组 nums ,你可以对它进行一些操作。
每次操作中,选择任意一个 nums[i] ,删除它并获得 nums[i] 的点数。之后,你必须删除 所有 等于 nums[i] - 1 和 nums[i] + 1 的元素。
开始你拥有 0 个点数。返回你能通过这些操作获得的最大点数。
理解题意后,找出两个关键点:
- 获取nums[i]的点数后,不能获取nums[i]-1和nums[i]+1的点数,即不能获取连续两个整数。其实和打家劫舍题目里面,不能连续偷两个房间是一样的,只是这里提到了前后两个相邻。
- 获取nums[i]的点数后,就需要删除数组中所有等于nums[i]-1和nums[i]+1的点数;删除这些点数以后,如果还有剩余的nums[i],就没有后顾只有的全获取了,因此如果选择nums[i],实际可以获取nums[i]*x个点数,x是nums[i]的数量;
java代码如下:
class Solution {
public int deleteAndEarn(int[] nums) {
int max = Arrays.stream(nums).max().getAsInt();
int[] sum = new int[max+1];
// 统计数组中每个数出现的次数,这里直接统计每个数能够获取的点数
for(int i = 0; i < nums.length ; i++)
sum[nums[i]] += nums[i];
// 同打家劫舍的更新过程一样,优化后只需要两个变量
int max1 =0, max2 = 0;
for(int i = 1; i <= max; i++){
int curMax = Math.max(max1 + sum[i], max2);
max1 = max2;
max2 = curMax;
}
return max2;
}
}
变形:213. 打家劫舍 II

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都== 围成一圈 == ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
这个题跟上面一个只有一点不同:围城一圈,第一个房子和最后一个房子是挨着的,那么:
- 如果偷了第一个,最后一个肯定就不能偷了,2~n-2的房子随意。那么能够偷窃的范围是1~n-1;
- 如果第一个不偷,那么最后一个偷就能偷,同样2~n-2的房子随意。 那么能够偷窃的范围是2~n;
最终选择上面两种情况中能够偷窃到的更大者。 java代码如下:
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]);
// 找到0~n-2和1~n-1两个偷盗区间偷盗金额的更大者
return Math.max(subRob(nums,0,n-2),subRob(nums,1,n-1));
}
// 返回nums[start]~nums[end]之间能够偷盗到的最大值
public int subRob(int[] nums, int start, int end){
int money1 = 0;
int money2 = nums[start];
for(int i = start + 1; i <= end; i++){
int curMax = Math.max(money1 + nums[i], money2);
money1 = money2;
money2 = curMax;
}
return money2;
}
}
变形:337. 打家劫舍 III
337. 打家劫舍 III
涉及到树结构……后续补充……
变形:2560. 打家劫舍 IV
2560. 打家劫舍 IV
读不通题……后续补充……
2140. 解决智力问题
给你一个下标从 0 开始的二维整数数组 questions ,其中 questions[i] = [pointsi, brainpoweri] 。
这个数组表示一场考试里的一系列题目,你需要 按顺序 (也就是从问题 0 开始依次解决),针对每个问题选择 解决 或者 跳过 操作。解决问题 i 将让你 获得 pointsi 的分数,但是你将 无法 解决接下来的 brainpoweri 个问题(即只能跳过接下来的 brainpoweri 个问题)。如果你跳过问题 i ,你可以对下一个问题决定使用哪种操作。
比方说,给你 questions = [[3, 2], [4, 3], [4, 4], [2, 5]] :
如果问题 0 被解决了, 那么你可以获得 3 分,但你不能解决问题 1 和 2 。
如果你跳过问题 0 ,且解决问题 1 ,你将获得 4 分但是不能解决问题 2 和 3 。
请你返回这场考试里你能获得的 最高 分数。
其实这个和打家劫舍也差不多的!打家劫舍是不能同时偷窃挨着的两家,这个是不能同时解决i和i+1~i+brainpoweri的题目。 我们同样用dp数组存储中间结果。这里有两种方案:
- 方案一,从前往后遍历,那么对于题目i有两种选择:①解决:那么得有一个条件,即i不在0~i-1题目中某一个题目的冻结范围内,比如i-3选择了,但是其后面5个题目不能解决,那么题目i只能跳过的。因此需要往前遍历,找到一个题目j,使得i在题目j的冻结范围以外
dp[i]=points[i]+max(0, dp[j]) (j∈[0,i−1], j+brainpower[j]<i)
。 同时解决题目i,其后面brainpoweri不能解决了……②不解决,直接的dp[i] = dp[i-1]。 总之感觉有点复杂…… - 方案二,从后往前遍历,①解决题目i,那么下一个能解决的是i+brainpower[i]+1,
dp[i] = points[i] +dp[i+brainpoweri+1]
;②不解决,dp[i] = dp[i+1];二者取更大者……这个看起来简单多了!
⭐⭐方法一的代码,测试案例不能全部ac……之后再补充吧……【chatgpt修改以后,代码可以了……】
class Solution {
public long mostPoints(int[][] questions) {
int n = questions.length;
long[] dp = new long[n + 1];
for (int i = 0; i < n; i++) {
int points = questions[i][0];
int skip = questions[i][1];
// 解决问题 i
if (i + skip + 1 < n) {
dp[i + skip + 1] = Math.max(dp[i + skip + 1], dp[i] + points);
} else {
dp[n] = Math.max(dp[n], dp[i] + points);
}
// 跳过问题 i
dp[i + 1] = Math.max(dp[i + 1], dp[i]);
}
return dp[n];
}
}
方法二Java代码:
class Solution {
public long mostPoints(int[][] questions) {
int n = questions.length;
long[] curMax = new long[n + 1];
for(int j = n-1; j >=0; j --){
// 需要注意i+brainpoweri+1 > n的情况,此时后面能解决的没有题目,只会获得point[i]的点数,这里可以用
curMax[j] = Math.max(curMax[j+1],curMax[Math.min(j + questions[j][1] + 1 , n)]+questions[j][0]);
}
return curMax[0];
}
}
等差数组
413. 等差数列划分
如果一个数列 至少有三个元素 ,并且任意两个相邻元素之差相同,则称该数列为等差数列。
例如,[1,3,5,7,9]、[7,7,7,7] 和 [3,-1,-5,-9] 都是等差数列。
给你一个整数数组 nums ,返回数组 nums 中所有为等差数组的 子数组个数。
子数组 是数组中的一个连续序列。
假设nums[i-1] - nums[i-2] = d,那么当nums[i] - nums[i-1] = d的话,即可判断nums[i]~nums[i-2]这个子数组是等差数组;对于nums[i-2]之前的数字,我们继续遍历,直到nums[j] - nums[j-1] ≠ d,即找到了区间nums[j]~nums[i]整个以nums[i]结尾的最长等差子数组。比如[4,2,3,4,5,6],对于数字6,要一直往前遍历到2,时间复杂度变成了n^2。
需要注意的是,如果用一维数组dp存储中间结果,那么dp[i]可以有两层意义:
- dp[i]直接统计nums[0]~nums[i]中的等差子数组数量,那么每增加一个nums[i],按照上面的方法找到以nums[i]结尾的等差子数组数量x,
dp[i] = dp[i -1] + x
。重点是统计这个x,需要再用一轮遍历…… - dp[i]只表示以nums[i]结尾的等差子数组的数量,那么
dp[i] = dp[i-1] + 1
!!我们假设数组[1,2,3,4,5,……],dp[2] = 1(即子数组[1,2,3]),dp[3] = 2(即子数组[1,2,3,4],[2,3,4]),dp[4] = 3(即子数组[1,2,3,4,5], [2,3,4,5], [3,4,5])……经过观察,每增加一个nums[i],如果nums[i] - nums[i-1] == nums[i-1] - nums[i-2],那么以nums[i-1]结尾的等差子数组后面再附加一个nums[i],又构成了新的等差子数组;同时,会增加一个nums[i-2]~nums[i]这个等差子数组,因此dp[i] = dp[i -1] + 1,用这个更新过程同时减少了一轮遍历!!
总之,重点是统计以nums[i]结尾的等差子数组的数量。
方法一,两层遍历的Java代码:
class Solution {
public int numberOfArithmeticSlices(int[] nums) {
int len = nums.length;
if(len <= 2)
return 0;
int[] count = new int[len];
for(int i = 2; i < len ; i++){
int curNum = 0;
// 新增一层循环
for(int j = i-1; j >= 1 && nums[i] - nums[i-1] == nums[j] - nums[j-1]; j--){
curNum++;
}
count[i] = count[i-1] + curNum;
}
return count[len-1];
}
}
方法二,优化后的Java代码:
class Solution {
public int numberOfArithmeticSlices(int[] nums) {
int len = nums.length;
if(len <= 2)
return 0;
// 统计总的子数组的数量
int sum = 0;
// count[i]统计以nums[i]结尾的等差子数组数量
int[] count = new int[len];
for(int i = 2; i < len ; i++){
// 判断是否满足等差数组条件
if(nums[i] - nums[i-1] == nums[i-1] - nums[i-2])
count[i] = count[i-1] + 1;
else
count[i] = 0;
// 总数要加上count[i]
sum += count[i];
}
return sum;
}
}