打家劫舍也是经典的动态规划问题,下面几道题都是力扣上的打家劫舍相关题目
House Robber I
常规解法
1、唯一的状态:有多少间房子
所以定义dp[i]=ans:抢到第i间房子时,能抢到的最大的金额。
2、两种选择:抢或者不抢。
-
如果你抢了这间房子,那么你肯定没有抢上一间房子,只能从上上间房子的状态转移过来。
-
如果你不抢这件房子,只能从上间房子的状态转移过来。
所以状态转移方程为:dp[i]=Math.max(dp[i-1],dp[i-2]+nums[i-1]);
3、base case:
因为状态转移方程中有i-2,那么i必须从2开始遍历,就必须为dp[0]和dp[1]的初始化
dp[0]=nums[0]; 只有一间房子,肯定抢
dp[1]= nums[0]>nums[1]?nums[0]:nums[1]; 有两间,抢最大的
class Solution {
public int rob(int[] nums) {
int n=nums.length;
if(n==1){
return nums[0];
}
int[]dp=new int[n];
dp[0]=nums[0];
dp[1]=Math.max(nums[0],nums[1]);
for (int i=2;i<n;i++){
dp[i]=Math.max(dp[i-2]+nums[i],dp[i-1]);
}
return dp[n-1];
}
}
C++:
class Solution {
public:
int rob(vector<int>& nums) {
int n=nums.size();
if(n==1){
return nums[0];
}
vector<int>dp(n,0);
dp[0]=nums[0];
dp[1]=max(nums[0],nums[1]);
for(int i=2;i<n;i++){
dp[i]=max(dp[i-1],dp[i-2]+nums[i]);
}
return dp[n-1];
}
};
状态压缩
我们又发现状态转移只和 dp[i]
最近的两个状态有关,所以可以进一步优化,将空间复杂度降低到 O(1)。
class Solution {
public int rob(int[] nums) {
int n=nums.length;
if(n==1){
return nums[0];
}
if(n==2){
return nums[0]>nums[1]?nums[0]:nums[1];
}
int x= nums[0];
int y= nums[0]>nums[1]?nums[0]:nums[1];
for (int i = 2; i < n; i++) {
int z=Math.max(y,x+nums[i]);
x=y;
y=z;
}
return y;
}
}
House Robber II
213.打家劫舍II
首先,首尾房间不能同时被抢,那么只可能有三种不同情况:要么都不被抢;要么第一间房子被抢最后一间不抢;要么最后一间房子被抢第一间不抢。
只要比较情况二和三
就行了,因为这两种情况对于房子的选择余地比情况一大,房子里的钱数都是非负数,所以选择余地大,最优决策结果肯定更大。
所以只需分别计算这两种情况取最大即可!
class Solution {
public int rob(int[] nums) {
int n=nums.length;
//因为执行f的nums长度至少为2,于是n至少为3,所以1,2都需特判
if(n==1){
return nums[0];
}
if(n==2){
return Math.max(nums[0],nums[1]);
}
return Math.max( f(nums,0,n-2), f(nums,1,n-1));
}
private int f(int[] nums, int st, int ed) {
int[]dp=new int[nums.length-1];
dp[0]=nums[st];
dp[1]=Math.max(nums[st],nums[st+1]);
//dp和nums的下标不能用同一个了,因为nums的下标取值有两种情况,如果混用会导致dp数组下标越界
for (int i = st+2,j=2; i <=ed; i++,j++) {
dp[j]=Math.max(dp[j-2]+nums[i],dp[j-1]);
}
return dp[nums.length-2];
}
}
状态压缩
class Solution {
public int rob(int[] nums) {
int n=nums.length;
//因为执行f的nums长度至少为2,于是n至少为3,所以1,2都需特判
if(n==1){
return nums[0];
}
if(n==2){
return Math.max(nums[0],nums[1]);
}
return Math.max(f(nums,0,n-2),f(nums,1,n-1));
}
//状态压缩的下标处理要简单一些
private int f(int[] nums, int start, int end) {
int x= nums[start];
int y= Math.max(nums[start],nums[start+1]);
int z=0;
for (int i = start+2; i <= end; i++) {
z=Math.max(y,x+nums[i]);
x=y;
y=z;
}
return y;
}
}
House Robber III
整体的思路完全没变,对于当前节点,还是做抢或者不抢的选择:
如果我打劫了当前节点,那么我接下来打劫谁,把所有打劫到的钱加起来x
如果我不打劫当前节点,那么我接下来打劫谁,把所有打劫到的钱加起来y
选择x和y中较大者即可!!
另外、需要记忆化剪枝,否则超时!
class Solution {
//key:节点值 val:以该节点为root能打劫到的最大价值
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;
}
}
N叉树
上题是二叉树,如果扩展成N叉树呢,原理不变的、
import java.util.HashMap;
import java.util.Map;
public class Solution {
Map<TreeNode,Integer> memo=new HashMap<>();
public int solution(TreeNode root){
if(root==null){
return 0;
}
if(memo.containsKey(root)){
return memo.get(root);
}
int do_it=root.val;
for (TreeNode child : root.child) {
if(child!=null){
for (TreeNode node : child.child) {
do_it+=solution(node);
}
}
}
int not_do=0;
for (TreeNode child : root.child) {
not_do+=solution(child);
}
int max=Math.max(do_it,not_do);
memo.put(root,max);
return max;
}
}
二叉树染色
添加链接描述
同样是树形dp
状态:所有节点
选择:每个节点是否染色
状态转移:当前节点为根的子树的最大染色节点值的和= Math.max(do_it,not_do);
一般都需要剪枝,但是本题不行,因为多了一个可以染色的最大个数k,每次k不同,所以子树对应的最大值也不同。当然可以将节点和k封装在一起作为key,来判断是否是之前计算过的。
class Solution {
int cnt;
public int maxValue(TreeNode root, int k) {
cnt=k;
return dfs(root,k);
}
private int dfs(TreeNode root, int k) {
if(root==null){
return 0;
}
//root染色
int do_it=0;
if (k>0){
for (int i=0;i<k;i++){
int left=dfs(root.left,i); //左节点最多可以染i
int right=dfs(root.right,k-i-1);//则右节点最多可以染k-i-1
int cur_sum=left+right;
do_it=Math.max(do_it,cur_sum);
}
do_it+=root.val;
}
//root不染色,左右子树可以染cnt个
int not_do=0;
int left=dfs(root.left,cnt);
int right=dfs(root.right,cnt);
not_do+=left+right;
int ans= Math.max(do_it,not_do);
map.put(root,ans);
return ans;
}
}
删除并获得点数
打家劫舍解法
题目意思可以理解为:选择了x,就不能选择x-1和x+1
。这和打家劫舍是一个意思。
那么怎么转换为打家劫舍题型呢?通过分析很容易想到:
1、应该将每个值出现的次数统计起来便于计算,即为cnt。
2、最重要的是要将相同元素放在一起,这样才能转换为打家劫舍。
那么怎么将元素值x和x出现次数cnt很好的联系在一起呢?毫无疑问是hash。
开一个all数组,用x作为下标,all[x]=cnt。
举例:
nums = [2, 2, 3, 3, 3, 4]
构造all:
all=[0, 0, 2, 3, 1]; 就是代表着0和1的个数为0, 2 的个数有2个,3 的个数有 3 个,4 的个数有 1 个。
dp[i]:遍历到i时的最大获得点数
dp[i] = Math.max(dp[i - 1], dp[i - 2] + i * all[i]);
max( 不选i, 选i )
class Solution {
public int deleteAndEarn(int[] nums) {
//计算nums数组的最大值
int max = Integer.MIN_VALUE;
for (int num : nums) {
max = Math.max(max, num);
}
int[] all = new int[max + 1];
for (int item : nums) { //记录nums中的每个值的出现次数
all[item] ++;
}
int[] dp = new int[max + 1];
dp[0] = 0;
dp[1] = all[1];
for (int i = 2; i <= max; i++) {
dp[i] = Math.max(dp[i - 1], dp[i - 2] + all[i] * i);
}
return dp[max];
}
}