House Robber系列一共有三道题,分别是:
No.198 House Robber
No.213 House Robber II
No.337 House Robber III
这个系列是动态规划的代表作,难度层层递进,从简单的动态规划到与二叉树结合,非常具有特色,接下来我就对这三题的思路和解法进行讲解
**题目:**给定一个数组,表示每个房子存放的金额,你不能抢相邻的房子,求能抢劫到的最大金额。
**思路:**这题具有非常明显的动态规划特征,当你来一个房子面前,你有:
两种状态:抢了上一个房子,没抢上一个房子
两种选择:抢这间房子,不抢这间房子
由于你不能抢相邻的两个房子,并且要抢最大金额,不可能出现连续两个房子都不抢的情况,因此只有两种组合,即:
组合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;
**题目:**题目相比上一题多了个条件,这些房子不是一排了,而是环状的,即第一个房子和最后一个房子是连着的。
**思路:**既然只多了一个条件,说明我们很可能还是可以使用第一题的解法,但是我们要找出环状带来的区别,区别就是:如果你抢了第一房子,那你最后一个房子就抢不了,反之亦然。那总的来看,就只有以下两种情况:
那只要看看这两种情况那个抢的多就行,那就是将一个数组拆成两个,做两道第一题就行了。
代码:
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);
}
**题目:**现在房子的分布结构从数组变成了二叉树,要求变成了不能抢相连接的两个房子。
**思路:**我们细化一下什么叫不能抢相连接的房子,即:
那么当我们遍历树的时候,对于每一个结点都有:
两个状态:当前结点的左孩子或右孩子被抢了,孙子没被抢;当前结点的左孩子和右孩子都没有被抢,孙子被抢了
两个选择:抢当前结点,不抢当前结点
那么组合就变成了:
组合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;
}