动态规划
动态规划(dynamicprogramming)是运筹学的一个分支,是求解决策过程(decisionprocess)最优化的数学方法。20世纪50年代初美国数学家R.E.Bellman等人在研究多阶段决策过程(multistepdecisionprocess)的优化问题时,提出了著名的最优化原理(principleofoptimality),把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解,创立了解决这类过程优化问题的新方法——动态规划。1957年出版了他的名著《DynamicProgramming》,这是该领域的第一本著作。
中文名:动态规划
外文名:dynamicprogramming
简称:DP
学科:运筹学
1、分类
动态规划一般可分为线性动规,区域动规,树形动规,背包动规四类。
举例:
线性动规:拦截导弹,合唱队形,挖地雷,建学校,剑客决斗等;
区域动规:石子合并,加分二叉树,统计单词个数,炮兵布阵等;
树形动规:贪吃的九头龙,二分查找树,聚会的欢乐,数字三角形等;
背包问题:01背包问题,完全背包问题,分组背包问题,二维背包,装箱问题,挤牛奶(同济ACM第1132题)等;
应用实例:
最短路径问题,项目管理,网络流优化等;
POJ动态规划题目列表:
容易:1018,1050,1083,1088,1125,1143,1157,1163,1178,1179,1189,1191,1208,1276,1322,1414,1456,1458,1609,1644,1664,1690,1699,1740,1742,1887,1926,1936,1952,1953,1958,1959,1962,1975,1989,2018,2029,2039,2063,2081,2082,2181,2184,2192,2231,2279,2329,2336,2346,2353,2355,2356,2385,2392,2424。
不易:1019,1037,1080,1112,1141,1170,1192,1239,1655,1695,1707,1733(区间减法加并查集),1737,1837,1850,1920(加强版汉罗塔),1934(全部最长公共子序列),1964(最大矩形面积,O(n*m)算法),2138,2151,2161,2178。
推荐:1015,1635,1636,1671,1682,1692,1704,1717,1722,1726,1732,1770,1821,1853,1949,2019,2127,2176,2228,2287,2342,2374,2378,2384,2411。
2、概念意义
动态规划问世以来,在经济管理、生产调度、工程技术和最优控制等方面得到了广泛的应用。例如最短路线、库存管理、资源分配、设备更新、排序、装载等问题,用动态规划方法比用其它方法求解更为方便。
虽然动态规划主要用于求解以时间划分阶段的动态过程的优化问题,但是一些与时间无关的静态规划(如线性规划、非线性规划),只要人为地引进时间因素,把它视为多阶段决策过程,也可以用动态规划方法方便地求解。
动态规划程序设计是对解最优化问题的一种途径、一种方法,而不是一种特殊算法。不像搜索或数值计算那样,具有一个标准的数学表达式和明确清晰的解题方法。动态规划程序设计往往是针对一种最优化问题,由于各种问题的性质不同,确定最优解的条件也互不相同,因而动态规划的设计方法对不同的问题,有各具特色的解题方法,而不存在一种万能的动态规划算法,可以解决各类最优化问题。因此读者在学习时,除了要对基本概念和方法正确理解外,必须具体问题具体分析处理,以丰富的想象力去建立模型,用创造性的技巧去求解。我们也可以通过对若干有代表性的问题的动态规划算法进行分析、讨论,逐渐学会并掌握这一设计方法。
概念引入
多阶段决策过程的最优化问题。
含有递推的思想以及各种数学原理(加法原理,乘法原理等等)。
在现实生活中,有一类活动的过程,由于它的特殊性,可将过程分成若干个互相联系的阶段,在它的每一阶段都需要作出决策,从而使整个过程达到最好的活动效果。当然,各个阶段决策的选取不是任意确定的,它依赖于当前面临的状态,又影响以后的发展,当各个阶段决策确定后,就组成一个决策序列,因而也就确定了整个过程的一条活动路线,如图所示:(看词条图)这种把一个问题看作是一个前后关联具有链状结构的多阶段过程就称为多阶段决策过程,这种问题就称为多阶段决策问题。
基本思想
动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式。
基本概念
如果一类活动过程可以分为若干个互相联系的阶段,在每一个阶段都需作出决策(采取措施),一个阶段的决策确定以后,常常影响到下一个阶段的决策,从而就完全确定了一个过程的活动路线,则称它为多阶段决策问题。
各个阶段的决策构成一个决策序列,称为一个策略。每一个阶段都有若干个决策可供选择,因而就有许多策略供我们选取,对应于一个策略可以确定活动的效果,这个效果可以用数量来确定。策略不同,效果也不同,多阶段决策问题,就是要在可以选择的那些策略中间,选取一个最优策略,使在预定的标准下达到最好的效果.
阶段:把所给求解问题的过程恰当地分成若干个相互联系的阶段,以便于求解,过程不同,阶段数就可能不同.描述阶段的变量称为阶段变量。在多数情况下,阶段变量是离散的,用k表示。此外,也有阶段变量是连续的情形。如果过程可以在任何时刻作出决策,且在任意两个不同的时刻之间允许有无穷多个决策时,阶段变量就是连续的。
在前面的例子中,第一个阶段就是点A,而第二个阶段就是点A到点B,第三个阶段是点B到点C,而第四个阶段是点C到点D。
状态:状态表示每个阶段开始面临的自然状况或客观条件,它不以人们的主观意志为转移,也称为不可控因素。在上面的例子中状态就是某阶段的出发位置,它既是该阶段某路的起点,同时又是前一阶段某支路的终点。在前面的例子中,第一个阶段有一个状态即A,而第二个阶段有两个状态B1和B2,第三个阶段是三个状态C1,C2和C3,而第四个阶段又是一个状态D。
过程的状态通常可以用一个或一组数来描述,称为状态变量。一般,状态是离散的,但有时为了方便也将状态取成连续的。当然,在现实生活中,由于变量形式的限制,所有的状态都是离散的,但从分析的观点,有时将状态作为连续的处理将会有很大的好处。此外,状态可以有多个分量(多维情形),因而用向量来代表;而且在每个阶段的状态维数可以不同。当过程按所有可能不同的方式发展时,过程各段的状态变量将在某一确定的范围内取值。状态变量取值的集合称为状态集合。
无后效性:我们要求状态具有下面的性质:如果给定某一阶段的状态,则在这一阶段以后过程的发展不受这阶段以前各段状态的影响,所有各阶段都确定时,整个过程也就确定了。换句话说,过程的每一次实现可以用一个状态序列表示,在前面的例子中每阶段的状态是该线路的始点,确定了这些点的序列,整个线路也就完全确定。从某一阶段以后的线路开始,当这段的始点给定时,不受以前线路(所通过的点)的影响。状态的这个性质意味着过程的历史只能通过当前的状态去影响它的未来的发展,这个性质称为无后效性。
决策:一个阶段的状态给定以后,从该状态演变到下一阶段某个状态的一种选择(行动)称为决策。在最优控制中,也称为控制。在许多问题中,决策可以自然而然地表示为一个数或一组数。不同的决策对应着不同的数值。描述决策的变量称决策变量,因状态满足无后效性,故在每个阶段选择决策时只需考虑当前的状态而无须考虑过程的历史。
决策变量的范围称为允许决策集合。策略:由每个阶段的决策组成的序列称为策略。对于每一个实际的多阶段决策过程,可供选取的策略有一定的范围限制,这个范围称为允许策略集合。允许策略
AB1C2是A到C2的最短路径,B1C2D也是B1到D的最短路径……──事实正是如此,因此我们认为这个例子满足最优性原理的要求。
基本结构
多阶段决策问题中,各个阶段采取的决策,一般来说是与时间有关的,决策依赖于当前状态,又随即引起状态的转移,一个决策序列就是在变化的状态中产生出来的,故有“动态”的含义,称这种解决多阶段决策最优化问题的方法为动态规划方法。
基本模型
根据上例分析和动态规划的基本概念,可以得到动态规划的基本模型如下:
(1)确定问题的决策对象。(2)对决策过程划分阶段。(3)对各阶段确定状态变量。(4)根据状态变量确定费用函数和目标函数。(5)建立各阶段状态变量的转移过程,确定状态转移方程。
状态转移方程的一般形式:
一般形式:U:状态;X:策略顺推:f=opt{f+L}其中,L:状态Uk-1通过策略Xk-1到达状态Uk的费用初始f;结果:f。
倒推:f=opt{f+L}L:状态Uk通过策略Xk到达状态Uk+1的费用初始f;结果:f(U1)
适用条件
任何思想方法都有一定的局限性,超出了特定条件,它就失去了作用。同样,动态规划也并不是万能的。适用动态规划的问题必须满足最优化原理和无后效性。
1.最优化原理(最优子结构性质)最优化原理可这样阐述:一个最优化策略具有这样的性质,不论过去状态和决策如何,对前面的决策所形成的状态而言,余下的诸决策必须构成最优策略。简而言之,一个最优化策略的子策略总是最优的。一个问题满足最优化原理又称其具有最优子结构性质。
2.无后效性将各阶段按照一定的次序排列好之后,对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。换句话说,每个状态都是过去历史的一个完整总结。这就是无后向性,又称为无后效性。
3.子问题的重叠性动态规划将原来具有指数级时间复杂度的搜索算法改进成了具有多项式时间复杂度的算法。其中的关键在于解决冗余,这是动态规划算法的根本目的。动态规划实质上是一种以空间换时间的技术,它在实现的过程中,不得不存储产生过程中的各种状态,所以它的空间复杂度要大于其它的算法。
3、实现问题
算法实现是比较好考虑的。但有时也会遇到一些问题,而使算法难以实现。动态规划思想设计的算法从整体上来看基本都是按照得出的递推关系式进行递推,这种递推相对于计算机来说,只要设计得当,效率往往是比较高的,这样在时间上溢出的可能性不大,而相反地,动态规划需要很大的空间以存储中间产生的结果,这样可以使包含同一个子问题的所有问题共用一个子问题解,从而体现动态规划的优越性,但这是以牺牲空间为代价的,为了有效地访问已有结果,数据也不易压缩存储,因而空间矛盾是比较突出的。另一方面,动态规划的高时效性往往要通过大的测试数据体现出来(以与搜索作比较),因而,对于大规模的问题如何在基本不影响运行速度的条件下,解决空间溢出的问题,是动态规划解决问题时一个普遍会遇到的问题。
一个思考方向是尽可能少占用空间。如从结点的数据结构上考虑,仅仅存储必不可少的内容,以及数据存储范围上精打细算(按位存储、压缩存储等)。当然这要因问题而异,进行分析。另外,在实现动态规划时,一个我们经常采用的方法是用一个与结点数一样多的数组来存储每一步的决策,这对于倒推求得一种实现最优解的方法是十分方便的,而且处理速度也有一些提高。但是在内存空间紧张的情况下,我们就应该抓住问题的主要矛盾。省去这个存储决策的数组,而改成在从最优解逐级倒推时,再计算一次,选择某个可能达到这个值的上一阶段的状态,直到推出结果为止。这样做,在程序编写上比上一种做法稍微多花一点时间,运行的时效也可能会有一些(但往往很小)的下降,但却换来了很多的空间。因而这种思想在处理某些问题时,是很有意义的。
但有时,即使采用这样的方法也会发现空间溢出的问题。这时就要分析,这些保留下来的数据是否有必要同时存在于内存之中。因为有很多问题,动态规划递推在处理后面的内容时,前面比较远处的内容实际上是用不着的。对于这类问题,在已经确信不会再被使用的数据上覆盖数据,从而使空间得以重复利用,如果能有效地使用这一手段,对于相当大规模的问题,空间也不至于溢出(为了求出最优方案,保留每一步的决策仍是必要的,这同样需要空间)。
一般地说,这种方法可以通过两种思路来实现:一种是递推结果仅使用Data1和Data2这样两个数组,每次将Data1作为上一阶段,推得Data2数组,然后,将Data2通过复制覆盖到Data1之上,如此反复,即可推得最终结果。这种做法有一个局限性,就是对于递推与前面若干阶段相关的问题,这种做法就比较麻烦;而且,每递推一级,就需要复制很多的内容,与前面多个阶段相关的问题影响更大。另外一种实现方法是,对于一个可能与前N个阶段相关的问题,建立数组Data,其中各项为前面N个阶段的保存数据。这样不采用这种内存节约方式时对于阶段k的访问只要对应成对数组Data中下标为kmod(N+1)的单元的访问就可以了。这种处理方法对于程序修改的代码很少,速度几乎不受影响,而且需要保留不同的阶段数也都能很容易实现。
当采用以上方法仍无法解决内存问题时,也可以采用对内存的动态申请来使绝大多数情况能有效出解。而且,使用动态内存还有一点好处,就是在重复使用内存而进行交换时,可以只对指针进行交换,而不复制数据,这在实践中也是十分有效的。
4、应用
常用软件
MATLAB、LINGO
作用
在编程中常用解决最长公共子序列问题、矩阵连乘问题、凸多边形最优三角剖分问题、电路布线等问题。
搜索
记忆化
给你一个数字三角形,形式如下:
1
23
456
78910
找出从第一层到最后一层的一条路,使得所经过的权值之和最小或者最大.
对于动态规划算法解决这个问题,我们根据状态转移方程和状态转移方向,比较容易地写出动态规划的循环表示方法。但是,当状态和转移非常复杂的时候,也许写出循环式的动态规划就不是那么简单了。
我们尝试从正面的思路去分析问题,如上例,不难得出一个非常简单的递归函数:
并且记忆搜索占的内存相对来说较少。
计算核心片段:
练习题
USACO2.2SubsetSums
题目如下:
对于从1到N的连续整数集合,能划分成两个子集合,且保证每个集合的数字和是相等的。
举个例子,如果N=3,对于{1,2,3}能划分成两个子集合,他们每个的所有数字和是相等的:
{3}and{1,2}
这是唯一一种分法(交换集合位置被认为是同一种划分方案,因此不会增加划分方案总数)
如果N=7,有四种方法能划分集合{1,2,3,4,5,6,7},每一种分发的子集合各数字和是相等的:
{1,6,7}and{2,3,4,5}{注1+6+7=2+3+4+5}
{2,5,7}and{1,3,4,6}
{3,4,7}and{1,2,5,6}
{1,2,4,7}and{3,5,6}
给出N,你的程序应该输出划分方案总数,如果不存在这样的划分方案,则输出0。程序不能预存结果直接输出。
PROGRAMNAME:subset
INPUTFORMAT
输入文件只有一行,且只有一个整数N
SAMPLEINPUT(filesubset.in)
7
OUTPUTFORMAT
输出划分方案总数,如果不存在则输出0。
SAMPLEOUTPUT(filesubset.out)
4
参考程序如下(C++语言):
题目如下:
在生物学中,一些生物的结构是用包含其要素的大写字母序列来表示的。生物学家对于把长的序列分解成较短的(称之为元素的)序列很感兴趣。
如果一个集合P中的元素可以通过并运算(允许重复;并,即∪,相当于Pascal中的“+”运算符)组成一个序列S,那么我们认为序列S可以分解为P中的元素。并不是所有的元素都必须出现。举个例子,序列ABABACABAAB可以分解为下面集合中的元素:
{A,AB,BA,CA,BBC}
序列S的前面K个字符称作S中长度为K的前缀。设计一个程序,输入一个元素集合以及一个大写字母序列,计算这个序列最长的前缀的长度。
PROGRAMNAME:prefix
INPUTFORMAT
输入数据的开头包括1..200个元素(长度为1..10)组成的集合,用连续的以空格分开的字符串表示。字母全部是大写,数据可能不止一行。元素集合结束的标志是一个只包含一个“.”的行。集合中的元素没有重复。接着是大写字母序列S,长度为1..200,000,用一行或者多行的字符串来表示,每行不超过76个字符。换行符并不是序列S的一部分。
SAMPLEINPUT(fileprefix.in)
AABBACABBC
.
ABABACABAABC
OUTPUTFORMAT
只有一行,输出一个整数,表示S能够分解成P中元素的最长前缀的长度。
SAMPLEOUTPUT(fileprefix.out)
11
示例程序如下:
#include<stdio.h>
#defineMAXP200
#defineMAXL10
charprim;
intnump;
intstart;
chardata;
intndata;
intmain(intargc,char**argv)
{
FILE*fout,*fin;
intbest;
intlv,lv2,lv3;
if((fin=fopen("prim.in","r"))==NULL)
{
perror("fopenfin");
exit(1);
}
if((fout=fopen("prim.out","w"))==NULL)
{
perror("fopenfout");
exit(1);
}
while(1)
{
fscanf(fin,"%s",prim);
if(prim!='.')
nump++;
else
break;
}
ndata=0;
while(fscanf(fin,"%s",data+ndata)==1)
ndata+=strlen(data+ndata);
start=1;
best=0;
for(lv=0;lv<ndata;lv++)
if(start)
{
best=lv;
for(lv2=0;lv2<nump;lv2++)
{
for(lv3=0;lv+lv3<ndata&&prim==data;lv3++)
if(!prim)
start=1;
}
}
if(start)
best=ndata;
fprintf(fout,"%i",best);
return0;
}
动态规划作为一种重要的信息学竞赛算法,具有很强的灵活性。以上提供的是一些入门练习题,深入的学习还需要逐步积累经验。
#include<stdio.h>
typedefstructObject{
intweight;
intvalue;//floatrate;
}
Object;
Object*array;//用来存储物体信息的数组
intnum;//物体的个数
intcontainer;//背包的容量
int**dynamic_table;//存储动态规划表
bool*used_table;//存储物品的使用情况
//ouputthetableofdynamicprogramming,it'sfordetection
voidprint_dynamic_table(){
printf("动态规划表如下所示:");
/*for(intj=0;j<=container;j++)printf("%d",j);printf("");*/
for(inti=1;i<=num;i++){
for(intj=0;j<=container;j++)
printf("%d",dynamic_table);
printf("");
}
}
//打印动态规划表
voidprint_array(){
for(inti=1;i<=num;i++)
printf("第%d个物品的重量和权重:%d%d",i,array.weight,array.value);
}
//打印输入的物品情况//插入排序,按rate=value/weight由小到大排//动态规划考虑了所有情况,所以可以不用排序
/*voidsort_by_rate(){
for(inti=2;i<=num;i++){
Objecttemp=array;
for(intj=i-1;j>=1;j--)
if(array.rate>temp.rate)
array=array;
elsebreak;
array=temp;
}}*/
voidprint_used_object(){
printf("所使用的物品如下所示:");
for(inti=1;i<=num;i++)
if(used_table==1)
printf("%d-%d",array.weight,array.value);
}
//打印物品的使用情况
/*做测试时使用
voidprint_used_table(bool*used_table){
printf("usedtableasfollows:");
for(inti=1;i<=num;i++)
printf("object%dis%d",i,used_table);
}*/
voidinit_problem(){
printf("输入背包的容量:");
scanf("%d",&container);
printf("输入物品的个数:");
scanf("%d",&num);
array=newObject;
printf("输入物品的重量和价值,格式如:4-15");
for(inti=1;i<=num;i++){
charc;
scanf("%d%c%d",&array.weight,&c,&array.value);
//array.rate=array.value/array.weight;
}
print_array();
}
//对物体的使用情况进行回查
voidtrace_back(){
intweight=container;
used_table=newbool;
for(inti=1;i<=num;i++)used_table=0;
//initalizetheused_tabletobenon-used
for(intj=1;j<num;j++){
//说明物品j被使用
if(dynamic_table!=dynamic_table){
weight-=array.weight;
used_table=1;
}
//print_used_table(used_table);
}
//检测第num个物品是否被使用
if(weight>=array.weight)
used_table=1;
}
voiddynamic_programming(){
dynamic_table=newint*;
for(intk=1;k<=num;k++)
dynamic_table=newint;
//dynamic_programmingtable
//为二维动态规划表分配内存
for(intm=1;m<num;m++)
for(intn=0;n<=container;n++)
dynamic_table=0;
inttemp_weight=array.weight;
for(inti=0;i<=container;i++)
dynamic_table=i<temp_weight?0:array.value;
//初始化动态规划表
for(intj=num-1;j>=1;j--){
temp_weight=array.weight;
inttemp_value=array.value;
for(intk=0;k<=container;k++)
if(k>=temp_weight&&dynamic_table<dynamic_table+temp_value)
dynamic_table=dynamic_table+temp_value;
elsedynamic_table=dynamic_table;
}//构建动态规划表
print_dynamic_table();//打印动态规划表
}
voidmain(){
init_problem();
dynamic_programming();
trace_back();
print_used_object();
}
5、推荐书籍
《DynamicProgramming》
推荐阅读