用一个故事教会我动态规划:(转载)查看这里
实例一:Fibonacci序列
F(n) = F(n-1) + F(n-2),F(0) = 1;f(1) = 1;
- 递归算法:
int fuc(int n){
if(n==0 || n==1 ) return 1;
else return fuc(n-1) + fuc(n-2);
}
该简单的递归算法具有指数级的算法复杂度;因为在递归运算中存在重复运算的问题,比如计算F(5),先求出F(3)+F(4),然后要求出F(2)+F(1)和F(3)+F(2),然后从后面一次返回。我们可以看出F(3)重复计算了两次。
- 改进后的递归
int rec[20]; //记录数组,将F(n)的值保存起来。
int fuc(int n){
if(rec[n]>0) return rec[n]; //如果rec[n]保存了f(n)的值,不需要重复计算,直接返回。
if(n==0 || n==1 ) rec[n] = 1;
else rec[n] = fuc(n-1) + fuc(n-2);
return rec[n];
}
虽然该方法避免了大量的重复计算,但是存在大量的函数调用,每次都需要花费大量的时间用于参数的传递和动态链接等。
事实上,我们可以使用内存换时间,计算F(n) ,只需要知道 F(n-1) 和F(n-2),所以我们可以从迭代出口开始,利用迭代算法。
- 迭代算法:
int fuc(int n){
int i,f[20];
f[0] = 1;
f[1] = 1;
for(i = 2;i<=n;i++){
f[i] = f[i-1]+f[i-2];
}
return f[n];
}
这个就是最简单的动态规划算法,时间复杂度O(n);
实例二:01背包问题
有n个重量和价值分别为W[i],V[i]的物品。从这些物品中选出总重量不超过Weight的物品,求所有方案中价值总和的最大值。
分析:我最先想到的是进行暴力搜索,把所有情况都计算一遍,然后得出最大值。
代码如下:
int n = 4; //数量
int w[4] = {2,1,3,2}; //每个物品的重量
int value[4] = {3,2,4,2}; //,每个物品的价值
int Weight = 5; //总重量
int max(int a,int b){
return a>b?a:b;
}
int fuc(int W , int k){
int res;
if(k == n) res = 0; //注意 k是从0开始,当k等于4的时候,已经没有物品了,w[4]已经超界了。
else if(W>=w[k]){ //当选择某个物品的时候,剩余总重量大于当前物品的总重量
res = max( fuc(W,k+1),fuc(W-w[k],k+1) + value[k] ); //有两个选择,选择当前物品,或者后面有性价比更高的物品,
}else{ //放弃当前物品 。
res = fuc(W,k+1);
}
return res;
}
int main() {
printf("%d\n",fuc(5,0));
return 0;
}
可以看出这个递归的搜索方法的深度是n,而且每次都有两次分支,可知时间的复杂度是O(2^n)。从实例一可以知道,递归往往存在着重复计算,本题也不例外。所以我们再次考虑使用记忆化数组保存中间结果,提高算法效率。
代码如下:
#include <stdio.h>
#include <stdlib.h>
//int f[20]; //记录数组,将F(n)的值保存起来。
int n = 4; //数量
int w[4] = {2,1,3,2}; //每个物品的重量
int value[4] = {3,2,4,2}; //,每个物品的价值
int Weight = 5; //总重量
int rec[5][5]; //记忆化数组
int max(int a,int b){
return a>b?a:b;
}
int fuc(int W , int k){
int res;
if(rec[W][k]>0) return rec[W][k]; //如果保存了fuc(W,k)的值就直接返回。
if(k == n) res = 0; //注意 k是从0开始,当k等于4的时候,已经没有物品了,w[4]已经超界了。
else if(W>=w[k]){ //当选择某个物品的时候,剩余总重量大于当前物品的总重量
res = max( fuc(W,k+1),fuc(W-w[k],k+1) + value[k] ); //有两个选择,选择当前物品,或者后面有性价比更高的物品,
}else{ //放弃当前物品 。
res = fuc(W,k+1);
}
return rec[W][k] = res;
}
int main() {
printf("%d\n",fuc(5,0));
return 0;
}
这种算法大幅度提高了效率。
Fibonacci序列问题存在迭代式,01背包问题同样存在迭代式
dp[W][i]表示剩余重量为W,选取物品编号为i的最大价值数。
迭代式:
dp[W][n] = 0;当选到第n个时,已经没有物品可选
dp[W][i] = dp[W][i+1] ( W < w[i]); 当剩余重量小于当前物品重量时,不能选取。
dp[W][i] = max( dp[W][i+1],dp[W-w[i] ][i+1] + value[i] ) (W>=w[i]); 分为两种选取或不选取,求其最大值。
代码如下:
int n = 4; //数量
int w[4] = {2,1,3,2}; //每个物品的重量
int value[4] = {3,2,4,2}; //,每个物品的价值
int Weight = 5; //总重量
int dp[5][6]; //保存物品价值和
int max(int a,int b){
return a>b?a:b;
}
int fuc(int W , int k){
int i,j;
for(i=k-1;i>=0;i--){
for(j=0;j<=W;j++){
if(j<w[i]){
dp[i][j] = dp[i+1][j];
}else{
dp[i][j] = max( dp[i+1][j], dp[i+1][ j - w[i] ] + value[i] );
}
}
}
return dp[0][W];
}
int main() {
printf("%d\n",fuc(5,4));
return 0;
}
比较以上两种方法;时间复杂度相同O(n*w);记忆化递归方法实现比较容易,很多问题找迭代式很难,但是迭代式的动态规划代码相对简断。
未完待续