文章目录
- Day 52
- 01. 打家劫舍(No. 198)
- <1> 题目
- <2> 笔记
- <3> 代码
- 02. 打家劫舍 II(No. 213)
- <1> 题目
- <2> 笔记
- <3> 代码
- 03.打家劫舍III(No. 337)
- <1> 题目
- <2> 笔记
- <3> 代码
Day 52
01. 打家劫舍(No. 198)
题目链接
代码随想录题解
<1> 题目
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 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
<2> 笔记
打家劫舍问题算是力扣上比较知名的一类动态规划的题目,但其实有了前面背包问题的铺垫,做起来也没有那么难。
题目中讲的是一个小偷去偷沿街的房屋,但是相邻两个房屋不能同时被偷,否则就会触发警报,如何选择打劫的房屋能够使得小偷得到的钱最多。
先来看一下本题的状态,既然题目中问的是偷哪些房屋能获得的钱最多,那状态暂时确定成偷这些房屋能获得的最多的钱是多少。
再来思考一下这个状态能否转移,本题唯一的限制就是相邻的两个房屋不能同时偷,如果要确定现在偷的这个房屋的状态,必须要知道前面的房屋有没有被偷,如果前面的房屋被偷了,那这个状态一定就是上一次的状态(因为不能连着偷嘛)。
所以对于一个房屋应该规定两个状态:
- 这个房屋被偷的话,最多能获得多少钱。
- 这个房屋没有被偷的话,最多能获得多少钱。
有了这两个部分就可以推出后面的状态了,如果这个房屋不被偷的话,那前面房屋是否被偷就不会产生影响的,这时候最大值就是前面房屋被偷状态和不被偷状态取一个最大值。
如果这个房屋被偷的话,那一定是建立在前面的房屋没有被偷的状态的基础上,即前面房屋没有被偷的状态加上偷这个房屋的价格。
这两个状态 同时 都要被保存,因为后面的状态需要依靠这两个状态去推出。
确定了一个可以转移的状态,来明确一下 dp
数组的含义,因为要同时存储两个状态,所以 dp
应该被声明为一个二维数组,dp[i][0]
表示这个房屋没有被偷的话,最大值是多少,而
dp[i][1]
表示这个房屋如果被偷的话,最大值是多少。
再来看递推公式,有两个状态也就对应着两个递推公式:
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1]);
dp[i][1] = dp[i - 1][0] + nums[i];
如果不好理解的话,可以去回顾一下 dp
数组的含义
dp
数组的初始化:题目中说了 nums.length >= 1
所以可以放心大胆的使用 nums[0]
来作为初始化的条件,当然将数组扩充一下,初始化打劫 0
个房屋的情况也是可以的。
dp[0][0] = 0;
dp[0][1] = nums[0];
现在就可以写出代码了。
<3> 代码
class Solution {
public int rob(int[] nums) {
int n = nums.length;
int[][] dp = new int[n][2];
dp[0][0] = 0;
dp[0][1] = nums[0];
for (int i = 1; i < nums.length; i++) {
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1]);
dp[i][1] = dp[i - 1][0] + nums[i];
}
return Math.max(dp[n - 1][0], dp[n - 1][1]);
}
}
02. 打家劫舍 II(No. 213)
题目链接
代码随想录题解
<1> 题目
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
示例 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
<2> 笔记
本题与上一题的区别就是 最后一个和第一个不能同时取得,除此之外就没有任何差异了。
所以大致可以把本题分为三种情况:
-
第一个和最后一个都不要
-
要第一个但是不要最后一个
-
要最后一个不要第一个
注意这里说的要或者不要不是说一定会去偷第一家或者最后一家,而是要不要考虑偷这两家的情况,所以第一种情况其实是包含在第二种和第三种情况之中的,所以只需要求最后两种情况,然后返回其中的最大值即可。
public int rob(int[] nums) {
if (nums.length == 1) return nums[0];
int res1 = getRes(0, nums.length - 2, nums);
int res2 = getRes(1, nums.length - 1, nums);
return Math.max(res1, res2);
}
这段代码就是先求得后两种情况的值,然后去取最大值,下面来看一下 getRes
方法的具体实现,为了限制遍历的范围,传入了一个 startIndex
和一个 endIndex
,这个下标指的是元素在 nums
中的下标,所以在确认遍历遍历返回的时候,要保证遍历下标 一定要包含 endIndex,如果我们将 dp
数组初始化为这样:dp[endIndex - startInde][2]
,然后将遍历的范围设置为 [startIndex, dp.length]
会导致计算结果错误的原因,因为这样并不会遍历到 endIndex
这个下标。
所以要将 dp
数组的行长度初始化为 dp[endIndex + 1]
然后对 dp[startIndex]
的部分进行初始化,这样就可以保证能够准确规范遍历范围。
int[][] dp = new int[endIndex + 1][2];
dp[startIndex][0] = 0;
dp[startIndex][1] = nums[startIndex];
<3> 代码
class Solution {
public int rob(int[] nums) {
if (nums.length == 1) return nums[0];
int res1 = getRes(0, nums.length - 2, nums);
int res2 = getRes(1, nums.length - 1, nums);
return Math.max(res1, res2);
}
public int getRes(int startIndex, int endIndex, int[] nums) {
int[][] dp = new int[endIndex + 1][2];
dp[startIndex][0] = 0;
dp[startIndex][1] = nums[startIndex];
for (int i = startIndex + 1; i <= endIndex; i++) {
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1]);
dp[i][1] = dp[i - 1][0] + nums[i];
}
return Math.max(dp[dp.length - 1][0], dp[dp.length - 1][1]);
}
}
03.打家劫舍III(No. 337)
题目链接
代码随想录题解
<1> 题目
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root
。
除了 root
之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。
给定二叉树的 root
。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额
示例 1:
输入: root = [3,2,3,null,3,null,1]
输出: 7
解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7
示例 2:
输入: root = [3,4,5,1,3,null,1]
输出: 9
解释: 小偷一晚能够盗取的最高金额 4 + 5 = 9
提示:
- 树的节点数在
[1, 104]
范围内 0 <= Node.val <= 104
<2> 笔记
本题是二叉树和动态规划融合的题目;一个节点有两种选择:取或者不取,且只有 父节点 被取的情况下才会影响子节点的状态。
和上面的题目相同,声明一个数组来保存本结点取和不取的情况,因为本题采用的是递归的方式,每一层递归都会存储一个 dp
数组,所以直接将数组声明为一维数组,来存储本层的情况即可。
递推公式和上面相同,但本题牵扯到了二叉树,所以这里要考虑遍历顺序,中序遍历一般二叉搜索树才会用到,暂时不考虑,来考虑用前序遍历还是后序遍历。
-
前序遍历,就是在进入节点的时候就对节点的值进行处理,所以需要得知上一个节点是否被取得,但是前序遍历存在一个问题:因为本题中如果父节点没有被取的话,那左节点和右节点是可以 同时 被取得的,但是前序遍历在进入左子树和进入右子树之前 没有规范自己到底取没取(因为传入的是
dp
包含了两种情况),所以对于以上的情况解决起来比较困难,比如说这样:int[] dpLeft = reversal(node.left, lastDP); int[] dpRight = reversal(node.right, lastDP);
当递归回到这个节点,也就是第一个递归得到返回值的时候,在本层 无法确定 这个返回值是在取得本层节点还是在没有取得本层节点的情况下退出来的,就无法按照逻辑再对后面的内容进行规范。
-
后序遍历,而后序遍历就恰好适用于这种需要得到子节点状态的情况,对于一个节点来说,如果要取得本节点,那就是子节点都不取的情况,将
left[0] + right[0]
即可得到结果,如果不取本节点,拿得到的结果就是Math.max(left[0], left[1]) + Math.max(right[0], right[1])
。
通过后序遍历就可以保证数组是符合逻辑的;通过本题再次回顾一下后序遍历的使用时机,也就是父节点需要得到子节点状态的时候,所以后序遍历的代码一般是有返回值的。
<3> 代码
class Solution {
List<Integer> path = new ArrayList<>();
public int rob(TreeNode root) {
int[] res = reversal(root);
return Math.max(res[0], res[1]);
}
public int[] reversal(TreeNode node, int[] lastDP) {
if (node == null) {
return new int[]{0, 0};
}
int[] dp = new int[2];
int[] dpLeft = reversal(node.left, lastDP);
int[] dpRight = reversal(node.right, lastDP);
return dp;
}
}