打家劫舍分简单版和进阶版两个题目,分别对应leetcode-198以及leetcode-213,
普通版题目:专业的小偷,每间房内都藏有一定的现金,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
进阶版的题目加了一个条件,房子首尾相连;
思路
我是先看的进阶版题目,首尾相连的屋子,所以这次从进阶版题目开始说起。
拿到题目的时候,第一个反应是贪心(后来血泪史告诉我这个想法是错误的),想着先找到钱最多的那家开始偷,然后再比较n+2和n+3的值,哪个值大就偷哪家(就这样还让我过了二十多个样例):比如(**[1,7,9,4]**这个例子就不满足)
然后我又想啊,实际上这也就两种情况,一种是从第一家开始偷,一种是从第二家开始偷,下一次偷的时候比较一下n+2和n+3的谁大,谁大要谁,貌似逻辑比刚才那个要清晰一点点,于是出现了下面这段代码:
var rob = function(nums) {
// 第一家和最后一家只能选一家来偷
// 每偷一家可以选择它的下一家,下下一家甚至更远的那家
// 假设偷第n家的时候金额最大,第n-1家时就是除了自己之外相加最大的值
// 按照贪心,先找出最有钱那家作为第一家(因为是一个圈,所以哪家作为第一家都是可以的)
// 然后根据某一个方向开始找,每次比较n+1,n+2的值,取最大那个,直到n+1或者n+2等于最后一个数
const maxNum = Math.max(...nums);
const len = nums.length;
if(len < 4){
return maxNum;
}
// 数组处理,将最大的那个数作为第一个数,它前面的数都丢到它后面
// const maxIndex = nums.indexOf(maxNum);
// let newArr = nums.splice(0,maxIndex);
// nums = nums.concat(newArr);
// let index = 0
const getMax=(i,arr)=>{
let newArr = arr.splice(0,i);
arr = arr.concat(newArr);
let res = arr[0];
let index = 0;
console.log(res)
while(index < len -1){
if(index +1 === len-1 || index +2 === len-1){
console.log(res,index)
return res
}
let comparedMax = index + 3 === len-1 ? arr[index + 2] : Math.max(arr[index + 2],arr[index +3]);
console.log(res,comparedMax)
res += comparedMax
index +=index + 3 === len-1 ? 2 : (arr[index + 2]>= arr[index +3] ? 2:3)
}
}
return Math.max(getMax(0,nums),getMax(1,nums))
}
有那么点点道理的同时,其实也会有问题,比如**[2,4,8,9,9,3]**,一眼就看出来取2,8,9,但是按照贪心,第二次对比的时候,比较8和9,就会选择9,导致最后只会选择2,9,3
两次失败证明贪心在这道题中可行性不高,但回过头来看,按照我的分析,前三点是没毛病的,那我们现在来想想,初始情况就两种,要么从第一家开始偷(这个时候不能偷最后一家),要么从第二家开始偷,到最后比较的应该就是这两种情况下的最大值。
const rob=(nums)=>{
const len = nums.length;
const getMax=(start,end)=>{
// TODO
}
getMax(0,len-1),getMax(1,len)
}
架子搭起来之后,先不急着考虑写函数内容,先看看特殊情况,因为是首尾相连,所以当房子数小于4的时候,房子都是相连的,这个时候谁家钱多就去谁家,于是有以下判断条件:
if(len < 4) return Math.max(...nums) // Math.max([]) === 0
现在该正式写两种情况怎么偷了,两种情况实际上都是一样的,假设现在想偷第i家时,怎么样性价比最高呢,两种情况,偷或者不偷,不偷意味着偷i-1家之后得到的金额大于在i-2家时的状态,因为偷了i-1家就不能偷第i家,在i-2家的状态不一定是偷了i-2家,而是在i-2家是获得的最大金额(陷入了一个重复的逻辑)。所以我们可以写出一个公式:
dp[i]=Math.max(dp[i-2]+ num[i],dp[i-1])
于是就写出了下面这段代码:
var rob = function(nums) {
// 第一家和最后一家只能选一家来偷
// 每偷一家可以选择它的下一家,下下一家甚至更远的那家
// 假设偷第n家的时候金额最大,第n-1家时就是除了自己之外相加最大的值
const len = nums.length;
if(len < 4) return Math.max(...nums);
const getMax=(start,end)=>{
let first = 0
let second = 0
for (let i = start; i < end; i++) {
let temp = second;
second = Math.max(first + nums[i],second);
first = temp;
}
return second
}
return Math.max(getMax(0,len-1),getMax(1,len))
}
于是,没有连成一个圈的打家劫舍也很快做出来了
var rob = function(nums) {
const len = nums.length;
if(len < 3) return Math.max(...nums);
let first = nums[0];
let second = Math.max(first, nums[1]);
for(let i = 2; i < len;i++){
let temp = second;
second = Math.max(first + nums[i],second);
first = temp;
}
return second
};
然而,在我准备睡觉的时候,我又发现了另外一个打家劫舍(leetcode-337)
题目:在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。
计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。
这道题下回分解。。。。。。