动态规划是算法中常常考察的内容. 开始学习这一块的内容. 跟着 Leetcode 上的题目来学习.
本文参考了 大佬的文章, 上面有详细完整的解释, 可以去看看.
198. 打家劫舍
本题的思路较为简单, 用 dp[i] 表示子问题, 就是偷到第 i 家时, 小偷能偷到的最多的钱. 因为不能同时偷两间相同的房屋. 所以 dp[i] 的值有两个可能, 选择偷第 i 家的话, 那么一定不能偷 i - 1 家, 如果不偷第 i 家的话, 那么就可以偷第 i - 1 家.
dp[i] = Math.max(dp[i-1], dp[i - 2] + num[i])
class Solution {
public int rob(int[] nums) {
if (nums.length == 0) return 0;
// 因为会取到 dp[nums.length]
int dp[] = new int[nums.length + 1];
dp[0] = 0;
dp[1] = nums[0];
for(int i = 2; i <= nums.length; i++) {
dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i-1]);
}
return dp[nums.length];
}
}
从递归关系式里可以看到, 当前 dp [i] 只与 dp[i -1] 和 dp[i- 2] 相关, 所以这里可以使用三个整数来替代数组, 减小空间复杂度
class Solution {
public int rob(int[] nums) {
if (nums.length == 0) return 0;
int prev = 0;
int curr = 0;
for(int x : nums) {
int temp = curr;
// 给 curr 赋的新值表示 dp[i]
// curr 的旧值表示 dp[i - 1]
// prev 表示 dp[i - 2]
curr = Math.max(curr, prev + x);
prev = temp;
}
return curr;
}
}
213. 打家劫舍 ||
这里其实就是上面一题的进一步的情况, 因为第一个房间和最后一个房间是紧挨着的, 所以如果分为两种情况 :
1. 选择的范围包括第一间房子, 但是不包括最后一间房子;
2. 选择的范围包括最后一间房子, 但是不包括第一间房子;
所以可以将这两种情况分别计算一遍, 然后取两者的较大值即可.
class Solution {
public int rob(int[] nums) {
if (nums.length == 0) return 0;
if (nums.length == 1) return nums[0];
int max1 = robhelper(Arrays.copyOfRange(nums, 0, nums.length - 1));
int max2 = robhelper(Arrays.copyOfRange(nums, 1, nums.length));
return Math.max(max1, max2);
}
public int robhelper(int[] arr) {
int prev = 0;
int curr = 0;
for(int x : arr) {
int temp = curr;
curr = Math.max(curr, prev + x);
prev = temp;
}
return curr;
}
}
337.打家劫舍
本题就直接将树的情景放进来了. 相邻的节点的是不能同时打劫的. 看了高票的题解, 来说一下这题的思路, 用 F (
首先考虑通用的情况 : 三层的二叉树, 一个爷爷节点, 两个父节点, 四个孩子节点.
如果访问了爷爷节点 : 那么我们只能访问四个孩子节点.
或者我们访问这两个父节点.
那么对应的表达式就是 dp[root] = dp[root.left.left] + dp[root.left.right] + dp[root.right.left] + dp[root.right.right] + root.val;
dp[root] = dp[root.left] + dp[root.right] ; 比较这两个值的大小, 就是 dp[root].
那么按这个思路下来的直白版本 :
class Solution {
public int rob(TreeNode root) {
return robHelper(root);
}
public int robHelper(TreeNode root) {
if (root == null) return 0;
int money = root.val;
if (root.left != null) {
money += (robHelper(root.left.left) + robHelper(root.left.right));
}
if (root.right != null) {
money += (robHelper(root.right.left) + robHelper(root.right.right));
}
return Math.max(money, (robHelper(root.left) + robHelper(root.right)));
}
}
但是上面的思路的时间复杂度过高. 主要在于重复的计算, 在计算 root 的时候, 同时也计算了 root.left 和 root.right, 以及其子结点, 所以可以使用一个 hashMap 来存储求过的值. 减少时间复杂度.
class Solution {
public int rob(TreeNode root) {
HashMap<TreeNode, Integer> map = new HashMap<>();
return robHelper(root, map);
}
public int robHelper(TreeNode root, HashMap<TreeNode, Integer> map) {
if (root == null) return 0;
if (map.containsKey(root)) {
return map.get(root);
}
int money = root.val;
if (root.left != null) {
money += (robHelper(root.left.left, map) + robHelper(root.left.right, map));
}
if (root.right != null) {
money += (robHelper(root.right.left, map) + robHelper(root.right.right, map));
}
int result = Math.max(money, (robHelper(root.left, map) + robHelper(root.right, map)));
map.put(root, result);
return result;
}
}
还有一种思路, 就是每个节点可以选择取或者不取, 用一个长度为 2 的数组 result 来表示取或者不取的结果, result[0] 表示不取, result[1] 表示取.
root 节点的 result[0] = root.left取或者不取的最大值, 加上 root.left 取或者不取的最大值. (因为这里 左右子结点可以选或者不选)
root 节点的 result[1] = root.val + root.left 不选的值 + root.right 不选的值, (因为选了 root , 所以左右子结点都不能选)
class Solution {
public int rob(TreeNode root) {
int[] res = robHelper(root);
return Math.max(res[0], res[1]);
}
public int[] robHelper(TreeNode root) {
if (root == null) return new int[2];
int[] result = new int[2];
// 来取左右子结点的结果
int[] left = robHelper(root.left);
int[] right = robHelper(root.right);
result[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
// 因为left 和 right 子结点都不能选
result[1] = left[0] + right[0] + root.val;
return result;
}
}