原题链接
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。 给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
我们规定dp[i]
:为到第i
天为止,能偷到的最大金额。
dp[i] = dp[i-1] + num[i] = dp[i-2] + num[i]
dp[i] = dp[i-1]
因此:
dp[i] = Math.max(dp[i-2] + num[i], dp[i-1])
那么确定了递归公式之后,我们需要考虑动态规划数组的初始值了。考虑到上面的i-2
,因此我们需要考虑两个初始变量。
dp[0]
:也就是第一天的最大利润是多少,肯定是偷了才有利润:dp[0] = num[0]
dp[1]
:最大利润:要么第一天偷,要么第二天偷,取两者最高。dp[1] = max (num[0],num[1])
那么最终结果如下:
public int rob(int[] nums) {
int len = nums.length;
// 特判
if (len == 0) {
return 0;
}
if (len == 1) {
return nums[0];
}
// 初始化
int[] dp = new int[len];
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
// 递归
for (int i = 2; i < len; i++) {
dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[len - 1];
}
在第一题的基础上:房屋都围成了一圈。那么相比第一题,就要多思考几个点:
比如我有5个房子:[A,B,C,D,E]
, 那么面临的选择就是以下几种:(首尾最多选择一种)
A
不偷,E
可能偷。A
可能偷,E
不偷。那么也就是在第一题的情况下做出选择:
代码就是:
public int rob(int[] nums) {
int len = nums.length;
// 特判
if (len == 0) {
return 0;
}
if (len == 1) {
return nums[0];
}
if (len == 2) {
return Math.max(nums[0], nums[1]);
}
// 情况一:第一家肯定不偷
int res1 = robOne(Arrays.copyOfRange(nums, 1, len));
// 情况二:最后一家肯定不偷
int res2 = robOne(Arrays.copyOfRange(nums, 0, len - 1));
return Math.max(res1, res2);
}
// 第一题代码
public int robOne(int[] nums) {
int len = nums.length;
// 初始化
int[] dp = new int[len];
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
// 递归
for (int i = 2; i < len; i++) {
dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[len - 1];
}
第三题呢则是在第一题的基础上,将数组房屋改造成了一个二叉树:即相邻的两个节点也是不能够偷的。
这个题目由于搞成了二叉树结构,那么就跟原本的数组结构天差地别。
假设dp[node][i]
代表以node
为根节点时,能取到的最大利润值。i
有两种值:0代表不偷当前节点,1代表偷当前节点。我们需要考虑的就是:
那么我们就需要自底向上
推算根节点root
能够获取的最大利润。
我们写一个递归函数,他返回一个int[]
类型的数组,即:
public int[] dfs(TreeNode node) {
}
他代表以node
为根节点时,返回一个数组大小为2的最大利润集:分别为当偷当前节点和不偷当前节点时候的最大利润。
那么首先,对于递归函数的终止条件,这个简单明了:
if (node == null) {
return new int[]{0, 0};
}
其次我们拿到当前根节点的左右孩子的最大利润集合:
int[] left = dfs(node.left);
int[] right = dfs(node.right);
以左孩子为例,left
集合代表着如下含义:
node.left
为根节点,并且偷node.left
的最大利润。node.left
为根节点,并且不偷node.left
的最大利润。那么接下来的,就应该写最大利润是怎么获得的:分为两种情况:
dp[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1])
left[0]
和right[0]
:dp[1] = node.val + left[0] + right[0];
int[] dp = new int[2];
dp[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
dp[1] = node.val + left[0] + right[0];
return dp;
最终代码就是:
public int rob(TreeNode root) {
int[] res = dfs(root);
return Math.max(res[0], res[1]);
}
public int[] dfs(TreeNode node) {
if (node == null) {
return new int[]{0, 0};
}
// 以左孩子为根节点时,取到的 两种 最大利润可能。
int[] left = dfs(node.left);
// 以右孩子为根节点时,取到的 两种 最大利润可能。
int[] right = dfs(node.right);
int[] dp = new int[2];
// 以node为根节点时能够获取的最大利润,并且当前节点不偷。
// 那么左右孩子都可以偷,因此当前节点的最大利润为两个孩子的最大利润之和
dp[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
// 以node为根节点时能够获取的最大利润,并且当前节点偷
// 那么当前节点偷了的话,它的左右孩子肯定是不能够偷的,根据 相邻不能偷 的原则
// 那么这种情况下以左孩子为例,左孩子肯定是不能够偷的,那么它的最大利润就是left[0]
dp[1] = node.val + left[0] + right[0];
return dp;
}