动态规划初探及什么是无后效性?(转)

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

动态规划初探及什么是⽆后效性?(转)
对于动态规划,我是这样理解的:
把待解决的问题分为⼀个规模较原问题⼩的⼦问题、
然后要考虑的就是如何更具这个⼦问题如何得到原问题的解以及如何解决这个⼦问题
当然、原问题和⼦问题需要有相同的解决⽅式、它们只有问题规模的区别。

这样讲有点抽象、⽤⼀个简单的图来说明下:
可以简单的这样理解、把原问题划分为⼩的问题(能组合成原问题的,⼩的问题再划分、持续下去,找到简单解
反⽅向计算回来(记下每⼀步结果)最后就能得到解。

听起来似乎不难,但是要作⽐较深⼊的理解还是得通过实例说话
有向⽆环图的最长简单路径:
对于⼀般的图,求最长路径并不向最短路径那样容易,因为最长路径并没有最优⼦结构的属性。

但DGA例外
问题描述:
给⼀个带权有向⽆环图G=(V,E),找出这个图⾥的最长路径。

说实话初学者直接给出这个图会看蒙的、再看看问题,不知道从何下⼿。

好了,对上图做个简单的处理:
现在看起来是不是清晰多了呢
⽤dilg(v)表⽰以点结尾的最长路径,现在考虑dilg(D), dilg(B), dilg(C)
dilg(D)=max{dilg(B)+1, dilg(C)+3}
来解释⼀下:点D的⼊度边有CD、BD。

以D结尾的最短路径必定经过C、B中的⼀点;如果是C点,则以dilg(C)+3(权值)定会⼤于等于dilg(B)+1(权值)
如果没能看懂,请注意dilg(V)的定义
对于任意的点V可以有如下表达式:
dilg(v)=max{dilg(u)+w(u, v),(u,v)∈E}
这样、问题dilg(V)就会被转化为较⼩的⼦问题dilg(U)(当然,U是连⼊V的点)
任何⼀个问题都可以⽤这样的⽅式转化、变⼩。

但总不能⽆限变⼩啊,最后回成为最简单的问题。

当有两个点,且其中的⼀个点⼊度为0的时候如图中的S-->C他们的最长距离就是权值2。

⼊门篇中说过,思考⽅向是从复杂到简单,⽽计算⽅向是从简单到复杂。

算法如下
Initialize all dilg(.) values to ∞;
1.Let S be the set of vertices with indegree=0; ////设集合S,⾥⾯的是⼊度为0的点
2.For each vertex v in S do
dilg(v)=0;
3. For each v∈V\S in Topological Sorting order do //对于V中除S外的点、按照拓扑排序的顺序,依次求出最长路径并保存好 dilg(v)=max{dilg(u)+w(u, v),(u,v)∈E} //拓扑排序可以简单的理解为从起点到终点的最短路径
4. Return the dilg(v) with maximum value.
现在是找到最长路径的⼤⼩了、但是如何得到最长路径呢?
只需做点⼩⼩的改动:
Let S be the set of vertices with indegree=0;
for each vertex v in S do
dist(v)=0;
4. For each v∈V\S in Topological Sorting order do
dilg(v)=max(u,v)∈E{dilg(u)+w(u, v)}
let (u,v) be the edge to get the maximum
value;
dad(v)=u;
5. Return the dilg(.) with maximum value.
每⼀步都记下V的⽗节点、最后根据dad()数组即可得到路径。

对于上⾯的问题:先找出⼦问题、然后解决由⼦问题如何得到⽗问题以及如何把⼦问题分为更⼩的⼦问题
注意:问题的本质没有改变,只是规模变⼩。

我的理解:
动态规划的⽬的正是在不改变问题本质的情况下不断缩⼩⼦问题的规模、规模很⼩的时候,也就能很容易得到解啦(正如上⾯的只有两个点的情况)
上图可以这样理解:
问题A5被分成了⼦问题A3、A4解决它们就能解决A5,由于A3、A4和A5有相同的结构(问题的本质没变)所以A3可以分为问题A1、A2。

当然A4也能
分为两个⼦问题,只是图中没画出来。

下⾯的是再⽹上看到的不错的思路:
Dynamic programming:
(1)problem is solved by identifying a collection of
subproblems,
(2) tackling them one by one, smallest rst,
(3) using the answers of small problems to help
figure out larger ones,
(4) until the whole lot of them is solved.
初探动态规化
刚学动态规划,或多或少都有⼀些困惑。

今天我们来看看什么是动态规划,以及他的应⽤。

学过分治⽅法的⼈都知道,分治⽅法是通过组合⼦问题来求解原问题,⽽动态规划与分治⽅法相似,都是通过组合⼦问题的解来求解原问题,不同的是分治每次都产⽣⼀个新的⼦问题,⽽动态规划会不⼀定产⽣新问题,可能产⽣重叠的⼦问题,即不同的⼦问题有公共的⼦⼦问题。

说了这么多的⽐较苦涩的话,只是为了回头再看,我们通过⼀个例⼦来具体说明⼀下:
钢条切割问题
⼩王刚来到⼀家公司,他的顶头boss买了⼀条长度为10的钢条,boss让⼩王将其切割为短钢条,使得这条钢条的价值最⼤,⼩王应该如何做?我们假设切割⼯序本⾝没有成本⽀出。

已知钢条的价格表如下:
长度 i12345678910
价格P(i)1589101717202430
⼩王是⼀个⾮常聪明的⼈,⽴刻拿了张纸画了⼀下当这根钢条长度为4的所有切割⽅案(将问题的规模缩⼩)
⼩王很快看出了可以获得的最⼤收益为5+5=10,他想了想,⾃⼰画图以及计算最⼤值的整个流程,整理了⼀下:
1>先画出切⼀⼑所有的切割⽅案:(1+8)、(5+5)、(8+1)⼀共三种可能,也就是 4 - 1中可能,把这个 4 换成 n(将具体情况换成⼀般情况),就变成了长度为 n 的钢条切⼀⼑会有 n -1 中可能,然后在将⼀⼑切过的钢条再进⾏切割。

同上,(1+8)这个组合会有 2 中切法,(1+1+5)和(1+5+1)【看图】,同理,(5+5)会有两种切法(1+1+5)和(5+1+1),由于
(1+1+5)和上⾯的(1+1+5)重合,所以算⼀种切法,依次类推。

由于我们对 n-1个切点总是可以选择切割或不切割,所以长度为 n 的钢条共有 2^(n-1)中不同的切割⽅案
2>从这2^(n-1)中⽅案中选出可以获得最⼤收益的⼀种⽅案
学过递归的⼩王很快就把上述过程抽象成了⼀个函数,写出了以下的数学表达式:
设钢条长度为n的钢条可以获得的最⼤收益为 r(n) (n>=1)
第⼀个参数P(n)表⽰不切割对应的⽅案,其他 n-1个参数对应着另外 n-1中⽅案(对应上⾯的⼀⼑切)
为了求解规模为 n 的原问题,我们先求解形式完全⼀样,但规模更⼩的⼦问题。

即当完成⾸次切割后,我们将两段钢条看成两个独⽴的钢条切割问题来对待。

我们通过组合两个相关⼦问题的最优解,并在所有可能的两段切割⽅案中选取组合收益最⼤者,构成原问题的最优解我们成钢条问题满⾜最优⼦结构性质
编程能⼒很强的⼩王拿出笔记本
很快的在电脑上写下了如下代码
#include <stdio.h>
int CUT_ROD(int * p ,int n);
int max(int q, int a);
int main(void){
int i = 0;
int p[10] = {1,5,8,9,10,17,17,20,24,30};
printf("请输⼊钢条的长度(正整数):\n");
scanf("%d",&i);
int maxEarning = CUT_ROD(p,i); // 切割钢条
printf("钢条长度为 %d 的钢条所能获得的最⼤收益为:%d\n",i,maxEarning);
return0;
}
// 切割钢条
int CUT_ROD(int * p,int n){
int i;
if(n < 0){
printf("您输⼊的数据不合法!\n");
return -1;
}else if(n == 0){
return0;
}else if(n > 10){
printf("您输⼊的值过⼤!\n");
return -1;
}
int q = -1;
for(i = 0; i < n;i++){
q = max(q,p[i] + CUT_ROD(p,n-1-i));
int max(int q, int a){
if(q > a)
return q;
return a;
}
沾沾⾃喜的⼩王拿着⾃⼰的代码到boss⾯前,说已经搞定了。

boss看了看他的代码,微微⼀笑,说,你学过指数爆炸没,你算算你的程序的时间复杂度是多少,看看还能不能进⾏优化?⼩王⼀听蒙了,⾃⼰这些还没有想过,⾃⼰拿着笔和纸算了好⼤⼀会,得出了复杂度为T(n) = 2^n,没想到⾃⼰写的代码这么烂,规模稍微变⼤就不⾏了。

boss看了看⼩王,说:你想想你的代码效率为什么差?⼩王想了想,说道:“我的函数CUT-ROD反复地利⽤相同的参数值对⾃⾝进⾏递归调⽤,它反复求解了相同的⼦问题了”boss说:“还不错嘛?知道问题出在哪⾥了,那你怎样解决呢?”⼩王摇了摇头,boss说:“你应该听过动态规划吧,你可以⽤数组把你对⼦问题求解的值存起来,后⾯如果要求解相同的⼦问题,直接⽤之前的值就可以了,动态规划⽅法是付出额外的内存空间来节省计算时间,是典型的时空权衡”,听到这⾥⼩王暗⾃佩服眼前的boss,姜还是⽼的辣呀。

boss说完拿起⼩王的笔记本,写下了如下代码:
// 带备忘的⾃顶向下法求解最优钢条切割问题
#include <stdio.h>
int MEMOIZED_CUT_ROD(int * p,int n);
int MEMOIZED_CUT_ROD_AUX(int * p, int n, int * r);
int max(int q,int s);
int main(void){
int n;
int p[11]={-1,1,5,8,9,10,17,17,20,24,30};
printf("请输⼊钢条的长度(正整数 < 10):\n");
scanf("%d",&n);
if(n < 0 || n >10){
printf("您输⼊的值有误!");
}else{
int r = MEMOIZED_CUT_ROD(p,n);
printf("长度为%d的钢条所能获得的最⼤收益为:%d\n",n,r);
}
return0;
}
int MEMOIZED_CUT_ROD(int * p, int n){
int r[20];
for(int i = 0; i <= n; i++){
r[i] = -1;
}
return MEMOIZED_CUT_ROD_AUX(p,n,r);
}
int MEMOIZED_CUT_ROD_AUX(int * p, int n, int * r){
if(r[n] >= 0){
return r[n];
}
if(n == 0){
return0;
}else{
int q = -1;
for(int i = 1; i <= n; i++){// 切割钢条,⼤纲有 n 中⽅案
q = max(q,p[i] + MEMOIZED_CUT_ROD_AUX(p,n-i,r));
}
r[n] = q;// 备忘
return q;
}
}
int max(int q, int s){
if(q > s){
return q;
⼩王两眼瞪的直直的。

boss好厉害,我这刚⼊职的⼩⽩还得好好修炼呀。

写完boss说:这中⽅法被称为带备忘的⾃顶向下法。

这个⽅法按⾃然的递归形式编写过程,但过程会保存每个⼦问题的解(通常保存在⼀个数组或散列表中),当需要⼀个⼦问题的解时,过程⾸先检查是否已经保存过此解,如果是,则直接返回保存的值。

还有⼀种⾃底向上法。

这种⽅法⼀般需要恰当定义⼦问题的规模,使得任何⼦问题的求解都只依赖于“更⼩的”⼦问题的求解。

因⽽我们可以将⼦问题按规模排序,按由⼩⾄⼤的顺序进⾏求解,当求解某个⼦问题是,它所依赖的那些更⼩的⼦问题都已经求解完毕,结果已经保存,每个⼦问题只需求解⼀次。

如果你有兴趣回去好好看看书⾃⼰下去好好研究下吧。

最后再考考你,我写的这个带备忘的⾃顶向下法的时间复杂度是多少?⼩王看了看代码,⼜是循环,⼜是递归的,脑⼦都转晕了。

boss接着说:“你可以这样想,我这个函数是不是对规模为0,1,…,n的问题进⾏了求解,那么你看,当我求解规模为n的⼦问题时,for循环是不是迭代了n次,因为在我整个n规模的体系中,每个⼦问题只求解⼀次,也就是说我for循环⾥的递归直接返回的是之前已经计算了的值,⽐如说求解 n =3的时候,for(int i = 1,i <=3;i++),循环体执⾏三次,n=4时,循环体执⾏四次,所以说,我这个函数MEMOIZED_CUT_ROD进⾏的所有递归调⽤执⾏此for循环的迭代次数是⼀个等差数列,其和是O(n^2)”,是不是效率⾼了许多。

⼩王嗯嗯直点头,想着回去得买本《算法导论》好好看看。

⽆后效性是⼀个问题可以⽤动态规划求解的标志之⼀,理解⽆后效性对求解动态规划类题⽬⾮常重要某阶段的状态⼀旦确定,则此后过程的演变不再受此前各种状态及决策的影响
百度百科是这样定义的,是不是很苦涩,难懂。

并且⽹上对这个名词的解释⼤多都是理论性的,不好理解,今天我们通过⼀个例⼦来看看什么是⽆后效性
现在有⼀个四乘四的⽹格,左上⾓有⼀个棋⼦,棋⼦每次只能往下⾛或者往右⾛,现在要让棋⼦⾛到右下⾓
假设棋⼦⾛到了第⼆⾏第三列,记为s(2,3),如下图,画了两条路线和⼀条不符合题意的路线,那么当前的棋⼦[s(2,3)位置]怎么⾛到右下⾓和之前棋⼦是如何⾛到s(2,3)这个位置⽆关[不管是⿊⾊尖头的路线还是蓝⾊箭头的路线]
换句话说,当位于s(2,3)的棋⼦要进⾏决策(向右或者向下⾛)的时候,之前棋⼦是如何⾛到s(2,3)这个位置的是不会影响我做这个决策的。

之前的决策不会影响了未来的决策(之前和未来相对于现在棋⼦位于s(2,3)的时刻),这就是⽆后效性,也就是所谓的“未来与过去⽆关”
看完了⽆后效性,那我们再来看看有后效性,还是刚才的例⼦,只不过现在题⽬的条件变了,现在棋⼦可以上下左右⾛但是不能⾛重复的格⼦
那么现在红⾊箭头就是⼀个合法的路线了,当我的棋⼦⾛到了s(2,3)这个位置的时候,要进⾏下⼀步的决策的时候,这时候的决策是受之前棋⼦是如何⾛到s(2,3)的决策的影响的,⽐如说红⾊箭头的路线,如果是红⾊箭头决策⽽形成的路线,那么我下⼀步决策就不能往下⾛了[因为题意要求不能⾛重复的格⼦],之前的决策影响了未来的决策,”之前影响了未来”,这就叫做有后效性
在此感谢腾讯⼤神的指导,学习离不开⾃⼰的努⼒和名师的指导。

相关文档
最新文档