不怕小偷有文化,就怕小偷会规划。
今天来看一道一维动态规划的例题:打家劫舍
首先来尝试一下利用递归法求解
先来考虑一下是否可以用递归法求解
那么我们需要考虑这几个条件是否满足:
1.是否可以分解为子问题
2.是否满足最优子结构
3.是否存在递归基
是否可以分解为子问题
假设我们现在偷的房子集为[2,7,9,3,1],我们可以假设小偷是从左偷到右的,而当我们偷到最右边也就是财富值为1的房子时,就代表所有的房子全部偷完了。而当我们偷到最后一个房子的时候,我们可以产生两个选择:偷或者不偷。显然,无论是我们做了哪一个选择,该问题都能被分解。
偷,则分解为偷[2, 7, 9]和1
不偷,则分解为偷[2,7,9,3]
是否满足最优子结构
何谓最优子结构?
即由子问题的最优解可以得出该问题的最优解。
显然,无论我们分解成何种情况,最大的金额总是能由子问题的最大金额得出,故满足最优子结构。
是否存在递归基
考虑最基本的情况。
1.只有一个房子时:最大金额即为这个房子的财富值
2.当有两个房子的时候,来到最后一个房子也就是第二个房子,首先考虑是否可以分解为上面两种情况。显然,当选择偷的时候,由于该房子前面只有一个房子,故不能分解为上面两种分解情况中的第一种情况。所以有两个房子的情况也为基本的情况,而最大金额则为两个房子财富值中的最大值。
3.当有三个房子的时候,显然可以分为:偷第三个房子——偷第一个房子和第三个房子,不偷第三个房子——只偷到第二个房子,放弃第三个房子。
由此得出两种基本情况:只有一个或者两个房子。
这便是我们的递归基。
递归实现
既然以上三个条件均满足,那么该题可用递归法解。
考虑到题目函数的参数的特殊性,如果不借助辅助函数,递归法可以用Python来写:
def rob(self, nums):
return max(nums) if len(nums) < 3 else max(nums[-1] + self.rob(nums[0 : -2]), self.rob(nums[0 : -1]))
而借助辅助函数(表示当前偷到哪一间房为止),则也可用c++写:
int rob(vector<int>& nums) {
return helper(nums, nums.size());
}
int helper(vector<int>& nums, int n){
return n == 1 ? nums[0] : n == 2 ? max(nums[0], nums[1]) : max(nums[n - 1] + helper(nums, n - 2), helper(nums, n - 1));
}
当然,这种方法肯定会超时,我们可以改为带备忘录的递归。
int rob(vector<int>& nums) {
return robCur(nums, nums.size());
}
int robCur(vector<int>& nums, int n) { //当前偷到哪一间为止
vector<int> m(nums.size() + 1, -1);
return helper(nums, m, n);
}
int helper(vector<int>& nums, vector<int>& m, int n) {
return m[n] != -1 ? m[n] : n == 1 ? m[1] = nums[0] : n == 2 ? m[2] = max(nums[0], nums[1]) : m[n] = max(nums[n - 1] + helper(nums, m, n - 2), helper(nums, m, n - 1));
}
动态规划实现
纯递归法之所以超时,是因为存在大量重复子问题,那么我们就可以用动态规划来求解。
首先确认状态和状态转移方程:
状态:dp[n]为偷到第n间房子为止。
状态转移方程:dp[n] = max(nums[n - 1] + dp[n - 2], dp[n - 1])
具体实现如下:
int rob(vector<int>& nums) {
if(nums.empty()) return 0;
int n = nums.size();
vector<int> dp(n + 1, 0);
dp[1] = nums[0]; //原本是需要将dp[2]赋初值的,这里取巧,因为dp[0]的存在就不用赋初值了
for(int i = 2; i <= n; ++i){
dp[i] = max(dp[i - 1], dp[i - 2] + nums[i - 1]);
}
return dp[n];
}
当然也可以进一步优化空间:
int rob(vector<int>& nums) {
if(nums.empty()) return 0;
int n = nums.size();
if(n == 1) return nums[0];
int pre1 = 0, pre2 = 0, cur;
for(int i = 1; i <= n; ++i){
cur = max(pre1, pre2 + nums[i - 1]);
pre2 = pre1;
pre1 = cur;
}
return cur;
}