一、打家劫舍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 。
思路:
明确dp数组的含义,本题dp数组的含义为包含前 i 个元素(包含 i)所得的最大值为dp[i],分别有两种情况
第一种
要第 i 个元素:此时第 i-1 个元素不能要,只能从第 i-2个元素拿,表达为dp[i-2]+nums[i]
第二种
不要第 i 个元素:此时应该从第 i-1 个元素选择拿与不拿,表达为dp[i-1]
因此可得递推式为
dp[i] +=max(dp[i-1],dp[i-2]+nums[i])
代码:
public int rob(int[] nums) {
// 如果输入数组为空或者长度为0,则没有房子可打劫,返回0
if (nums == null || nums.length == 0)
return 0;
// 如果只有一个房子,只能打劫这个房子,直接返回其金额
if (nums.length == 1)
return nums[0];
// dp 数组用来存储到第 i 个房子时能偷到的最大金额
int[] dp = new int[nums.length + 1];
// 初始化 dp 数组
dp[0] = nums[0]; // 偷第一个房子时的最大金额
dp[1] = Math.max(nums[0], nums[1]); // 偷第一个或第二个房子中金额较大的一个
// 从第三个房子开始计算
for (int i = 2; i < nums.length; i++) {
// dp[i] 表示偷到第 i 个房子时的最大金额
// 选择偷第 i 个房子的情况,应该加上之前能偷到的最大金额(即 dp[i-2]);
// 或者选择不偷第 i 个房子,则最大金额等于 dp[i-1]
dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
}
// 最终结果是到最后一个房子的最大金额
return dp[nums.length - 1];
}
-
边界条件处理:
if (nums == null || nums.length == 0) return 0;
- 处理输入为空或没有房子的情况,直接返回
0
。
- 处理输入为空或没有房子的情况,直接返回
if (nums.length == 1) return nums[0];
- 处理只有一个房子的情况,直接返回那个房子的金额。
-
动态规划数组初始化:
int[] dp = new int[nums.length + 1];
- 创建一个大小为
nums.length + 1
的数组dp
。dp[i]
表示偷到第i
个房子时的最大金额。
- 创建一个大小为
dp[0] = nums[0];
- 如果只有第一个房子,那么偷这个房子的钱就是
dp[0]
。
- 如果只有第一个房子,那么偷这个房子的钱就是
dp[1] = Math.max(nums[0], nums[1]);
- 比较前两个房子的金额,取较大的一个,因为只能选择偷第一个房子或第二个房子中的一个。
-
状态转移:
for (int i = 2; i < nums.length; i++)
- 从第三个房子开始计算。
dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
- 选择偷第
i
个房子的情况:此时的金额为dp[i - 2] + nums[i]
。 - 不偷第
i
个房子的情况:此时的金额为dp[i - 1]
。 - 取两者中的最大值作为
dp[i]
的值。
- 选择偷第
-
返回结果:
return dp[nums.length - 1];
- 最终返回
dp
数组最后一个元素,即偷到最后一个房子时能获得的最大金额。
- 最终返回
二、打家劫舍2
题目:
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
示例 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数组连接成一个环形结构,因此存在三种情况
第一种:选择首元素和中间所有元素,尾元素则不能选取
第二种:选择尾元素和中间所有元素,首元素不能选择
第三种:首位元素均不选择,只选择中间的元素
第三种情况可以包含在第一种情况中,在第一种情况中如果我们不拿首元素即为第三种情况
因此需要讨论第一种和第二种情况中哪个可以拿到更多,取两种情况最大值
代码:
public int rob(int[] nums) {
// 如果输入数组为空或长度为0,则没有房子可打劫,返回0
if (nums == null || nums.length == 0)
return 0;
int len = nums.length;
// 如果只有一个房子,只能打劫这个房子,直接返回其金额
if (len == 1)
return nums[0];
// 计算两种情况的最大值
// 1. 从第一个房子到倒数第二个房子(不打劫最后一个房子)
// 2. 从第二个房子到最后一个房子(不打劫第一个房子)
return Math.max(robAction(nums, 0, len - 1), robAction(nums, 1, len));
}
int robAction(int[] nums, int start, int end) {
int x = 0; // 代表上一个房子之前的最大金额
int y = 0; // 代表上一个房子的最大金额
int z = 0; // 当前房子的最大金额
// 遍历从 start 到 end(不包括 end)范围内的房子
for (int i = start; i < end; i++) {
y = z; // 当前的最大金额之前的最大金额
z = Math.max(y, x + nums[i]); // 当前房子的最大金额是之前最大金额和加上当前房子金额后的最大值
x = y; // 更新 x 为之前的最大金额
}
return z; // 返回当前房子的最大金额
}
三、打家劫舍3
题目:
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 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
思路:
本题为树形结构,采用后序遍历的方法,相邻节点之间不能同时拿取,在定义dp数组时分别有两种情况,分别是dp[0],代表在不拿当前节点的情况下所偷取的最大金额,dp[1]代表在拿当前节点的情况下所偷取的最大金额
若不拿取当前节点的,则需要在其左右子节点中判断是否要拿去,若拿当前节点,则应跳过其左右子节点去判断是否拿取下一层的金额
代码:
public int rob(TreeNode root) {
// 调用辅助函数 robAction 计算最大可窃取金额
int[] res = robAction(root);
// 返回 robAction 的结果中最大的值
return Math.max(res[0], res[1]);
}
int[] robAction(TreeNode root) {
// res[0]: 不打劫当前房子的最大金额
// res[1]: 打劫当前房子的最大金额
int res[] = new int[2];
// 如果当前节点为空,返回 [0, 0]
if (root == null)
return res;
// 递归计算左子树和右子树的结果
int[] left = robAction(root.left);
int[] right = robAction(root.right);
// res[0] 是不打劫当前房子的最大金额
// 选择打劫左右子树中最大的金额
res[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
// res[1] 是打劫当前房子的最大金额
// 当前房子的值加上左右子树不打劫的情况
res[1] = root.val + left[0] + right[0];
// 返回当前节点的结果
return res;
}
-
rob
方法:- 参数:
TreeNode root
表示二叉树的根节点。 - 功能:计算从根节点开始的最大可窃取金额。
- 步骤:
- 调用
robAction
方法获取当前树的最大可窃取金额。 res
数组的两个元素分别表示打劫和不打劫根节点的情况。- 使用
Math.max
选择这两种情况中的较大值作为最终结果并返回。
- 调用
- 参数:
-
robAction
方法:- 参数:
TreeNode root
表示当前处理的节点。 - 返回值:一个数组
res
,其中res[0]
表示不打劫当前节点时的最大可窃取金额,res[1]
表示打劫当前节点时的最大可窃取金额。 - 步骤:
- 初始化
res
数组为[0, 0]
,表示当前节点为空时的结果。 - 如果当前节点为
null
,直接返回[0, 0]
,因为没有任何价值可以打劫。 - 递归调用
robAction
方法处理左子树和右子树,分别将结果存储在left
和right
中。 -
res[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
- 在这种情况下,可以选择打劫或不打劫左右子树,从而得到最大金额。
res[1] = root.val + left[0] + right[0];
- 在这种情况下,打劫当前节点的值,并且左右子树的最大金额必须是它们分别不打劫的情况。
- 返回
res
数组,包含当前节点的打劫和不打劫情况的最大金额。
- 初始化
- 参数:
今天的学习就到这里