一、概念
动态规划过程是:每次决策依赖于当前状态,又随即引起状态的转移。一个决策序列就是在变化的状态中产生出来的,所以,这种多阶段最优化决策解决问题的过程就称为动态规划。
适用于动态规划算法的问题一般有以下三个性质:(1) 最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。(2) 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。(3)有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)
动态规划算法一般的求解步骤是:(1)分析最优解的性质,并刻画其结构特征。(2)递归的定义最优解。(3)以自底向上或自顶向下的记忆化方式(备忘录法)计算出最优值。(4)根据计算最优值时得到的信息,构造问题的最优解。
二、具体应用
1、0-1背包问题
问题描述:给定一个载重量为C的背包,n个物品,其重量为wi,价值为vi,1<=i<=n,要求:把物品装入背包,并使包内物品价值最大。x[n]表示物品的选择,x[1]表示将物品放入包中,数学表达式如下:
问题分析:
a.抽象之后问题转化为求一个最优化数组,x1,x2,...,xn的0-1序列
b.假设最优解的序列为x1,x2,....,xn,能使背包容量C的总价值最大
如果x1 = 1,x2,....,xn依然是背包容量C-w1总价值最大的序列
如果x1 = 0,x2,....,xn依然是背包容量C总价值最大的序列
这便是上面提到的最优子序列问题。
c.定义v[i,w]为放入1,2,…,n个物体到容量为w的背包里产生的最大价值,取值分析
1)v[0,w] = 0, 背包里面没有东西,背包容量为w, 价值为0
2)v[i, 0] = 0, 背包容量为0,不论i为多少,价值均为0
3)对于第i个物品有两种选择,放进背包或者不放
如果wi > w, 即背包容量不够放进第i个物品
v[i,w] = v[i-1,w];
如果wi<= w, 即背包容量还可以放进第i个物品,可以选择放或者不放,选择的标准就是让总价值最大
v[i,w] = max( v[i-1,w], ( v[i-1, w - wi ] + vi ) );
具体测试代码如下:
#include <iostream>
#include<algorithm>
#include <cstdlib>
//存储元素的结构
typedef struct item
{
int weight;
int values;
}item;
using namespace std;
int main()
{
int weight=10;
int itemnum=4;
//vector<vector<int> > k(11,vector<int>(5));
int k[11][5]={0};
item items[4]={{6,30},{3,14},{4,16},{2,9}};
for(int w=1;w<=weight;w++)
{
for(int j=1;j<=itemnum;j++)
{
if(items[j-1].weight>w) //物品质量大于背包容量,舍去
{
k[w][j]=k[w][j-1];
}
else //对于两种情况选出较大值
{
k[w][j]=max(k[w][j-1],(k[w-items[j-1].weight][j-1]+items[j-1].values));
}
}
}
cout<<"输出结果:"<<k[weight][itemnum]<<"\n";
for(int i=0;i<=weight;i++)
{
for(int j=0;j<=itemnum;j++)
{
cout<<k[i][j]<<" ";
}
cout<<"\n";
}
system( "pause" );
}
2、线性算法求解Fibonacci数列
通常用递归方法求解Fibonacci,会重复求解很多项,这样会严重影响运行时间。利用动态规划方法自下而上的方法去求解Fibonacci可以保证线性时间O(N)得到结果。
具体代码如下:
#include <iostream>
#include <cstdlib>
using namespace std;
typedef long long LL;
//注意数列项溢出的情况
LL Fibonacci( int n ){
if( n == 0 )
return 0;
if( n == 1 )
return 1;
LL Front = 1, back = 0;
LL temp;
for( int i = 1; i < n; i++ ){
temp = Front;
Front = Front + back;
back = temp;
}
return Front;
}
int main(){
int number;
cout<<"请输入Fibonacci数列的项数:";
cin>>number;
cout<<Fibonacci( number )<<endl;
system( "pause" );
}
3、硬币找零问题
问题描述:现存在一堆面值为 V1、V2、V3 … 个单位的硬币,问最少需要多少个硬币才能找出总值为 T 个单位的零钱?假设这一堆面值分别为 1、2、5、21、25 元,需要找出总值 T 为 63 元的零钱。
问题分析:基于动态规划的思想,我们可以从 1 元开始计算出最少需要几个硬币,然后再求 2 元、3元…每一次求得的结果都保存在一个数组中,以后需要用到时则直接取出即可。从 1 元开始依次找零时,可以尝试一下当前要找零的面值(这里指 1 元)是否能够被分解成另一个已求解的面值的找零需要的硬币个数再加上这一堆硬币中的某个面值之和,如果这样分解之后最终的硬币数是最少的,那么问题就能解决了。先假定以下变量:
values[] : 保存每一种硬币的币值的数组
valueKinds :币值不同的硬币种类数量,即values[]数组的大小
money : 需要找零的面值
coinsUsed[] : 保存面值为 i 的纸币找零所需的最小硬币数
当求解总面值为 i 的找零最少硬币数 coinsUsed[ i ] 时,将其分解成求解 coinsUsed[ i – cents]和一个面值为 cents 元的硬币,由于 i – cents < i , 其解 coinsUsed[ i – cents] 已经存在,如果面值为 cents 的硬币满足题意,那么最终解 coinsUsed[ i ] 则等于 coinsUsed[ i – cents] 再加上 1(即面值为 cents)的这一个硬币。
具体代码如下:
#include <iostream>
#include <cstdlib>
using namespace std;
void makeChange( int* values, int valueKinds, int money, int* coinsUsed ){
coinsUsed[0] = 0;
//对于每一分钱都找零,即保存子问题以备用,填表
for( int cents = 1; cents <= money; cents++ ){
//当用最小硬币找零时,所需硬币数量最多
int minCoins = cents;
//遍历每一种面值硬币,看是否作为找零的其中之一
for( int kind = 0; kind < valueKinds; kind++ ){
if( values[kind] <= cents ){
int temp = coinsUsed[cents-values[kind]] + 1;
if( temp < minCoins )
minCoins = temp;
}
}
coinsUsed[cents] = minCoins;
cout<<"面值为"<<cents<<"的最小硬币数:"<<coinsUsed[cents]<<endl;
}
}
int main(){
int conValue[5] = { 25, 12, 10, 5, 1 };
int money = 63;
//保存每一个面值所需的最小硬币数,0号舍弃不用,所以加1
int* coinsUsed = new int[money+1];
makeChange( conValue, 5, money, coinsUsed );
system( "pause" );
}