最近在学习算法分析,贪心算法,分治算法,动态规划是算法的三大基石,解决任何问题几乎都离不开这三种算法,在这里写下学到的和理解到的。
一:什么是动态规划?
学习一个东西首先要搞明白它是什么:
和分治法一样,动态规划(dynamicprogramming)是通过组合子问题而解决整个问题的解。
分治法是将问题划分成一些独立的子问题,递归地求解各子问题,然后合并子问题的解。
动态规划适用于子问题不是独立的情况,也就是各子问题包含公共的子子问题。独立的时候用分治算法。
此时,分治法会做许多不必要的工作,即重复地求解公共的子问题。动态规划算法对每个子问题只求解一次,将其结果保存起来,从而避免每次遇到各个子问题时重新计算答案。
二:依据的原理:
“一个过程的最优决策具有这样的性质:即无论其初始状态和初始决策如何,其今后诸策略对以第一个决策所形成的状态作为初始状态的过程而言,必须构成最优策略”。简言之,一个最优策略的子策略,对于它的初态和终态而言也必是最优的。
这个“最优化原理”如果用数学化一点的语言来描述的话,就是:假设为了解决某一优化问题,需要依次作出n个决策D1,D2,…,Dn,如若这个决策序列是最优的,对于任何一个整数k,1 < k < n,不论前面k个决策是怎样的,以后的最优决策只取决于由前面决策所确定的当前状态,即以后的决策Dk+1,Dk+2,…,Dn也是最优的。
最优化原理是动态规划的基础。任何一个问题,如果失去了这个最优化原理的支持,就不可能用动态规划方法计算。能采用动态规划求解的问题都需要满足一定的条件:
(1) 问题中的状态必须满足最优化原理;
(2) 问题中的状态必须满足无后效性。
所谓的无后效性是指:“下一时刻的状态只与当前状态有关,而和当前状态之前的状态无关,当前的状态是对以往决策的总结”。
以上似乎有点绕口,不过不明白也不要要紧,我是粘贴过来的,也不是很理解,老师也没讲。
三:问题的求解模式:
动态规划所处理的问题是一个多阶段决策问题,一般由初始状态开始,通过对中间阶段决策的选择,达到结束状态。这些决策形成了一个决策序列,同时确定了完成整个过程的一条活动路线(通常是求最优的活动路线)。如图所示。动态规划的设计都有着一定的模式,一般要经历以下几个步骤。
初始状态→│决策1│→│决策2│→…→│决策n│→结束状态
图1 动态规划决策过程示意图
(1)划分阶段:按照问题的时间或空间特征,把问题分为若干个阶段。在划分阶段时,注意划分后的阶段一定要是有序的或者是可排序的,否则问题就无法求解。
(2)确定状态和状态变量:将问题发展到各个阶段时所处于的各种客观情况用不同的状态表示出来。当然,状态的选择要满足无后效性。
(3)确定决策并写出状态转移方程:因为决策和状态转移有着天然的联系,状态转移就是根据上一阶段的状态和决策来导出本阶段的状态。所以如果确定了决策,状态转移方程也就可写出。但事实上常常是反过来做,根据相邻两段各状态之间的关系来确定决策。
(4)寻找边界条件:给出的状态转移方程是一个递推式,需要一个递推的终止条件或边界条件。
四:算法实现:
动态规划的主要难点在于理论上的设计,也就是上面4个步骤的确定,一旦设计完成,实现部分就会非常简单。使用动态规划求解问题,最重要的就是确定动态规划三要素:问题的阶段,每个阶段的状态以及从前一个阶段转化到后一个阶段之间的递推关系。递推关系必须是从次小的问题开始到较大的问题之间的转化,从这个角度来说,动态规划往往可以用递归程序来实现,不过因为递推可以充分利用前面保存的子问题的解来减少重复计算,所以对于大规模问题来说,有递归不可比拟的优势,这也是动态规划算法的核心之处。确定了动态规划的这三要素,整个求解过程就可以用一个最优决策表来描述,最优决策表是一个二维表,其中行表示决策的阶段,列表示问题状态,表格需要填写的数据一般对应此问题的在某个阶段某个状态下的最优值(如最短路径,最长公共子序列,最大价值等),填表的过程就是根据递推关系,从1行1列开始,以行或者列优先的顺序,依次填写表格,最后根据整个表格的数据通过简单的取舍或者运算求得问题的最优解。
五:算法设计:
两种方法:
自顶向下(又称记忆化搜索、备忘录):基本上对应着递归函数实现,从大范围开始计算,要注意不断保存中间结果,避免重复计算
自底向上(递推):从小范围递推计算到大范围
动态规划的重点:
递归方程+边界条件
六:背包问题
1, 首先说明背包问题属于NP完备问题。简单地说NP完备问题就是可以用多项式时间表示的问题。但是它却有一个动态规划解法(贪心算法不能解决该问题)
如上图所示:第一列值代表的是物品的种类,第二列代表宝藏的价值,第三列代表宝藏的重量,背包可以容纳的总重量是11,显然根据贪心算法不能得到最多的财富。那么动态规划算法是否具有多项式的时间呢? 重点来了:该解法有着O(n*W)的时间复杂度,n是物品的个数,W是背包限制的最大负重!
其实多项式时间是相对于输入规模来说的,输入规模最直观的理解就是输入到该算法的数据占了多少比特内存。0-1背包的输入有n个物品的价值,n个物品的重量,还有背包的最大负重W。如今假设W占用的比特数为L(也就是说背包的最大负重的输入规模是L),那么log(W)=L,所以O(n*W)=O(n*2^L),由此看到,该算法的时间复杂度对于输入规模L来说是指数级别的,随着输入规模L的增加,运算时间会迅速增长。
实际上,人们把这种动态规划的算法称为伪多项式时间算法(pseudo-polynomial time algorithm),这种算法不能真正意义上实现多项式时间内解决问题。
2,算法分析与实现:对于任何一个递归算法来说最重要的就是得到一个目标函数,有了这个目标函数问题就简单的多了。
在这里我们的目标函数OPT(i,w)代表了i个物品容量是w时的最大价值,case1,case2,很简单就不做翻译了!
OPT(i,w):i=0 时就为0,这是个递归算法必要的边界条件;OPT(i-1,w),这说明第i个物品过重背包装不下,当然就不要装了;否则比较把第i-1个物品用第i个物品替换前后哪个价值更大就选哪个!(替换后物品总数还是i-1),这就是用到了每一步都是最优的原理!
3,这里采用自底向上的递推算法:如下图
程序代码如下:
#include<stdio.h>
int V[200][200];//前i个物品装入容量为j的背包中获得的最大价值
int max(int a,int b)
{
if(a>=b)
return a;
else return b;
}
int KnapSack(int n,int w[],int v[],int x[],int C)
{
int i,j;
for(i=0;i<=n;i++)
V[i][0]=0;
for(j=0;j<=C;j++)
V[0][j]=0;
for(i=0;i<=n-1;i++)
for(j=0;j<=C;j++)
if(j<w[i])
V[i][j]=V[i-1][j];
else
V[i][j]=max(V[i-1][j],V[i-1][j-w[i]]+v[i]);
j=C;
for(i=n-1;i>=0;i--)
{
if(V[i][j]>V[i-1][j])
{
x[i]=1;
j=j-w[i];
}
else
x[i]=0;
}
printf("选中的物品是:\n");
for(i=0;i<n;i++)
printf("%d ",x[i]);
printf("\n");
return V[n-1][C];
}
void main()
{
int s;//获得的最大价值
int w[15];//物品的重量
int v[15];//物品的价值
int x[15];//物品的选取状态
int n,i;
int C;//背包最大容量
n=5;
printf("请输入背包的最大容量:\n");
scanf("%d",&C);
printf("输入物品数:\n");
scanf("%d",&n);
printf("请分别输入物品的重量:\n");
for(i=0;i<n;i++)
scanf("%d",&w[i]);
printf("请分别输入物品的价值:\n");
for(i=0;i<n;i++)
scanf("%d",&v[i]);
s=KnapSack(n,w,v,x,C);
printf("最大物品价值为:\n");
printf("%d\n",s);
}
程序运行结果符合预期