很多动态规划问题(dp)和深度优先搜索(dfs)的解题思路其实是类似的,都是通过递归求解寻找最优值。两者不同之处在于dp算法中增加了中间结果的存储,以空间换时间(这一点和回溯法很像)。dp算法的要点有以下两条:
- 找出问题的状态转移方程。
- 中间结果的记录。
以fibonacci数列第n个值的求解为例,最容易想到的方式就是利用其递归式(fib[n] = fib[n-1] + fib[n-2])进行计算,这也是该问题的状态转移方程(问题中某一状态与其他状态的函数关系),下面介绍基于递归式的两种实现方式。
- dfs实现:即代码中dfs()函数的实现,空间复杂度为O(1),但时间复杂度却是指数级的,其原因主要就是中间值的重复计算。
- dp实现:在dp()函数的实现中,我们额外申请了一块数组空间用来保存fibonacci数列的中间结果,用来保存中间值,避免重复计算。
在dp()函数实现中有几个注意点如下:
- 合理选择中间结果数组的初始值,保证该值不会出现在中间结果中,在这里,我们选择-1,因为fibonacci数列的值都是非负的。
- 在递归过程中保存中间结果值。
- 添加递归终止条件,不同于dfs,递归终止于fib[0]和fib[1],在dp中,我们需要额外判断当前索引的结果是否已被计算,判断依据就是中间结果数组中当前索引的值是否等于初始值,这样,就能避免中间值的重复计算了。
#include <iostream>
#include <cstring>
#define N 20
using namespace std;
int dfs(int index) {
if(index == 0 || index == 1) {
return index;
}
else {
return dfs(index-1) + dfs(index-2);
}
}
int dp(int* fib, int index) {
if(index >= N) { //out of the bound
return 0;
}
else if(index == 0 || index == 1) {
fib[index] = index; //fib[0] = 0; fib[1] = 1;
return fib[index];
}
else if(fib[index] != -1) {
return fib[index];
}
else {
fib[index] = dp(fib, index-1) + dp(fib, index-2);
return fib[index];
}
}
int main() {
int fib[N];
memset(fib, -1, N*sizeof(int));
cout << dfs(N-1) << endl;
cout << dp(fib, N-1);
return 0;
}
一部分简单的dp问题同时也可以利用递推来求解。有一种说法,dp的实现方式可以分为两种:递推和记忆化搜索,后者即上面我们提到的方法。记忆化搜索的优势在于我们只需要知道从之前相邻状态转移到某一状态的过程,就可以进行递归搜索;而对于递推而言,我们需要理清每一个状态如何获得,以及不同状态达到的先后顺序,需要更加清晰的思路,换来的优势则是更加快速的算法(记忆化搜索则需要许多额外的试探)。具体例子以后再说。