打家劫舍系列問題
作者: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);
}