目录
1、LeetCode198.打家劫舍
1.1、动态规划(官方题解)
1.2、动态规划(二维数组)
1.3、回溯算法
2、LeetCode213.打家劫舍II(环形数组)
3、LeetCode337.打家劫舍III(二叉树)
3.1、动态规划
3.2、回溯算法
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 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 。
提示:
0 <= nums.length <= 100
0 <= nums[i] <= 400
定义一个二维数组dp[i][2],i表示房间数,用0、1表示不偷和偷两种状态, 状态方程如下:
//本房间不偷:上一间房偷、不偷都可以,取最大值
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]);
//本房间偷:上一间房没偷或者将偷的扔掉+本房间值,取最大值
dp[i][1] = Math.max(dp[i-1][0]+nums[i],dp[i-1][1]-nums[i-1]+ nums[i]);
= Math.max(dp[i-1][0],dp[i-1][1] - nums[i-1]) + nums[i];
上述存在重复比较计算,因为,本房间不偷的场景已经继承上一间房的偷的场景,因此本房间偷的场景可以直接等于上一件房没有偷的场景+加上本房间的金额即可。
//本房间不偷:上一间房偷、不偷都可以,取最大值
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]);
//本房间偷:上一间房没偷+加上本房间金额
dp[i][1] = dp[i-1][0] + nums[i];
或者,本房间不偷的场景直接继承上一间房 偷的场景,本房间偷的场景为上一间房没偷或者将偷的扔掉+本房间值,取最大值。
//本房间不偷:上一间房偷
dp[i][0] = dp[i-1][1];
//本房间偷:上一间房没偷或者将偷的扔掉+本房间值,取最大值
dp[i][1] = Math.max(dp[i-1][0]+nums[i],dp[i-1][1]-nums[i-1]+ nums[i]);
= Math.max(dp[i-1][0],dp[i-1][1] - nums[i-1]) + nums[i];
上述三种状态方程都对,第一种更为保险一点但稍微复杂一点,第二种更为容易接受。
public int rob(int[] nums) {
if(nums == null || nums.length == 0){
return 0;
}
int [][]dp = new int[nums.length][2];
dp[0][0] = 0;
dp[0][1] = nums[0];
for(int i = 1; i < nums.length;i++){
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]);
dp[i][1] = Math.max(dp[i-1][0],dp[i-1][1]-nums[i-1]) + nums[i];
}
return Math.max(dp[nums.length - 1][0],dp[ nums.length-1][1]);
}
...//替换成该状态方程也对
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]);
dp[i][1] = dp[i-1][0] + nums[i];
本题也可以用回溯算法,本房间不偷:去下一家;本房间偷:去下下家。
public int rob(int[] nums,int index) {
if(index >= nums.length){
return 0;
}else{
int robVal = rob(nums,index + 2) + nums[index];
int noRobVal = rob(nums,index + 1);
return Math.max(robVal,noRobVal);
}
}
存在重复计算用一个数组保存历史数据:
public int rob(int[] nums) {
if(nums == null || nums.length == 0){
return 0;
}
if(nums.length == 1){
return nums[0];
}
int []dp = new int[nums.length];
Arrays.fill(dp,-1);
return rob(nums,dp,0);
}
public int rob(int[] nums,int []dp,int index) {
if(index >= nums.length){
return 0;
}else{
if(dp[index] != -1){
return dp[index];
}
int robVal = rob(nums,index + 2) + nums[index];
int noRobVal = rob(nums,index+1);
dp[index]= Math.max(robVal,noRobVal);
return dp[index];
}
}
结果显示还是超时,尴尬。
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
示例 1:
输入: [2,3,2]
输出: 3
解释: 你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
示例 2:
输入: [1,2,3,1]
输出: 4
解释: 你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
和上一题不同的是,本次数组为环形数组,因此,首尾房间不能同时被抢,如示例1所示,那么只可能有三种不同情况:要么都不被抢;要么第一间房子被抢最后一间不抢;要么最后一间房子被抢第一间不抢。
public int rob(int[] nums) {
if(nums == null || nums.length == 0){
return 0;
}
if(nums.length == 1){
return nums[0];
}
return Math.max(rob(nums,1),rob(nums,0));
}
public int rob(int[] nums,int index) {
int [][]dp = new int[nums.length][2];
dp[index][0] = 0;
dp[index][1] = nums[index];//初始化起始偷的位置
for(int i = index + 1; i < nums.length;i++){
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][1]);
dp[i][1] = Math.max(dp[i-1][0],dp[i-1][1]-nums[i-1]) + nums[i];
}
if(index == 0){
return dp[nums.length - 1][0];//第一家偷,最后一家不能偷
}else{//第一家不偷即从第二家开始偷,最后一家可偷、可不偷
return Math.max(dp[nums.length - 1][0],dp[ nums.length-1][1]);
}
}
在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。
示例 1:
输入: [3,2,3,null,3,null,1]
3
/ \
2 3
\ \
3 1
输出: 7
解释: 小偷一晚能够盗取的最高金额 = 3 + 3 + 1 = 7.
示例 2:
输入: [3,4,5,1,3,null,1]
3
/ \
4 5
/ \ \
1 3 1
输出: 9
解释: 小偷一晚能够盗取的最高金额 = 4 + 5 = 9.
利用二维动态规划结构计算,如下:
public int rob(TreeNode root) {
int []vals = robEx(root);
return Math.max(vals[0],vals[1]);
}
public int[] robEx(TreeNode root) {
if(root == null){
return new int[]{0,0};
}
int []left = robEx(root.left);
int []right = robEx(root.right);
int leftVal = 0,rightVal = 0;
if(root.left != null){
leftVal = root.left.val;
}
if(root.right != null){
rightVal = root.right.val;
}
//本节点不偷,左侧可偷可不偷 + 右侧可偷可不偷
int noRobVal = Math.max(left[0],left[1]) + Math.max(right[0],right[1]);
//本节点偷,左侧可不偷/偷了扔掉 + 右侧可不偷/偷了扔掉 + 本地节点
int robVal = Math.max(left[0],left[1] - leftVal)
+ Math.max(right[0],right[1] - rightVal)+ root.val;
return new int[]{noRobVal,robVal};
}
用第二种状态方程改写,更为简洁:
public int[] robEx(TreeNode root) {
if(root == null){
return new int[]{0,0};
}
int []left = robEx(root.left);
int []right = robEx(root.right);
//本节点不偷,左侧可偷可不偷 + 右侧可偷可不偷
int noRobVal = Math.max(left[0],left[1]) + Math.max(right[0],right[1]);
//本节点偷,左侧可不偷 + 右侧可不偷 + 本地节点
int robVal = left[0] + right[0] + root.val;
return new int[]{noRobVal,robVal};
}
算法:
本节点不偷:去偷 左右子节点
本地偷取:去偷 左右子节点的子节点
注意事项:存在重复计算,需要保持历史路径,否则效率也太低了
Map map = new HashMap<>();
public int rob(TreeNode root) {
if(root == null){
return 0;
}
if(map.containsKey(root)){//避免重复计算
return map.get(root);
}
int noRobVal = rob(root.left)+ rob(root.right);//本地不偷则:取左右子节点
int leftChildRob = root.left == null ? 0: rob(root.left.left)+rob(root.left.right);
int rightChildRob = root.right == null ? 0: rob(root.right.left)+rob(root.right.right);
int robVal = leftChildRob + rightChildRob + root.val;//本地偷取则:去左右子节点的子节点
int max = Math.max(noRobVal,robVal);
map.put(root,max);
return max;
}