DP-打家劫舍


打家劫舍也是经典的动态规划问题,下面几道题都是力扣上的打家劫舍相关题目

House Robber I

198.打家劫舍

常规解法

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

337.打家劫舍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]; 就是代表着01的个数为02 的个数有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];
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值