2020.05.29
问题描述
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例
第n家 | 1 | 2 | 3 | 4 |
家中钱数 | 3 | 2 | 6 | 1 |
输入:[3,2,6,1] 输出:9
答案代码
1. 先把大佬@lkaruga的代码悄悄贴上来——
class Solution {
public:
int rob(vector<int>& nums) {
int sumOdd = 0; //最直接的解题思路就是分奇偶来满足间隔盗窃的条件
int sumEven = 0; //先进行奇偶各自和的初始赋值
for (int i = 0; i < nums.size(); i++)
{
if (i % 2 == 0)
{
sumEven += nums[i];
sumEven = max(sumOdd, sumEven); //备注1
}
else
{
sumOdd += nums[i];
sumOdd = max(sumOdd, sumEven); //备注1
}
}
return max(sumOdd, sumEven); //返回奇或偶出发点中的最大值即可
};
};
分两种初始情况讨论——前两所房子中,偷1或者偷2。其他地方都还蛮好理解的,备注1部分是大佬思路里比较巧妙有趣的地方。简单来说,这两行备注1部分的代码就是为了解决这样的问题——不一定要所有房子间隔为1时才能偷到最多,在一些特定情况下,间隔可能是2。(间隔不会是3、4等更大的数字,因为间隔a、b、c这3间房子的话,b房间是可以去抢的,多多益善。)
e.g. 输入:[2,1,2,1,1,100]
错误输出1:5(2+2+1)
错误输出2:102(1+1+100)
正确输出:104(2+2+100)
区别于正常逻辑理解里只有让数组下标相差2才是实现间隔2的唯一方式,作者意识到间隔数是几的根源在于旧有奇偶形制是否发生了变化,即,若保持了1、3、5、7或2、4、6、8的单奇数或单偶数形制,间隔就会是1;而倘若发生一次跳换,例如1、3、6、8,那么在跳换奇偶形制时,也就满足了间隔变为2的情况。
为了应对这种情况,作者选择了max()函数来间接实现跳换。
由于我们只需要求出最终偷到的最大钱数,所以任一流程步骤中,若已经被判定为非最优解,便可以将其用更优解覆盖掉。于是在代码中的备注1处,可以看到,在以偶数0为偷窃起点(偷第一所房子,房子编号为0)的每一次新偷钱数加和完成之后,都需要将此方案下的现有总钱数和以奇数1为偷窃起点的方案(不偷第一所房子,偷第二所,房子编号为1)中上一次的总钱数进行比较。若比较结果里另一方案中上一次的总钱数还大一些,那么直接继承另一方案,完全抹掉本方案的旧值——实现跳换,房屋间隔数为2。
同理,以偷第二所房子开始的方案也实时需要和另一方案的旧值进行比较大小,随时面临着跳换的可能。
结合上例,该思路具体为:
房子编号 | 0 | 1 | 2 | 3 | 4 | 5 | 总钱数 |
钱数 | 2 | 1 | 2 | 1 | 1 | 100 | / |
从0偷起 | 2 | - | 4 | - | 5 | - | 5 |
从1偷起 | - | 1 | - | 2(2没有大于4!)=> 4 | - | 104 | 104 |
啊怎么样,是不是非常巧妙呢(#^.^#)(狗头)
2. 再来康康力扣官方的答案——
class Solution {
public:
int rob(vector<int>& nums) {
if (nums.empty()) { //类似归纳法的分类思路,从没有房子可偷开始,哈哈哈
return 0; //当然空空如也
}
int size = nums.size();
if (size == 1) { //只有一栋房子怎么办?只能给它偷咯
return nums[0]; //这家编号为0的房子里钱都归我啦!哈哈
}
int first = nums[0], second = max(nums[0], nums[1]); //备注2:开始递归了,下方文字详述
for (int i = 2; i < size; i++) {
int temp = second;
second = max(first + nums[i], second);
first = temp;
}
return second;
}
};
怎么说呢,这个思路其实也非常非常符合正常理解,估计老手都不用动脑子,直接上来就能写叭~ 所以作为归纳分类的前件,size = 0 和size = 1的情况不作解释咯,直接可以看代码中的注释。
备注2部分的逻辑梳理是这样的——
当进行到第n个房子时,在安全不被抓的情况下,我是偷呢?还是不偷呢?
当然看偷的钱多还是不偷的钱多了!!这时就有小白(比如我)会问:这不傻帽儿吗?反正安全,当然是进去偷的钱多啦!不偷,赚0元;偷,起码有1w。
其实非也。我们需要注意的是,偷或不偷还会影响对下一栋房子进行偷窃的安全系数。如果你因为这次偷窃是安全的就狠下黑手,那么下一栋房子里纵然是有100w也只能跳过了(ಥ﹏ಥ)。所以这种在当前安全的情况下“偷或不偷”的分类方式是合理的,且也巧妙地分离了间隔为1和2的情况(^-^)。
好的,现在只需要确定“偷或不偷”的判定标准就可以啦!
于是我们华丽引出“动态规划”的概念。锵锵锵锵~我们运用了递归的思想来写这个判定公式。由之前的代码,我们已知n<=2时的情况,那么问题来了,比如n=3时,这第三间房子偷不偷?安全的情况下,偷,总钱数就是nums[0] + nums[2] , 不偷,总钱数就是nums[0] ;不安全的情况下当然不能偷,总钱数就是nums[1]。
开始公式推导。
偷第一间房子时最多获得钱数MostMoney[ 0 ] = nums[ 0 ] ,
到第二间时最多获得的钱数MostMoney[ 1 ] = max ( nums[0] , nums[1] );
到第三间时最多获得的钱数MostMoney[ 2 ] = max ( MostMoney[ 1 ] , MostMoney[ 0 ] + nums[ 2 ] );
……
递归公式可写为:MostMoney[ i ] = MAX ( MostMoney[ i - 1 ] , MostMoney[ i - 2 ] + nums[ i ] )
在上面的代码中,标答使用了滚动数组,用temp临时数组实现了仅用三个存储位的O(1)空间复杂度;力扣也给出了完全存储此递归的数组循环写法:
for (int i = 2; i < size; i++) {
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
}
总结
动态规划——将大问题分解成子问题。
实现方式:
1.自底向上 解决完最末层的所有子问题之后,再往上一层迭代。自底向上的一个经典实现是斐波那楔数列的递推实现,即F[i] = F[i - 1] + F[i - 2] 。
2.自顶向下 也即记忆化搜索。从问题的最终状态出发,一般采用递归的方法实现。