House Robber

Leetcode整理之——House Robber系列

House Robber系列一共有三道题,分别是:

No.198 House Robber

No.213 House Robber II

No.337 House Robber III

这个系列是动态规划的代表作,难度层层递进,从简单的动态规划到与二叉树结合,非常具有特色,接下来我就对这三题的思路和解法进行讲解

No.198 House Robber

**题目:**给定一个数组,表示每个房子存放的金额,你不能抢相邻的房子,求能抢劫到的最大金额。

**思路:**这题具有非常明显的动态规划特征,当你来一个房子面前,你有:

两种状态:抢了上一个房子,没抢上一个房子

两种选择:抢这间房子,不抢这间房子

由于你不能抢相邻的两个房子,并且要抢最大金额,不可能出现连续两个房子都不抢的情况,因此只有两种组合,即:

组合A:你抢了上一个房子,当前的房子不抢

组合B:你上一个房子没抢,必须要抢当前的房子

那么我假设dp[i]表示你走到第i个房间的时候,做出选择后,能抢到的最大金额,那么:

对于组合A,有:dp[i-1];当前房间不能抢,那么到当前房间为止最大金额就是上一个房间抢完后的最大金额

对于组合B,有:dp[i-2]+price[i];如果要抢当前房间,那么就只能那上上个房间抢完后的金额加上当前房间的金额

状态转移方程为:dp[i]=max(dp[i-1],dp[i-2]+price[i])

代码:

//前面省略一些特殊边界,只展示核心代码
int[] dp=new int[nums.length];
dp[0]=nums[0];
dp[1]=Math.max(nums[0],nums[1]);
for(int i=2;i<nums.length;i++){
    dp[i]=Math.max(dp[i-1],dp[i-2]+nums[i]);
}
return dp[nums.length-1];

当然,我们可以用两个变量代替dp[i-1]和dp[i-2]

int beforeMax=0;
int currentMax=0;
for(int i=0;i<nums.length;i++){
    int t=currentMax;
    currentMax=Math.max(beforeMax+nums[i],currentMax);
    beforeMax=t;
}
return currentMax;

No.213 House Robber II

**题目:**题目相比上一题多了个条件,这些房子不是一排了,而是环状的,即第一个房子和最后一个房子是连着的。

**思路:**既然只多了一个条件,说明我们很可能还是可以使用第一题的解法,但是我们要找出环状带来的区别,区别就是:如果你抢了第一房子,那你最后一个房子就抢不了,反之亦然。那总的来看,就只有以下两种情况:

  1. 抢了第一个和倒数第二个
  2. 抢了第二个和倒数第一个

那只要看看这两种情况那个抢的多就行,那就是将一个数组拆成两个,做两道第一题就行了。

代码:

public int rob(int[] nums) {
    if(nums.length==1){
        return nums[0];
    }
    int result=0;
    //抢第一个到倒数第二个之间的房子
    int beforeMax=0;
    int currentMax=0;
    for(int i=0;i<nums.length-1;i++){
        int t=currentMax;
        currentMax=Math.max(beforeMax+nums[i],currentMax);
        beforeMax=t;
    }
    result=currentMax;
    //抢第二个到倒数第一个之间的房子
    beforeMax=0;
    currentMax=0;
    for(int i=1;i<nums.length;i++){
        int t=currentMax;
        currentMax=Math.max(beforeMax+nums[i],currentMax);
        beforeMax=t;
    }
    return Math.max(result,currentMax);
}

No.337 House Robber III

**题目:**现在房子的分布结构从数组变成了二叉树,要求变成了不能抢相连接的两个房子。

**思路:**我们细化一下什么叫不能抢相连接的房子,即:

  1. 抢了父结点,就不能抢任何一个子结点,但可以抢孙结点
  2. 抢了一个子结点,就不能抢它的父结点,但是可以抢另一个子结点

那么当我们遍历树的时候,对于每一个结点都有:

两个状态:当前结点的左孩子或右孩子被抢了,孙子没被抢;当前结点的左孩子和右孩子都没有被抢,孙子被抢了

两个选择:抢当前结点,不抢当前结点

那么组合就变成了:

组合A:当前结点的左孩子或右孩子被抢了,孙子没被抢,不抢当前结点

组合B:当前结点的左孩子和右孩子都没有被抢,孙子被抢了,抢当前结点

那么状态转移方程也就出来了:

组合A:left.value+right.value

组合B:current.value+left.left.value+left.right.value+right.left.value+right.right.value

当前结点能抢到的最大金钱maxValue=max(组合A,组合B)

代码:

public int rob(TreeNode root) {
    if(root==null){//空结点,抢到0块钱
        return 0;
    }
    //抢劫自己和四个孙子 vs 抢劫自己的两个儿子
    int money = root.val;
    if (root.left != null) {
        money += (rob(root.left.left) + rob(root.left.right));
    }
    if (root.right != null) {
        money += (rob(root.right.left) + rob(root.right.right));
    }
    return Math.max(money, rob(root.left) + rob(root.right));
}

我们发现,对于当前结点,既计算了儿子又计算了孙子,那儿子作为当前结点的时候,又要计算儿子和孙子,就会出现重复,那么我们就把每个计算过的当前结点和对应的最大金钱存到map中

private Map<TreeNode,Integer> map=new HashMap<>();
public int rob(TreeNode root) {
    if(root==null){//空结点,抢到0块钱
        return 0;
    }
    //如果已经计算过当前结点,则直接返回
    if(map.containsKey(root)){
        return map.get(root);
    }
    //抢劫自己和四个孙子 vs 抢劫自己的两个儿子
    int money = root.val;
    if (root.left != null) {
        money += (rob(root.left.left) + rob(root.left.right));
    }
    if (root.right != null) {
        money += (rob(root.right.left) + rob(root.right.right));
    }
    int maxValue=Math.max(money, rob(root.left) + rob(root.right));
    //记录
    map.put(root,maxValue);
    return maxValue;
}

有没有发现,之前的动态规划都只考虑一层关系就行,这次我们既考虑了子结点,又考虑了孙结点,是不是也存在重复的子问题了呢?没错,我们重新思考一下不考虑孙结点的思路:

对于当前结点,有:

两个状态:左孩子或右孩子被抢了,左孩子和右孩子都没有被抢

两个选择:抢自己,不抢自己

两种组合:

组合A:左孩子或右孩子被抢了,不抢自己

组合B:左孩子和右孩子都没有被抢,抢自己

状态方程变成了:

组合A:left.maxValue+right.maxValue

组合B:current.value

这样对吗?

显然是有问题的,因为对于组合B,我们抢了自己,不代表自己的孙子不能被抢,因此我们不能只用一个maxValue的维度来表示到当前结点抢到的最大金钱,而应该把有没有抢自己这个维度也加进去,即:

maxValue=new int[2]

maxValue[0]表示没有抢自己的情况下,抢到的最大金钱;maxValue[1]表示抢自己的情况下的最大金钱

那么状态转移方程就是:

current.maxValue[0]=max(current.left.maxValue[0],current.left.maxValue[1])+max(current.left.maxValue[0],current.left.maxValue[1])

current.maxValue[1]=current.left.maxValue[0]+current.left.maxValue[0]+current.val

代码:

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

public int[] dp(TreeNode cur){
    if(cur==null){
        return new int[]{0,0};
    }
    int[] maxValue=new int[2];
    int[] leftMaxValue=dp(cur.left);
    int[] rightMaxValue=dp(cur.right);
    maxValue[0]=Math.max(leftMaxValue[0],leftMaxValue[1])+Math.max(rightMaxValue[0],rightMaxValue[1]);
    maxValue[1]=leftMaxValue[0]+rightMaxValue[0]+cur.val;
    return maxValue;
}

你可能感兴趣的:(LeetCode)