从小偷问题来考虑动态规划的一般流程:
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例1:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。偷窃到的最高金额 = 1 + 3 = 4 。
示例2:
输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。偷窃到的最高金额 = 2 + 9 + 1 = 12 。
解法如下:
任何数学递推公式都可以直接翻译成递归法,但是基本现实是编译器不能正确对待递归算法,产生低效的程序。这时,需要给编译器提供一些帮助,将递归算法重新写成非递归算法,让后者把那些子问题的答案都写到一个表(数组)内,这一技巧成为动态规划。
能采用动态规划求解的问题的一般要具有3个性质:
(1)最优化原理:假设问题的最优解所包括的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。DP状态的最优值是由更小规模的DP状态的最优值推出
(2)无后效性:即某阶段状态一旦确定。就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响曾经的状态。仅仅与当前状态有关;无论DP状态如何得到,都不会影响后续DP状态的取值
(3)有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到(该性质并非动态规划适用的必要条件,可是假设没有这条性质。动态规划算法同其它算法相比就不具备优势)。
动态规划的的四个解题步骤一般是:
- 定义子问题
- 写出子问题的递推关系
- 确定 DP 数组的计算顺序
- 空间优化
1,定义子问题
子问题是和原问题相似,但规模较小的问题。例如这道小偷问题,原问题是“从全部房子中能偷到的最大金额”,将问题的规模缩小,子问题就是“从 k个房子中能偷到的最大金额”,用 f(k)表示。
子问题需要有两个性质:
- 原问题要能由子问题表示。例如这道小偷问题中,k=n 时实际上就是原问题。否则,解了半天子问题还是解不出原问题,那子问题岂不是白解了。
- 一个子问题的解要能通过其他子问题的解求出。例如这道小偷问题中,f(k)可以由 f(k-1)和 f(k-2) 求出。这个性质就是教科书中所说的“最优子结构”。如果定义不出这样的子问题,那么这道题实际上没法用动态规划解。
2,写出子问题的递推关系
所谓递推关系,就是状态转移方程,是动态规划最为关键的一步。实际上,这个步骤是上述子问题第二个性质的延伸,即将f(k)由 f(k-1)和 f(k-2) 表达。对于本问题,递推关系很容易发现。对于第k间,我们有偷或不偷两种选择,我们不能连续两间偷现金,对应两种方案计算总盗窃金额,求其最大值即可。
为第k间偷窃的金额,写出递推关系后,我们还需考虑边界条件:
3,确定DP数组的计算顺序
在确定了子问题的递推关系之后,下一步就是依次计算出这些子问题了。动态规划有两种计算顺序,一种是自顶向下的、使用备忘录的递归方法,一种是自底向上的、使用 dp 数组的循环方法。一般采用自底向上的方法
我们的dp数组的每一项值就代表要求的每一个,即
。
我们已经知道,也就是说到第k间的总金额与k-1,k-2间有关,所以计算顺序应该从左到右。这也可以保证求解一个子问题时,子问题之前的子问题已经被求解。
class Solution {
public:
int rob(vector<int>& nums) {
if (nums.size()==0) {
return 0;
}
if (nums.size() == 1) {
return nums[0];
}
vector<int> dp = vector<int>(nums.size(), 0);
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);
for (int i = 2; i < nums.size(); i++) {
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);
}
return dp[nums.size() - 1];
}
};
4,空间优化
空间优化即状态压缩的基本原理是,很多时候我们并不需要始终持有全部的 DP 数组。对于小偷问题,我们发现,最后一步计算 f(n)的时候,实际上只用到了 f(n-1)和 f(n-2)的结果。n-2之前的子问题,实际上早就已经用不到了。那么,我们可以只用两个变量保存两个子问题的结果,就可以依次计算出所有的子问题。
这样可以将空间复杂度从O(n)降到O(1)
class Solution {
public:
int rob(vector<int>& nums) {
if (nums.size()==0) {
return 0;
}
if (nums.size() == 1) {
return nums[0];
}
int first=nums[0];
int second=max(nums[0],nums[1]);
for(int i=2;i<nums.size();i++){
int temp=second;
second=max(nums[i]+first,temp);
first=temp;
}
return second;
}
};
动态规划本质上就是递归算法加上记忆功能