代码随想录算法训练营Day44|动态规划part07

65 阅读5分钟

LeetCode 198 打家劫舍

题目链接:leetcode.cn/problems/ho…

文档讲解:programmercarl.com/0198.打家劫舍.h…

视频讲解:www.bilibili.com/video/BV1Te…

思路

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

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

考虑一个房屋偷不偷会基于之前的状态,所以存在递推关系。打家劫舍也是动态规划解决的经典问题。

考虑动态规划五部曲:

  1. dp数组和下标含义:dp[i]表示走到第i间房,偷到的最大总金额(含第i间)。
  2. 确定递推公式:
    • 如果第i间偷,第i-1间肯定不能偷,所以dp[i-1]和dp[i-2]是一样的。dp[i] = dp[i-2] + nums[i]
    • 如果第i间不偷,dp[i] = dp[i-1]
    • 所以dp[i] = max(dp[i-1], dp[i-2] + nums[i])
  3. dp数组如何初始化:从递推公式看出,dp数组的基础是dp[0]和dp[1]。结合数组的含义,dp[0]只考虑第0间房,所以等于nums[0]。dp[1]只能偷一间房,所以是max(nums[0], nums[1])
  4. 确定遍历顺序:根据递推公式,从小到大遍历
  5. 举例推导dp数组

解法

class Solution {
	public int rob(int[] nums) {	
		int[] dp = new int[nums.length];		
		if (nums.length == 1) {		
			return nums[0];		
		}		
		dp[0] = nums[0];		
		dp[1] = Math.max(nums[0], nums[1]);		
		for (int i = 2; i < dp.length; i++) {		
			dp[i] = Math.max(dp[i-1], dp[i-2] + nums[i]);		
		}		
		return dp[dp.length-1];	
	}
}

LeetCode 213 打家劫舍II

题目链接:leetcode.cn/problems/ho…

文档讲解:programmercarl.com/0213.打家劫舍II…

视频讲解:www.bilibili.com/video/BV1oM…

思路

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

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

当房屋成环,第一个和最后一个房屋最多只能偷一个。所以要么只考虑第一个,要么只考虑最后一个。我们要找到收益最大的情况,所以可以遍历nums[0~n-2]nums[1~n-1]。得到两种考虑下的最大金额,取更大的一个。其余思路和上一题相同,不再赘述。

解法

class Solution {
	public int rob(int[] nums) {	
		int[] dp = new int[nums.length-1];		
		if (nums.length == 1) {		
			return nums[0];		
		}		
		if (nums.length == 2) {		
			return Math.max(nums[0], nums[1]);		
		}		
		// 考虑第一个不考虑最后一个		
		dp[0] = nums[0];		
		dp[1] = Math.max(nums[0], nums[1]);		
		for (int i = 2; i < dp.length; i++) {		
			dp[i] = Math.max(dp[i-1], dp[i-2]+nums[i]);		
		}		
		int result = dp[dp.length-1];	
			
		// 不考虑第一个考虑最后一个		
		dp[0] = nums[1];		
		dp[1] = Math.max(nums[1], nums[2]);		
		for (int i = 2; i < dp.length; i++) {		
			dp[i] = Math.max(dp[i-1], dp[i-2]+nums[i+1]);		
		}		
		result = Math.max(result, dp[dp.length-1]);		
		return result;	
	}
}

LeetCode 337 打家劫舍III

题目链接:leetcode.cn/problems/ho…

文档讲解:programmercarl.com/0337.打家劫舍II…

视频讲解:www.bilibili.com/video/BV1H2…

思路

小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。

除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。

给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。

两个直接相连的房子不能同时偷,也就是父子节点不能同时被偷。分开考虑二叉树的所有路径,每个路径的情况和基础版打家劫舍相同。

考虑动态规划五部曲:

  1. dp数组和下标含义:由于输入是一棵树,dp数组也应是一棵树。dp树的结构与输入完全相同,每个节点的含义是从以此节点为根节点的树中能偷到的最大金额。
  2. 确定递推公式:
    • 如果偷root,root的左右子节点都不能偷,左右子节点的收益来自他们的子节点。此处假设node为root在输入树中对应的节点。root.val = root.left.left.val + root.left.right.val + root.right.left.val + root.right.right.val + node.val
    • 如果不偷root,root的总收益是左右子树收益之和。root.val = root.left.val + root.right.val
    • 所以root.val应该取上述两个值的最大值
  3. dp数组如何初始化:根据递推公式可知,节点的值基于其子节点。所以如果节点为叶子节点,就初始化为对应输入树节点的值node.val。如果节点只有一层子节点,node.val = max(node.left.val+node.right.val, node.val)
  4. 确定遍历顺序:从树的叶子开始遍历,所以是后序遍历。
  5. 举例推导dp数组

解法

class Solution {
	public int rob(TreeNode root) {	
		// copy		
		TreeNode dpRoot = copy(root);		
		// 递推		
		traverse(dpRoot, root);		
		return dpRoot.val;	
	}		  
	
	private TreeNode copy(TreeNode root) {	
		if (root == null) {		
			return null;		
		}
		
		TreeNode dpNode = new TreeNode();		
		dpNode.left = copy(root.left);		
		dpNode.right = copy(root.right);		
		return dpNode;	
	}		  
	
	/**	
	* 遍历dp数组	
	* @param root dp数组的指针	
	* @param node 输入树的指针	
	*/	
	private void traverse(TreeNode root, TreeNode node) {	
		if (root == null) {		
			return ;		
		}		
		traverse(root.left, node.left);		
		traverse(root.right, node.right);
		
		// 情况0: 初始化dp节点的值		
		if (root.left == null && root.right == null) {		
			root.val = node.val;			
			return ;		
		}		
		else if (root.left == null && root.right != null) {		
			if (root.right.left == null && root.right.right == null) {	
				root.val = Math.max(node.val, node.right.val);			
				return ;			
			}		
		}		
		else if (root.left != null && root.right == null) {		
			if (root.left.left == null && root.left.right == null) {				
				root.val = Math.max(node.val, node.left.val);				
				return ;			
			}			
		}		
		else {		
			if (root.left.left == null && root.left.right == null && root.right.left == null && root.right.right == null) {			
				root.val = Math.max(node.val, node.left.val + node.right.val);				
				return ;			
			}		
		}
		
		// 情况1:偷root		
		int temp1 = 0; // 累计孙子节点的和		
		if (root.left != null) {		
			if (root.left.left != null) {			
				temp1 += root.left.left.val;			
			}			
			if (root.left.right != null) {			
				temp1 += root.left.right.val;			
			}		
		}		
		if (root.right != null) {		
			if (root.right.left != null) {			
				temp1 += root.right.left.val;				
			}			
			if (root.right.right != null) {			
				temp1 += root.right.right.val;			
			}		
		}		
		temp1 += node.val;
		
		// 情况2:不偷root		
		int temp2 = 0;		
		if (root.left != null) {		
			temp2 += root.left.val;		
		}		
		if (root.right != null) {		
			temp2 += root.right.val;		
		}		
		root.val = Math.max(temp1, temp2);	
	}
}

今日收获总结

今日学习3小时,打家劫舍的整体递推思路都是类似的。但是针对不同的情景需要处理的细节不同。环形房屋需要处理首尾关系,树形房屋要使用同样的数据结构作为dp数组。