LeetCode 打家劫舍问题

LeetCode 打家劫舍问题

一:House Robber1

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。

示例 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(base
case) 。

以上的逻辑很简单吧, 其实已经明确了「状态」 和「选择」 : 你⾯前房⼦的
索引就是状态, 抢和不抢就是选择

在这里插入图片描述
在两个选择中, 每次都选更⼤的结果, 最后得到的就是最多能抢到的money

// 主函数
public int rob(int[] nums) {
	return dp(nums, 0);
}

// 返回 nums[start..] 能抢到的最⼤值
private int dp(int[] nums, int start) {
	if (start >= nums.length) {
		return 0;
	} 
	int res = Math.max(
	// 不抢, 去下家
			dp(nums, start + 1),
	// 抢, 去下下家
			nums[start] + dp(nums, start + 2)
					);
	return res;
}

明确了状态转移, 就可以发现对于同⼀ start 位置, 是存在重叠⼦问题的, ⽐如下图:

在这里插入图片描述

如果每次到这都进⼊递归, 岂不是浪费时间? 所以说存在重叠⼦问题, 可以⽤备忘录进⾏优化:

private int[] memo;
// 主函数
public int rob(int[] nums) {
	// 初始化备忘录
	memo = new int[nums.length];
	Arrays.fill(memo, -1);
	// 强盗从第 0 间房⼦开始抢劫
return dp(nums, 0);
} 

// 返回 dp[start..] 能抢到的最⼤值
private int dp(int[] nums, int start) {
	if (start >= nums.length) {
		return 0;
	} 
	// 避免重复计算
	if (memo[start] != -1) 
		return memo[start];
	int res = Math.max(dp(nums, start + 1),
	nums[start] + dp(nums, start + 2));
	
	// 记⼊备忘录
	memo[start] = res;
	return res;
}

这就是⾃顶向下的动态规划解法, 我们也可以略作修改, 写出⾃底向上的解
法:

int rob(int[] nums) {
	int n = nums.length;
	// dp[i] = x 表⽰:
	// 从第 i 间房⼦开始抢劫, 最多能抢到的钱为 x
	// base case: dp[n] = 0
	int[] dp = new int[n + 2];
	for (int i = n - 1; i >= 0; i--) {
		dp[i] = Math.max(dp[i + 1], nums[i] + dp[i + 2]);
	} 
	return dp[0];
}

我们⼜发现状态转移只和 dp[i] 最近的两个状态有关, 所以可以进⼀步优化, 将空间复杂度降低到 O ( 1 ) O(1)

int rob(int[] nums) {
	int n = nums.length;
	
	// 记录 dp[i+1] 和 dp[i+2]
	int dp_i_1 = 0, dp_i_2 = 0;
	
	// 记录 dp[i]
	int dp_i = 0;
	for (int i = n - 1; i >= 0; i--) {
		dp_i = Math.max(dp_i_1, nums[i] + dp_i_2);
		dp_i_2 = dp_i_1;
		dp_i_1 = dp_i;
	} 
	return dp_i;
}

C++代码:

class Solution {
public:
    int rob(vector<int>& nums) {
        int n = nums.size();
        if(n == 0)
        {
            return n;
        }

	    // 记录 dp[i+1] 和 dp[i+2]
	    int dp_i_1 = 0, dp_i_2 = 0;
	
	    // 记录 dp[i]
	    int dp_i = 0;
	    for (int i = n - 1; i >= 0; i--) {
            //dp_i代表当前在i位置开始抢劫所能获得的最大momery,
		    dp_i = max(dp_i_1, nums[i] + dp_i_2);

            //记录在i - 2和 i - 1位置(反向遍历的)处开始抢劫所能获得的最大memory
		    dp_i_2 = dp_i_1;
		    dp_i_1 = dp_i;
	    } 
	    return dp_i;
    }
};

二:House Robber II

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。

示例 1:

输入: [2,3,2]
输出: 3
解释: 你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。

示例 2:

输入: [1,2,3,1]
输出: 4
解释: 你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。

这道题⽬和第⼀道描述基本⼀样, 强盗依然不能抢劫相邻的房⼦, 输⼊依然是⼀个数组, 但是告诉你这些房⼦不是⼀排, ⽽是围成了⼀个圈。也就是说, 现在第⼀间房⼦和最后⼀间房⼦也相当于是相邻的, 不能同时抢

这个约束条件看起来应该不难解决, 那么在这个问题上怎么处理呢?

⾸先, ⾸尾房间不能同时被抢, 那么只可能有三种不同情况: 要么都不被抢; 要么第⼀间房⼦被抢最后⼀间不抢; 要么最后⼀间房⼦被抢第⼀间不抢

在这里插入图片描述

那就简单了啊, 这三种情况, 那种的结果最⼤, 就是最终答案呗! 不过, 其实我们不需要⽐较三种情况, 只要⽐较情况⼆和情况三就⾏了, 因为这两种情况对于房⼦的选择余地⽐情况⼀⼤呀, 房⼦⾥的钱数都是⾮负数, 所以选择余地⼤, 最优决策结果肯定不会⼩

代码:

public int rob(int[] nums) {
	int n = nums.length;
	if (n == 1) return nums[0];
	return Math.max(robRange(nums, 0, n - 2),
					robRange(nums, 1, n - 1));
} 
	
// 仅计算闭区间 [start,end] 的最优结果
int robRange(int[] nums, int start, int end) {
	int n = nums.length;
	
	int dp_i_1 = 0, dp_i_2 = 0;
	
	int dp_i = 0;
	for (int i = end; i >= start; i--) {
		dp_i = Math.max(dp_i_1, nums[i] + dp_i_2);
		dp_i_2 = dp_i_1;
		dp_i_1 = dp_i;
	} 
	return dp_i;
}

C++代码:

class Solution {
public:
    int robRange(vector<int>& nums, int start, int end) 
    {
	    int n = nums.size();
	
	    int dp_i_1 = 0, dp_i_2 = 0;
	
	    int dp_i = 0;
	    for (int i = end; i >= start; i--) 
        {
		    dp_i = max(dp_i_1, nums[i] + dp_i_2);
		    dp_i_2 = dp_i_1;
		    dp_i_1 = dp_i;
	    } 
	    return dp_i;
    }

    int rob(vector<int>& nums) {
        int n = nums.size();
	    if (n == 1) 
            return nums[0];

        //[0][n - 2]区间代表不抢最后一间房子;[1][n - 1]:代表不抢第一间房子
	    return max(robRange(nums, 0, n - 2),
					    robRange(nums, 1, n - 1));
    }
};


三:House Robber III

在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。

计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。

示例 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.

你们说这个小偷是不是有点过分!!!竟然是传说中的⾼智商犯罪

整体的思路完全没变, 还是做抢或者不抢的选择, 去收益较⼤的选择。 甚⾄
我们可以直接按这个套路写出代码:

Map<TreeNode, Integer> memo = new HashMap<>();

public int rob(TreeNode root) {
	if (root == null) return 0;
	
	// 利⽤备忘录消除重叠⼦问题
	if (memo.containsKey(root))
		return memo.get(root);
		
	// 抢, 然后去下下家
	int do_it = root.val
				+ (root.left == null ?
				0 : rob(root.left.left) + rob(root.left.right))
				+ (root.right == null ?
				0 : rob(root.right.left) + rob(root.right.right));

	// 不抢, 然后去下家
	//
	int not_do = rob(root.left) + rob(root.right);
	int res = Math.max(do_it, not_do);
	memo.put(root, res);
	return res;
}

这道题就解决了, 时间复杂度 O ( N ) O(N) , N 为数的节点数。

还有更漂亮的解法。

int rob(TreeNode root) {
	int[] res = dp(root);
	return Math.max(res[0], res[1]);
} 

/* 返回⼀个⼤⼩为 2 的数组 arr
arr[0] 表⽰不抢 root 的话, 得到的最⼤钱数
arr[1] 表⽰抢 root 的话, 得到的最⼤钱数 */
int[] dp(TreeNode root) {
	if (root == null)
		return new int[]{0, 0};
		
	int[] left = dp(root.left);
	int[] right = dp(root.right);
	
	// 抢, 下家就不能抢了
	int rob = root.val + left[0] + right[0];
	
	// 不抢, 下家可抢可不抢, 取决于收益⼤⼩
	int not_rob = Math.max(left[0], left[1])
					+ Math.max(right[0], right[1]);
					
	return new int[]{not_rob, rob};
}

时间复杂度 O ( N ) O(N) , 空间复杂度只有递归函数堆栈所需的空间, 不需要备忘
录的额外空间

C++代码:

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
class Solution {
public:
    /* 返回⼀个⼤⼩为 2 的数组 arr
    arr[0] 表⽰不抢 root 的话, 得到的最⼤钱数
    arr[1] 表⽰抢 root 的话, 得到的最⼤钱数 */
    vector<int> dp(TreeNode* root) {
	    if (root == nullptr)
        {
            return vector<int>(2,0);
        }

	    vector<int> left = dp(root->left);
	    vector<int> right = dp(root->right);
	
	    // 抢, 下家就不能抢了
	    int rob = root->val + left[0] + right[0];
	
	    // 不抢, 下家可抢可不抢, 取决于收益⼤⼩,注意他是二叉树的结构,所以还要返回两种情况之和
	    int not_rob = max(left[0], left[1])
					    + max(right[0], right[1]);
						
        return vector<int>{not_rob,rob};
    }
    int rob(TreeNode* root) {
        vector<int> ret = dp(root);

	    return max(ret[0], ret[1]);
    }
};



发布了126 篇原创文章 · 获赞 57 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/wolfGuiDao/article/details/104629940