动态规划算法要求问题具有两个重要性质:最优子结构性质和子问题重叠性质。
1、设计动态规划算法的第一步是刻画最优解的结构。当问题的最优解包含子问题的最优解时,称该问题具有最优子结构性质。利用问题的最优子结构性质,以自底向上的方式递归的从子问题的最优解逐步构造出整个问题的最优解。
2、可用动态规划算法求解的问题的具备的另一个基本要素是子问题的重叠性质。用递归算法自顶向下解决问题时,每次产生的问题并不总是新问题,有些子问题被重复计算多次。动态规划算法利用了这种子问题的重叠性质,对每个子问题只解一次,而后将其解保存在一个数据结构中,当需要用到的时候,只用常熟时间查看以下。
下面结合0-1背包问题理解动态规划算法的思想:
1、问题:
有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
2、方法基本思路(一般是语言描述的算法)
根据最简单的情况分析:
输入: m 背包容量 n 物品个数
物品的重量: 3 4 5
物品的价值: 4 5 6
由于背包的容量未知,所以先从第一个物品开始试。如果能放进去,则再进行下一步操作,然后再进行下一次试探。
用一个图来表示背包的价值随着物品放入的变化:
解释:首先开辟一个较大的数组用于存储背包的价值随着放入物品的重量的变化。当放入物品1时,当前背包容量不足以容纳该物品时,就把该值赋值为0;如果可以装入,当前背包容量可以放入价值更大,则将该物品放入,由于上一个物品的价值是没有放入,则不能加上其他的价值,所以第一行一直未4。当第二个物品可以放入时,根据背包的容量调整放入的物品的价值,可以得到放入两个物品时,价值最大可达到9。当可以放入第三个物品时,依次类推,可得上表。
3、形式化和数学表达式
由基本思路对该问题形式化和抽象出数学表达式,抓住其核心思想
0-1背包问题的思想有:
状态转移方程:f[i][v]=max{f[i-1][v],f[i-1][v-w[i]]+v[i]}
记录子问题解:数组的值
4、编程实现
#include <iomanip> #include<iostream> using namespace std; int data[10][50]; //每种情况的最大价值,全局数组 int _tmain(int argc, _TCHAR* argv[]) { int m,n; int weight[4]={0,3,4,5}; //背包重量 int value[4]={0,4,5,6}; //背包价值 int ZeroOnePackage(int weight[4],int value[4],int m,int n); //背包函数 cout<<"请输入背包的最大容量和物品数:"<<endl; cin>>m>>n; //初始化 for(int i=0;i<10;i++) { for(int j=0;j<50;j++) data[i][j]=0; } cout<<"背包的最大价值为:"<<ZeroOnePackage(weight,value,m,n)<<endl; //输出背包的价值的变换数组 for(int i=1;i<=n;i++) { for(int j=1;j<=m;j++) cout<<setw(6)<<data[i][j]; cout<<endl; } return 0; } /* 动态规划方法: 1、状态转移方程:f[i][v]=max{f[i-1][v],f[i-1][v-wight[i]]+weight[i]}。 2、记录状态转换的数组,避免重复计算。 */ int ZeroOnePackage(int weight[4],int value[4],int m,int n) //背包函数 { for(int i=1;i<=n;i++) //数组下标很难控制 { for(int j=1;j<=m;j++) //j表示背包的当前容量 { if(weight[i]<=j) //物品的个数为i { if(data[i-1][j-weight[i]]+value[i]>data[i-1][j]) //当前背包容量,动态增长 { //状态转移,赋值为较大的,关键,状态转移方程 data[i][j]=data[i-1][j-weight[i]]+value[i]; } else { data[i][j]=data[i-1][j]; //赋值数组的上一个值给当前值 } } else { data[i][j]=data[i-1][j]; //赋值为0 } } } return data[n][m]; //保存最大价值的背包 }
5、优化时间空间复杂度算法
6、细节
7、总结
动态规划是自底向上解决问题,先解决子问题,并根据子问题解决新问题。递归是自顶向下解决问题。