题记:
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 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 。
提示:
题目来源:
作者:LeetCode
链接:https://leetcode.cn/leetbook/read/top-interview-questions-easy/xnq4km/
来源:力扣(LeetCode)
官方答案:
本质也是动态规划
class Solution {
/**
* @param Integer[] $nums
* @return Integer
*/
function rob($nums) {
$n = count($nums);
$dp = [0, $nums[0]];
for ($i = 2; $i <= $n; $i++) {
// max($dp[$i-1], $dp[$i-2])
$dp[$i] = max($dp[$i-1], ($dp[$i-2] + $nums[$i-1]));
}
return $dp[$n];
}
}
一:动态规划
这题和《动态规划解按摩师的最长预约时间》完全一样,只不过换了种写法,换汤不换药。我们可以参照写一下
数组中的值表示的是存放的金额,小偷可以选择偷和不偷,如果前一个偷了,那么下一个肯定是不能偷的,因为相邻的房屋在同一晚上被小偷闯入,系统会自动报警。如果上一个没偷,那么下一个可以选择偷也可以选择不偷,视情况而定。
这里可以定义一个二维数组dp[length][2],其中dp[i][0]表示第i+1(因为数组下标是从0开始的,所以这里是i+1)家没偷的最大总金额,dp[i][1]表示的是第i+1家偷了的最大总金额。那么我们找出递推公式
1,dp[i][0]=max(dp[i-1][0],dp[i-1][1])
他表示如果第i+1家没偷,那么第i家有没有偷都是可以的,我们取最大值即可。
2,dp[i][1]=dp[i-1][0]+nums[i]
他表示的是如果第i+1家偷了,那么第i家必须没偷,这里nums[i]表示的是第i+1家偷的金额。
递推公式找出来之后我们再来看下边界条件,第一家可以选择偷,也可以选择不偷,所以
dp[0][0]=0,第一家没偷 .
dp[0][1]=nums[0],第一家偷了
最后再来看下代码
public int rob(int[] nums) {
//边界条件判断
if (nums == null || nums.length == 0)
return 0;
int length = nums.length;
int[][] dp = new int[length][2];
dp[0][0] = 0;//第1家没偷
dp[0][1] = nums[0];//第1家偷了
//从第2个开始判断
for (int i = 1; i < length; i++) {
//下面两行是递推公式
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1]);
dp[i][1] = dp[i - 1][0] + nums[i];
}
//最后取最大值即可
return Math.max(dp[length - 1][0], dp[length - 1][1]);
}
转换为PHP代码为:
public function rob($nums){
//边界条件判断
if(count($nums) <= 0)
return 0;
$length = count($nums);
$dp = [[]];
$dp[0][0] = 0; //第一家没偷
$dp[0][1] = $nums[0]; //第一家偷了
//从第二个开始判断
for($i = 1; $i < $length; $i++){
//下面两行是递推公式
$dp[$i][0] = max($dp[$i - 1][0], $dp[$i - 1][1]);
$dp[$i][1] = $dp[$i - 1][0] + $nums[$i];
}
//最后取最大值即可
return max($dp[$length - 1][0], $dp[$length - 1][1]);
}
二:动态规划优化
上面定义了一个二维数组,但每次计算的时候都只是用二维数组的前一对值,在往前面的就永远使用不到了,这样就会造成巨大的空间浪费,所以我们可以定义两个变量来解决,来看下代码
public int rob(int[] nums) {
//边界条件判断
if (nums == null || nums.length == 0)
return 0;
int length = nums.length;
int dp0 = 0;//第1家没偷
int dp1 = nums[0];//第1家偷了
//从第2个开始判断
for (int i = 1; i < length; i++) {
//防止dp0被修改之后对下面运算造成影响,这里
//使用一个临时变量temp先把结果存起来,计算完
//之后再赋值给dp0.
int temp = Math.max(dp0, dp1);
dp1 = dp0 + nums[i];
dp0 = temp;
}
//最后取最大值即可
return Math.max(dp0, dp1);
}
转换为PHP代码为:
public function rob($nums){
//动态规划优化
//边界条件
if(count($nums) <= 0)
return 0;
$length = count($nums);
$dp0 = 0; //第一家没偷
$dp1 = $nums[0]; //第一家偷了
//从第二个开始判断
for($i = 1; $i < $length; $i++){
//防止dp0被修改之后对下面的运算造成影响,
//这里使用一个临时变量temp先把结果存起来,
//计算完成之后再赋值给dp0
$temp = max($dp0, $dp1);
$dp1 = $dp0 + $nums[$i];
$dp0 = $temp;
}
//最后取最大值即可
return max($dp0, $dp1);
}
三:递归
之前写过一篇文章《什么是递归,通过这篇文章,让你彻底搞懂递归》,里面提到递归的两个重要要素,一个是调用自己,一个是必须要有终止条件,我们先来定义一个函数
private int robHelper(int[] nums, int i) {
}
他表示的是前i+1(i是从0开始的,0表示的是第1个房屋)个房屋所能偷窃到的最大值,那么很明显
private int robHelper(nums, i-1)
表示的是前i个房屋所能偷窃到的最大值
private int robHelper(nums, i-2)
表示的就是前i-1个房屋所能偷窃到的最大值
因为第i-1个房屋和第i+1个房屋是不挨着的,所以如果偷完前i-1个房屋之后是可以再偷第i+1个房屋的。所以我们可以找到一种关系就是
private int robHelper(int[] nums, int i) {
//偷上上家之前所能得到的最大值
int lastLast = robHelper(nums, i - 2);
//偷上家之前所能得到的最大值
int last = robHelper(nums, i - 1);
//偷上上家之前的还可以再偷当前这一家
int cur = lastLast + nums[i];
//然后返回偷当前这一家和不偷当前这一家的最大值
return Math.max(last, cur);
}
问题结束了吗,当然没有,因为我们知道递归必须要有终止条件,那么终止条件是什么呢,就是i小于0,也就是说没有房屋可偷,最终代码如下。
public int rob(int[] nums) {
return robHelper(nums, nums.length - 1);
}
private int robHelper(int[] nums, int i) {
//终止条件
if (i < 0)
return 0;
//偷上上家之前所能得到的最大值
int lastLast = robHelper(nums, i - 2);
//偷上家之前所能得到的最大值
int last = robHelper(nums, i - 1);
//偷上上家之前的还可以再偷当前这一家
int cur = lastLast + nums[i];
//然后返回偷当前这一家和不偷当前这一家的最大值
return Math.max(last, cur);
}
转换为PHP代码为:
public function rob($nums){
//递归
return $this->robHelper($nums,count($nums) - 1);
}
private function robHelper($nums, $i){
//终止条件
if($i < 0)
return 0;
//偷上上家之前所能得到的最大值
$lastLast = $this->robHelper($nums, $i - 2);
//偷上家之前所能得到的最大值
$last = $this->robHelper($nums, $i - 1);
//偷上上家之前的还可以再偷当前这一家
$cur = $lastLast + $nums[$i];
//然后返回偷当前这一家和不偷当前这一家的最大值
return max($last, $cur);
}
运行超时,需要优化
四:递归优化
之前讲《剑指 Offer-斐波那契数列》和《青蛙跳台阶相关问题》的时候都提到过斐波那契数列的递归计算方式,递归的时候效率是很差的,因为代码中包含大量的重复计算。这题也一样,如果非要使用递归的方式解决,可以把计算的值先存起来,下次用的时候如果有就直接去取,如果没有,再计算。
public int rob(int[] nums) {
return robHelper(nums, nums.length - 1, new HashMap<>());
}
private int robHelper(int[] nums, int i, Map<Integer, Integer> map) {
//终止条件
if (i < 0)
return 0;
int lastLast = 0;
int last = 0;
//查看map中是否存在,如果存在就从map中取,不用再计算了
if (map.containsKey(i - 2))
lastLast = map.get(i - 2);
else {
//偷上上家之前所能得到的最大值
lastLast = robHelper(nums, i - 2, map);
//如果map中不存在就计算,计算完之后要存储在map中,下次用的
//时候直接从map中取,不用再计算了。
map.put(i - 2, lastLast);
}
//原理同时
if (map.containsKey(i - 1))
last = map.get(i - 1);
else {
//偷上家之前所能得到的最大值
last = robHelper(nums, i - 1, map);
map.put(i - 1, last);
}
//偷上上家之前的还可以再偷当前这一家
int cur = lastLast + nums[i];
//然后返回偷当前这一家和不偷当前这一家的最大值
return Math.max(last, cur);
}
方法来源:
作者:数据结构和算法
链接:https://leetcode.cn/leetbook/read/top-interview-questions-easy/xnq4km/?discussion=PmI185
来源:力扣(LeetCode)