int F(int n){
if(n == 0 || n == 1) return 1;
else return F(n-1) + F(n-2);
}
这个递归会涉及到很多重复的计算,如当n==5时,可以得到F(5)= F(4)+F(3),接下来计算F(4)时又会有F(4)= F(3)+F(2),这时不采取措施,F(3)将会被计算两次。如果n很大,重复计算的次数将难以想象。
实际上由于没有保存中间计算的结果,实际复杂度将会高达O(2n),即每次都会计算F(n-1)和F(n-2)这两个分支,基本上不能承受n较大的情况。
开一个数组dp,用来保存已经计算过的结果,其中dp[n]记录F(n)的结果,并用dp[n] = -1来表示F(n) 当前还没有被计算过。
int dp[MAXN];
然后就可以在递归中判断dp[n]是否是-1,如果不是-1,说明已经计算过F(n),直接返回dp[n]就是结果;否则,按照递归式进行递归。
int F(int n){
if(n == 0 || n == 1) return 1;
if(dp[n] != -1) return dp[n];
else{
dp[n] = F(n-1) + F(n-2);
return dp[n];
}
}
通过记忆化搜素,把复杂度从O(2n)降到O(n)。
斐波那契数列递归图:
斐波那契数列记忆化搜索示意图:
如计算F(45),直接使用递归用时9.082809,用记忆化搜索仅仅0.000001
计算代码:
#include
#include
#include
#include
using std::fill;
const int MAXN = 1000;
int dp[MAXN];
int F2(int n){
if(n == 0 || n == 1) return 1;
else return F2(n-1) + F2(n-2);
}
int F(int n){
if(n == 0 || n == 1) return 1;
if(dp[n] != -1) return dp[n];
else{
dp[n] = F(n-1) + F(n-2);
return dp[n];
}
}
int main(){
int n = 45;
int ans;
fill(dp, dp + MAXN, -1);
clock_t start,end;
srand((unsigned)time(NULL));//随机数种子
start = clock();//开始记时
ans = F2(n);
end = clock();
printf("%d\n", ans);
printf("Time is %f\n", double(end - start) / CLOCKS_PER_SEC);//输出运行时间
srand((unsigned)time(NULL));//随机数种子
start = clock();//开始记时
ans = F(n);
end = clock();
printf("%d\n", ans);
printf("Time2 is %f\n", double(end - start) / CLOCKS_PER_SEC);//输出运行时间
return 0;
}
以数塔问题为例:
将一些数字排成数塔形状,其中第一层有一个数字,第二层有两个数字,。。。第n层有n个数字。现在要从第一层走到第n层,每次只能走向下一层连接的两个数字中的一个。问:最后将路径上所有数字相加后得到的和最大是多少?
Sample Input:
5 //5层数塔,下面有5行
5
8 3
12 7 16
4 10 11 6
9 5 3 9 4
Sample Output:
44
其实可以把从7出发到达最低层的所有路径能产生的最大和记录下来,这样再次访问7就能直接获取这个最大值,避免重复计算。
如果要求出“从位置(1,1)到达最底层的最大和”dp[1][1],那么一定要求出它的两个子问题,“从位置(2,1)到达最底层的最大和”d[2][1]和"从位置(2,2)到达最底层的最大和"d[2][2],即进行一次决策,
走数字5左下,还是右下。于是dp[1][1]就是dp[2][1]和dp[2][2]的较大值加上5,写成式子就是:
dp[1][1] = max(dp[2][1], d[2][2])+f[1][1]
令dp[i][j]表示从第i行第j个数字出发的到达最低层的所有路径上所能得到的最大和
由此得到要求dp[i][j],
dp[i][j] = max(dp[i+1][j], dp[i + 1][j +1])+f[i][j]
把dp[i][j]称为状态,上面的式子称为状态转移方程,它把状态dp[i][j]转为dp[i+1][j]和dp[i + 1][j +1]。
可以发现dp[i][j]只与i+1层的状态有关,与其他层无关。
边界(直接确定其结果):数组dp最后一层dp总是等于元素自身
动态规划的递推写法,总是从这些边界出发,通过状态转移方程扩散到整个dp数组。
#include
#include
using std::max;
const int MAXN = 1000;
int f[MAXN][MAXN], dp[MAXN][MAXN];
int main(int argc, char const *argv[])
{
int n;
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
{
for (int j = 1; j <= i; ++j)
{
scanf("%d", &f[i][j]);
}
}
//边界
for (int i = 1; i <= n; ++i)
{
dp[n][i] = f[n][i];
}
//从n-1层不断向上计算dp[i][j]
for (int i = n - 1; i >= 1; --i)
{
for (int j = 1; j <= i; ++j)
{
//状态转移方程
dp[i][j] = max(dp[i + 1][j], dp[i + 1][j + 1]) + f[i][j];
}
}
printf("%d\n", dp[1][1]);
return 0;
}
问题拥有最优子结构:一个问题的最优解可由子问题的最优解有效构造出来。
最优子结构保证了动态规划中原问题的最优解可以由子问题的最优解推导而来。
因此一个问题必须拥有最优子结构,才能用动态规划解决。
一个问题必须拥有重叠子问题,才能使用动态规划求解。
重叠子问题:一个问题可以被分为若干个子问题,且这些子问题会重复出现。
一个问题必须拥有重叠子问题和最优子结构,才能用动态规划来解决。
共同点:都是将问题分解成子问题,然后合并子问题的解得到原问题。
共同点:都要求问题拥有最优子结构。