198.打家劫舍 Ⅰ
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 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 。
题解:动态规划法
如果你不了解动态规划法,参考看一遍就理解:动态规划详解
解题思路:
1:定义子问题
原问题是 “从全部房子中能偷到的最大金额”,假设房子的规模是n间,dp(n)表示从n个房间中能偷到的最大金额。将问题的规模缩小,子问题就是 “从 k(k<=n) 个房子中能偷到的最大金额 ”,用 f(k) 表示。通过求出子问题dp(0),dp(1)dp(2)...dp(n-3),dp(n-2),dp(n-1)来求出dp(n)。
2:写出子问题递推关系式
从最后一间房子分析,如果房屋数量大于两间,对于第 n (n>1)间房屋,有两个选项:偷还是不偷
如果偷窃第n间房屋,那么就不能偷窃第n-1间房屋,偷窃总金额为前n-2间房屋的最高总金额即dp(n-2)与第n间房屋的金额之和。
如果不偷窃第n间房屋,偷窃总金额为前n-1间房屋的最高总金额即dp(n-1)。
在两个选项中选择偷窃总金额较大的选项,该选项对应的偷窃总金额即为n间房屋能偷窃到的最高总金额。(nums数组代表下标为0,1,2...n-1间房子的可偷窃金额数组)
那么就有如下的状态转移方程:dp[n]=max(dp[n−2]+nums[n-1],dp[n−1])
在写递推关系的时候,写上 n=0 和 n=1的边界情况:
当 n=0 时,没有房子,所以 dp(0)=0。
当 n=1 时,只有一个房子,偷这个房子即可,所以 dp(1)=nums[0]
这样构成完整的递推关系。
dp(0)=0 (n=0)
dp(1)=nums[1] (n=1)
dp[n]=max(dp[n−2]+nums[n-1],dp[n−1]) (n>1)
由递推关系可知:dp[n] 依赖 dp[n-1] 和 dp[n-2]。那么dp[n-1] 依赖 dp[n-3] 和 dp[n-2]...最后dp[2] 依赖 dp[1] 和 dp[0].所以我们可以从边界条件dp[0]和dp[1]最后求出dp[n].
确定了 dp 数组的计算顺序之后,我们就可以写出题解代码:
var rob = function(nums) {
if(nums.length===0){
return 0
}
let N=nums.length
let dp=[]
dp[0]=0
dp[1]=nums[0]
for(let n=2;n<=N;n++){
dp[n]=Math.max(dp[n-1],dp[n-2]+nums[n-1])
}
return dp[N]
};
213.打家劫舍 Ⅱ
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
示例 1:
输入: nums = [2,3,2]
输出: 3
解释: 你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
示例 2:
输入: nums = [1,2,3,1]
输出: 4
解释: 你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
示例 3:
输入: nums = [1,2,3]
输出: 3
解题思路:
此题是《打家劫舍 Ⅰ》的拓展: 唯一的区别是此题中的房间是环状排列的(即首尾相接),而《打家劫舍 Ⅰ》中的房间是单排排列的。
环状排列意味着第一个房子和最后一个房子中 只能选择一个偷窃,因此可以把此环状排列房间问题约化为单排排列房间子问题:
因为从第一个房间来分析,存在偷与不偷的情况,如果偷,则最后一家一定不能偷,将最后一家置为0,即把nums数组截取[0,n-2],以此数组为基准运行198题的代码得到一个结果。如果不偷,那么把第一家置为0,最后一家无所谓,即把nums数组截取[1,n-1],以此数组为基准运行198题代码,又得到一个结果。返回两个结果较大的一个即可。
1.在不偷窃第一个房子的情况下,nums截取[1,n-1],最大金额是 p1
2.在不偷窃最后一个房子的情况下,nums截取[0,n-2],最大金额是 p2
综合偷窃最大金额: 为以上两种情况的较大值,即 max(p1,p2)。
// rob方法即为《打家劫舍 Ⅰ》中的rob方法
var robF = function(nums) {
if(nums.length===0){
return 0
}
if(nums.length===1){
return nums[0]
}
return Math.max(rob(nums.slice(1)),rob(nums.slice(0,nums.length-1)))
};
337.打家劫舍 Ⅲ
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。
除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。
给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。
示例 1:
输入: root = [3,2,3,null,3,null,1]
输出: 7
解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7
示例 2:
输入: root = [3,4,5,1,3,null,1]
输出: 9
解释: 小偷一晚能够盗取的最高金额 4 + 5 = 9
解题思路:
简化一下这个问题:一棵二叉树,树上的每个点都有对应的权值,每个点有两种状态(选中和不选中),问在不能同时选中有父子关系的点的情况下,能选中的点的最大权值和是多少。
我们用数组 arrDfs 表示某node节点的选中和不选中的最大权值和。arrDfs的结构是[f,g]。其中f表示某node节点选中时的最大权值和;g表示不选择某node节点的最大权值和。
(此处,力扣官方已经将数组中的值自动转化为二叉树的node节点,方法我们不探究了,node节点结构如下)
// Definition for a binary tree node.
function TreeNode(val, left, right) {
this.val = (val===undefined ? 0 : val)
this.left = (left===undefined ? null : left)
this.right = (right===undefined ? null : right)
}
1:定义子问题
从根节点rootNode出发,可知rootNode的arrDfs即[f,g]数组需要通过其左右子节点的arrDfs即左右子节点[f,g]数组算出,将问题的规模缩小,子问题就是 “求出子节点的arrDfs数组 ”最后通过值为null的节点的arrDfs就是[0,0]从而自底向上求出根节点root的arrDfs。
2:写出子问题递推关系式
当某node被选中时,该node的左右孩子都不能被选中,故node被选中情况下,是加上子树均不被选中的最大权值和,即node.val+node.left的arrDfs[1]+node.right的arrDfs[1]。就是该node节点数组中f的值
当某node不被被选中时,该node的左右孩子可选也可不选,故我们选取左右节点f与g中的较大值之和。即 max(node.left的arrDfs[0],node.left的arrDfs[1])+max(node.right的arrDfs[0],node.right的arrDfs[1])
边界条件:当node是null,arrDfs=[0,0]
最终我们通过比较根节点rootNode的arrDfs数组的f和g值,即max(f,g)
写出题解代码:
var rob = function(rootNode) {
const dfs = (node) => {
if (node === null) {
return [0, 0];
}
const l = dfs(node.left);
const r = dfs(node.right);
const selected = node.val + l[1] + r[1];
const notSelected = Math.max(l[0], l[1]) + Math.max(r[0], r[1]);
return [selected, notSelected];
}
const rootStatus = dfs(rootNode);
return Math.max(rootStatus[0], rootStatus[1]);
};
2560.打家劫舍 Ⅳ
沿街有一排连续的房屋。每间房屋内都藏有一定的现金。现在有一位小偷计划从这些房屋中窃取现金。
由于相邻的房屋装有相互连通的防盗系统,所以小偷 不会窃取相邻的房屋 。
小偷的 窃取能力 定义为他在窃取过程中能从单间房屋中窃取的 最大金额 。
给你一个整数数组 nums 表示每间房屋存放的现金金额。形式上,从左起第 i 间房屋中放有 nums[i] 美元。
另给你一个整数 k ,表示窃贼将会窃取的 最少 房屋数。小偷总能窃取至少 k 间房屋。
返回小偷的 最小 窃取能力。
示例 1:
输入: nums = [2,3,5,9], k = 2
输出: 5
解释:
小偷窃取至少 2 间房屋,共有 3 种方式:
- 窃取下标 0 和 2 处的房屋,窃取能力为 max(nums[0], nums[2]) = 5 。
- 窃取下标 0 和 3 处的房屋,窃取能力为 max(nums[0], nums[3]) = 9 。
- 窃取下标 1 和 3 处的房屋,窃取能力为 max(nums[1], nums[3]) = 9 。
因此,返回 min(5, 9, 9) = 5 。
示例 2:
输入: nums = [2,7,9,3,1], k = 2
输出: 2
解释: 共有 7 种窃取方式。窃取能力最小的情况所对应的方式是窃取下标 0 和 4 处的房屋。返回 max(nums[0], nums[4]) = 2 。
题目解析:小偷需要在保证“房屋的可偷盗数量”>=k的情况下,“偷走的最大金额”要尽量的小。我们分析可知,“房屋的可偷盗数量”和“偷走的最大金额”之间存在单调关系。
因为“偷走的最大金额”越大,那么“房屋的可偷盗数量”越少,例如 nums=[1,4,2,3]在最大金额为 2 时,“房屋的可偷盗数量”是2,只有 1 和 2 是可以偷的;在最大金额为 4 时,“房屋的可偷盗数量”是4,nums 中 1,2,3,4都可以偷。所以“房屋的可偷盗数量”和“偷走的最大金额”之间存在单调递增关系。
我们用f(y)表示“房屋的可偷盗数量”和“偷走的最大金额”之间的单调关系,其中y代表“偷走的最大金额”,f(y)表示“房屋的可偷盗数量”,则可以由题意我们可以将问题转化为:保证f(y)>=k的情况下,求y的最小值。
题解:二分法+贪心法
因为f(y)具有单调递增关系,所以我们要找到最小值y,并且在[min(nums),max(nums)]区间内,我们可以用二分法找到这个值。使得middle=⌊(min(nums)+max(nums))/2⌋,如果f(middle)>=k,因为单调性我们可以知道y可能可以更小,此时我们将搜索区间定位到[min(nums),middle-1],否则定位搜索区间到[middle+1,max(nums)](关于二分法开闭区间取值问题可参考二分法区间取值)
此处的贪心法应用的地方,是在遍历nums数组收集“房屋的可偷盗数量”时候。会从第一间房子只要满足nums[i]<=y,就给“房屋的可偷盗数量”开始计数,因为只要满足条件对“房屋的可偷盗数量”的贡献都是+1,所以能早计数就要早开始计数,遇到能抢的就抢了,不管下一个是不是能抢,都把下一个跳过,这样可以得到“房屋的可偷盗数量”是最大值。
由此我们可以得到如下代码:
var minCapability = function(nums, k) {
let lower = Math.min(...nums);
let upper = Math.max(...nums);
while (lower <= upper) {
const middle = Math.floor((lower + upper) / 2);
let count = 0;
for (let i = 0; i < nums.length; i++) {
if (nums[i] <= middle) {
count++;
i++
}
}
if (count >= k) {
upper = middle - 1;
} else {
lower = middle + 1;
}
}
return lower;
};