打家劫舍系列问题
作者:Grey
原文地址:
LeetCode 198. 打家劫舍
主要思路
定义和原始数组一样长的dp
数组,
int[] dp = new int[dp]
dp[i]
的含义是:[0...i]
区间内,得到最大的金额是多少。
显然有
// [0...0]范围内,最大金额就是`arr[0]`
dp[0] = arr[0];
// [0...1]范围内,最大金额要不选`arr[0]`,要不选`arr[1]`,不能同时选,因为会报警
dp[1] = Math.max(arr[0],arr[1]);
针对普遍位置dp[i]
,有两个方案:
方案1,不选择i
位置,这样的话dp[i] = dp[i-1]
,即:i
位置的答案,只依赖i-1
位置的答案。
方案2,选择i
位置,这样的话dp[i] = arr[i] + dp[i - 2]
,即:i
位置的答案,依赖i
位置的值加上i-2
位置的答案。
以上两种方案取最大值,就是i
当前位置的最大获取金额数。
dp[i] = Math.max(dp[i - 1], arr[i] + dp[i - 2]);
完整代码
public static int rob(int[] arr) {
if (arr == null || arr.length == 0) {
return 0;
}
if (arr.length == 1) {
return arr[0];
}
if (arr.length == 2) {
return Math.max(arr[0], arr[1]);
}
final int n = arr.length;
int[] dp = new int[n];
dp[0] = arr[0];
dp[1] = Math.max(arr[0], arr[1]);
for (int i = 2; i < n; i++) {
dp[i] = Math.max(dp[i - 1], arr[i] + dp[i - 2]);
}
return dp[n - 1];
}
时间复杂度O(N)
,空间复杂度O(N)
,根据如上代码,可以看到,整个dp
数组是有递推关系的,所以,可以进行压缩数组优化,仅需要几个变量就可以实现上述过程,优化后的代码如下
public static int rob2(int[] arr) {
if (arr == null || arr.length == 0) {
return 0;
}
if (arr.length == 1) {
return arr[0];
}
if (arr.length == 2) {
return Math.max(arr[0], arr[1]);
}
final int n = arr.length;
int prePre = arr[0];
int pre = Math.max(arr[0], arr[1]);
int max = pre;
for (int i = 2; i < n; i++) {
int cur = Math.max(pre, prePre + arr[i]);
prePre = pre;
pre = cur;
max = Math.max(cur, max);
}
return max;
}
以上优化后的算法,时间复杂度O(N)
,空间复杂度O(1)
。
LeetCode 213. 打家劫舍 II
主要思路
之前的问题不是环形数组,本题是环形数组,所以,最后一个位置和第一个位置有强关联关系,所以我们分两部分来解
第一部分,不包括最后一个位置。
第二部分,一定要包括最后一个位置。
上述两个部分的最大值,就是整体的要求的答案。
第一部分,不包括最后一个位置,那么整个区间就是下标从[0....n-2]
,调用上一个非环形数组问题的解法。
final int n = arr.length;
// 以下情况是考虑最后一个位置
int prePre = arr[1];
int pre = Math.max(arr[1], arr[2]);
int max = pre;
for (int i = 3; i < n; i++) {
int cur = Math.max(pre, prePre + arr[i]);
prePre = pre;
pre = cur;
max = Math.max(cur, max);
}
第二部分,包括最后一个位置,那么整个区间就是下标从[1...n-1]
,调用上一个非环形数组问题的解法。
// 以下情况是不考虑最后一个位置
prePre = arr[0];
pre = Math.max(arr[0], arr[1]);
for (int i = 2; i < n - 1; i++) {
int cur = Math.max(pre, prePre + arr[i]);
prePre = pre;
pre = cur;
max = Math.max(cur, max);
}
最后max
即为环形数组下的答案。完整代码见
public static int rob(int[] arr) {
if (arr == null || arr.length == 0) {
return 0;
}
if (arr.length == 1) {
return arr[0];
}
if (arr.length == 2) {
return Math.max(arr[0], arr[1]);
}
if (arr.length == 3) {
return Math.max(Math.max(arr[0], arr[1]), arr[2]);
}
final int n = arr.length;
// 以下情况是考虑最后一个位置
int prePre = arr[1];
int pre = Math.max(arr[1], arr[2]);
int max = pre;
for (int i = 3; i < n; i++) {
int cur = Math.max(pre, prePre + arr[i]);
prePre = pre;
pre = cur;
max = Math.max(cur, max);
}
// 以下情况是不考虑最后一个位置
prePre = arr[0];
pre = Math.max(arr[0], arr[1]);
for (int i = 2; i < n - 1; i++) {
int cur = Math.max(pre, prePre + arr[i]);
prePre = pre;
pre = cur;
max = Math.max(cur, max);
}
return max;
}
LeetCode 337. 打家劫舍 III
主要思路
由于变成了树状,所以,对于任意子树,只需要考虑两种情况即可:
情况1,包含子树头节点的情况下,最大收益是多少。
情况2,不包含子树头节点的情况下,最大收益是多少。
定义数据结构
public class Info {
// 选头节点
public int yes;
// 不选头节点
public int no;
public Info(int y, int n) {
yes = y;
no = n;
}
}
同时定义递归函数
public Info p(TreeNode root) {
// TODO
}
这个递归函数的含义是:以root
为头节点的树的最大收益是多少。可能性有如下几种
可能性1,最大值包含了root
节点,那么对于root
的左子树left
和右子树right
,需要获取到不包含其左右子树头节点的情况下,最大收益是多少。
int yes = root.val + left.no + right.no;
可能性2,最大值不包含root
节点,那么对于root
的最有子树,只要给出左右子树获取到的最大收益就可以了,不需要考虑左右子树头节点是否包含进来的问题。
int no = Math.max(left.yes, left.no) + Math.max(right.yes, right.no);
最后Math.max(yes,no)
就是以root
为头的数得到的最大收益是多少。
完整代码如下
public int rob(TreeNode root) {
Info info = p(root);
return Math.max(info.yes, info.no);
}
public class Info {
// 选头节点
public int yes;
// 不选头节点
public int no;
public Info(int y, int n) {
yes = y;
no = n;
}
}
public Info p(TreeNode root) {
if (root == null) {
return new Info(0, 0);
}
Info left = p(root.left);
Info right = p(root.right);
int yes = root.val + left.no + right.no;
int no = Math.max(left.yes, left.no) + Math.max(right.yes, right.no);
return new Info(yes, no);
}