https://leetcode.com/problems/house-robber/
You are a professional robber planning to rob houses along a street. Each house has a certain amount of money stashed, the only constraint stopping you from robbing each of them is that adjacent houses have security system connected andit will automatically contact the police if two adjacent houses were broken into on the same night.
Given a list of non-negative integers representing the amount of money of each house, determine the maximum amount of money you can rob tonightwithout alerting the police.
public int rob(int[] num)
在分析这一题之前,我想先介绍另一题,这一题是来自于著名算法入门书籍CLRS。
如果在一个工厂里,我有两个工作流水线A和B。两条工作流水线出产的零件数目每天都不一样,而且相互之间也不一样(也可能在某一天是一样的)。你每一天只能选择A或者B其中一个工作流水线进行生产活动。那么,我现在给定你N天里A和B的生产效率,让你给出最后最多你可以生产多少个零件。API大概长成这个样子
public int maxProduct(int[] A, int[] B)
follow up : 给出得到上一题结果的生产方案,也就是第几天你选的是A还是B,我得知道。
我觉得在大部分非ACM选手心目中,DP的入门题是fibonacci。然后小部分非ACM选手心目中,DP的入门题是杨辉三角。在ACM选手心目中,DP的入门题应该是最长递增子序列或者最长共同子序列(我猜...)。而上面那一题,是CLRS关于DP的入门题。
在我刚接触DP的时候,我觉得这一题是挺难的。当然,目前我还是觉得所有DP题都挺难的。。。毕竟智商有限
这一题的递归式也有点特殊。往常我们做到的DP的递归式都只有一个f(i),这一题勉强来讲意义上有两个。
它们长成这样 f(i) = max(f(i - 1), g(i - 1)) + A[i]. g(i) = max(f(i - 1), g(i - 1)) + B[i]
你看,长得都基本一样,只是f(i)表示的是我在第i天如果选了A的话,最大结果会是多少,g(i)表示的是我在第i天如果选了B的话,最大结果会是多少。这一题的解就是从最后f(i)和g(i)中选较大的那个即可。
至于follow up,如果你不是为了省空间弄成了constant space的话,倒退着解就可以了。
其实很强烈推荐CLRS这本书,一般我们做DP题都只追求结果,但是CLRS的所有解题中都包含了路径的推导。这是很不容易的。
回到这一题上,这题的本质就是不能选连续两个相邻的元素,然后求这个数组的最大子序列的和事实上这一题的灵感就是源自于上一题。在第i个点上,都是存在两个状态,而这两个状态的最优解,都是跟i - 1点的最优子解以及当前的二择直接相关。所以不难得出推导式如下:
f(i) = g(i - 1) + num[i]
g(i) = max(g(i - 1), f(i - 1))
f(i)表示的是我在第i个节点上选择拿的最优解。g(i)表示的是第i个节点上选择不拿的最优解。
所以f(i)能够做的最优的方式就是从之前不拿的那里加上我当前能拿的。
g(i)能够做的就是从上一节点拿和不拿的选最大的。
然后从f(i)和g(i)中选一个更大的就是解。根据算法,给出代码如下:
public int rob(int[] num) {
if(num.length == 0)
return 0;
int[] curYes = new int[num.length];
int[] curNo = new int[num.length];
curYes[0] = num[0];
for(int i = 1; i < num.length; i++){
curYes[i] = curNo[i - 1] + num[i];
curNo[i] = Math.max(curNo[i - 1], curYes[i - 1]);
}
return Math.max(curYes[num.length - 1], curNo[num.length - 1]);
}
因为事实上我们只需要保存当前和上一个状态,所以我们可以在空间上优化一下,给出代码如下:
public int rob(int[] num) {
if(num.length == 0)
return 0;
int curYes = 0, curNo = 0, prevYes = num[0], prevNo = 0;
for(int i = 1; i < num.length; i++){
curYes = prevNo + num[i];
curNo = Math.max(prevYes, prevNo);
prevYes = curYes;
prevNo = curNo;
}
return Math.max(prevYes, prevNo);
}