跟大牛学动归

来源:互联网 发布:c语言老师 编辑:程序博客网 时间:2024/04/28 03:05
下面贴了几篇我认为对学习动归很有用的文章,(都是转载的)网上都可以搜的到,我想把他们收集到一块,可能对于理解动归会更有帮助。

第一篇:(通过故事学习01背包,有助于记忆,好文章多看多分享)
文章出处:点击打开链接
通过金矿模型介绍动态规划

点击下载01背包测试数据.rar     

 

         对于动态规划,每个刚接触的人都需要一段时间来理解,特别是第一次接触的时候总是想不通为什么这种方法可行,这篇文章就是为了帮助大家理解动态规划,并通过讲解基本的01背包问题来引导读者如何去思考动态规划。本文力求通俗易懂,无异性,不让读者感到迷惑,引导读者去思考,所以如果你在阅读中发现有不通顺的地方,让你产生错误理解的地方,让你难得读懂的地方,请跟贴指出,谢谢!

 

 

 

 

----第一节----初识动态规划--------

 

       经典的01背包问题是这样的:

       有一个包和n个物品,包的容量为m,每个物品都有各自的体积和价值,问当从这n个物品中选择多个物品放在包里而物品体积总数不超过包的容量m时,能够得到的最大价值是多少?[对于每个物品不可以取多次,最多只能取一次,之所以叫做01背包,0表示不取,1表示取]

 

       为了用一种生动又更形象的方式来讲解此题,我把此题用另一种方式来描述,如下:

      

       有一个国家,所有的国民都非常老实憨厚,某天他们在自己的国家发现了十座金矿,并且这十座金矿在地图上排成一条直线,国王知道这个消息后非常高兴,他希望能够把这些金子都挖出来造福国民,首先他把这些金矿按照在地图上的位置从西至东进行编号,依次为0、1、2、3、4、5、6、7、8、9,然后他命令他的手下去对每一座金矿进行勘测,以便知道挖取每一座金矿需要多少人力以及每座金矿能够挖出多少金子,然后动员国民都来挖金子。

 

       题目补充1:挖每一座金矿需要的人数是固定的,多一个人少一个人都不行。国王知道每个金矿各需要多少人手,金矿i需要的人数为peopleNeeded[i]。

       题目补充2:每一座金矿所挖出来的金子数是固定的,当第i座金矿有peopleNeeded[i]人去挖的话,就一定能恰好挖出gold[i]个金子。否则一个金子都挖不出来。

       题目补充3:开采一座金矿的人完成开采工作后,他们不会再次去开采其它金矿,因此一个人最多只能使用一次。

       题目补充4:国王在全国范围内仅招募到了10000名愿意为了国家去挖金子的人,因此这些人可能不够把所有的金子都挖出来,但是国王希望挖到的金子越多越好。

       题目补充5:这个国家的每一个人都很老实(包括国王),不会私吞任何金子,也不会弄虚作假,不会说谎话。

       题目补充6:有很多人拿到这个题后的第一反应就是对每一个金矿求出平均每个人能挖出多少金子,然后从高到低进行选择,这里要强调这种方法是错的,如果你也是这样想的,请考虑背包模型,当有一个背包的容量为10,共有3个物品,体积分别是3、3、5,价值分别是6、6、9,那么你的方法取到的是前两个物品,总价值是12,但明显最大值是后两个物品组成的15。

       题目补充7:我们只需要知道最多可以挖出多少金子即可,而不用关心哪些金矿挖哪些金矿不挖。

 

       那么,国王究竟如何知道在只有10000个人的情况下最多能挖出多少金子呢?国王是如何思考这个问题的呢?

 

       国王首先来到了第9个金矿的所在地(注意,第9个就是最后一个,因为是从0开始编号的,最西边的那个金矿是第0个),他的臣子告诉他,如果要挖取第9个金矿的话就需要1500个人,并且第9个金矿可以挖出8888个金子。听到这里国王哈哈大笑起来,因为原先他以为要知道十个金矿在仅有10000个人的情况下最多能挖出多少金子是一件很难思考的问题,但是,就在刚才听完他的臣子所说的那句话时,国王已经知道总共最多能挖出多少金子了,国王是如何在不了解其它金矿的情况下知道最多能挖出多少金子的呢?他的臣子们也不知道这个谜,因此他的臣子们就问他了:“最聪明的国王陛下,我们都没有告诉您其它金矿的情况,您是如何知道最终答案的呢?”

 

       得意的国王笑了笑,然后把他最得意的“左、右手”叫到跟前,说到:“我并不需要考虑最终要挖哪些金矿才能得到最多的金子,我只需要考虑我面前的这座金矿就可以了,对于我面前的这座金矿不外乎仅有两种选择,要么挖,要么不挖,对吧?”

 

       “当然,当然”大臣们回答倒。

      

       国王继续说道:“如果我挖取第9座金矿的话那么我现在就能获得8888个金子,而我将用去1500个人,那么我还剩下8500个人。我亲爱的左部下,如果你告诉我当我把所有剩下的8500个人和所有剩下的其它金矿都交给你去开采你最多能给我挖出多少金子的话,那么我不就知道了在第9个金矿一定开采的情况下所能得到的最大金币数吗?”

      

       国王的左部下听后回答道:“国王陛下,您的意思是如果我能用8500个人在其它金矿最多开采出x个金币的话,那您一共就能够获得 x + 8888个金子,对吗?”

      

       “是啊,是啊……如果第9座金矿一定开采的话……”大臣们点头说到。

      

       国王笑着继续对着他的右部下说到:“亲爱的右部下,也许我并不打算开采这第9座金矿,那么我依然拥有10000个人,如果我把这10000个人和剩下的金矿都给你的话,你最多能给我挖出多少个金子呢?”

      

       国王的右部下聪明地说道:“尊敬的国王陛下,我明白您的意思了,如果我回答最多能购开采出y个金币的话,那您就可以在y和x+8888之间选择一个较大者,而这个较大者就是最终我们能获得的最大金币数,您看我这样理解对吗?”

      

 

       国王笑得更灿烂了,问他的左部下:“那么亲爱的左部下,我给你8500个人和其余金矿的话你能告诉我最多能挖出多少金子吗?”

 

       “请您放心,这个问题难不倒我”。左部下向国王打包票说到。

 

       国王高兴地继续问他的右部下:“那右部下你呢,如果我给你10000个人和其余金矿的话你能告诉我最多能挖出多少金子吗?”

 

       “当然能了!交给我吧!”右部下同左部下一样自信地回答道。

 

       “那就拜托给你们两位了,现在我要回到我那舒适的王宫里去享受了,我期待着你们的答复。”国王说完就开始动身回去等消息了,他是多么地相信他的两个大臣能够给他一个准确的答复,因为国王其实知道他的两位大臣要比他聪明得多。

 

       故事发展到这里,你是否在想国王的这两个大臣又是如何找到让国王满意的答案的呢?他们为什么能够如此自信呢?事实上他们的确比国王要聪明一些,因为他们从国王的身上学到了一点,就是这一点让他们充满了自信。

 

       国王走后,国王的左、右部下来到了第8座金矿,早已在那里等待他们的金矿勘测兵向两位大臣报道:“聪明的两位大臣,您们好,第8座金矿需要1000个人才能开采,可以获得7000个金子”。

 

       因为国王仅给他的左部下8500个人,所以国王的左部下叫来了两个人,对着其中一个人问到:“如果我给你7500个人和除了第8、第9的其它所有金矿的话,你能告诉我你最多能挖出多少金子吗?”

 

       然后国王的左部下继续问另一个人:“如果我给你8500个人和除了第8、第9的其它所有金矿的话,你能告诉我你最多能挖出多少金子吗?”

 

       国王的左部下在心里想着:“如果他们俩都能回答我的问题的话,那国王交给我的问题不就解决了吗?哈哈哈!”

 

       因为国王给了他的右部下10000个人,所以国王的右部下同样也叫来了两个人,对着其中一个人问:“如果我给你9000个人和除了第8、第9的其它所有金矿的话,你能告诉我你最多能挖出多少金子吗?”

 

       然后国王的右部下继续问他叫来的另一个人:“如果我给你10000个人和除了第8、第9的其它所有金矿的话,你能告诉我你最多能挖出多少金子吗?”

 

       此时,国王的右部下同左部下一样,他们都在为自己如此聪明而感到满足。

      

       当然,这四个被叫来的人同样自信地回答没有问题,因为他们同样地从这两个大臣身上学到了相同的一点,而两位自认为自己一样很聪明的大臣得意地笑着回到了他们的府邸,等着别人回答他们提出来的问题,现在你知道了这两个大臣是如何解决国王交待给他们的问题了吗?

 

       那么你认为被大臣叫去的那四个人又是怎么完成大臣交给他们的问题的呢?答案当然是他们找到了另外八个人!

 

       没用多少功夫,这个问题已经在全国传开了,更多人的人找到了更更多的人来解决这个问题,而有些人却不需要去另外找两个人帮他,哪些人不需要别人的帮助就可以回答他们的问题呢?

 

       很明显,当被问到给你z个人和仅有第0座金矿时最多能挖出多少金子时,就不需要别人的帮助,因为你知道,如果z大于等于挖取第0座金矿所需要的人数的话,那么挖出来的最多金子数就是第0座金矿能够挖出来的金子数,如果这z个人不够开采第0座金矿,那么能挖出来的最多金子数就是0,因为这唯一的金矿不够人力去开采。让我们为这些不需要别人的帮助就可以准确地得出答案的人们鼓掌吧,这就是传说中的底层劳动人民!

 

       故事讲到这里先暂停一下,我们现在重新来分析一下这个故事,让我们对动态规划有个理性认识。

 

       子问题:

       国王需要根据两个大臣的答案以及第9座金矿的信息才能判断出最多能够开采出多少金子。为了解决自己面临的问题,他需要给别人制造另外两个问题,这两个问题就是子问题。

 

       思考动态规划的第一点----最优子结构:

       国王相信,只要他的两个大臣能够回答出正确的答案(对于考虑能够开采出的金子数,最多的也就是最优的同时也就是正确的),再加上他的聪明的判断就一定能得到最终的正确答案。我们把这种子问题最优时母问题通过优化选择后一定最优的情况叫做“最优子结构”。

 

       思考动态规划的第二点----子问题重叠:

       实际上国王也好,大臣也好,所有人面对的都是同样的问题,即给你一定数量的人,给你一定数量的金矿,让你求出能够开采出来的最多金子数。我们把这种母问题与子问题本质上是同一个问题的情况称为“子问题重叠”。然而问题中出现的不同点往往就是被子问题之间传递的参数,比如这里的人数和金矿数。

      

       思考动态规划的第三点----边界:

       想想如果不存在前面我们提到的那些底层劳动者的话这个问题能解决吗?永远都不可能!我们把这种子问题在一定时候就不再需要提出子子问题的情况叫做边界,没有边界就会出现死循环。

 

       思考动态规划的第四点----子问题独立:

       要知道,当国王的两个大臣在思考他们自己的问题时他们是不会关心对方是如何计算怎样开采金矿的,因为他们知道,国王只会选择两个人中的一个作为最后方案,另一个人的方案并不会得到实施,因此一个人的决定对另一个人的决定是没有影响的。我们把这种一个母问题在对子问题选择时,当前被选择的子问题两两互不影响的情况叫做“子问题独立”。

 

 

       这就是动态规划,具有“最优子结构”、“子问题重叠”、“边界”和“子问题独立”,当你发现你正在思考的问题具备这四个性质的话,那么恭喜你,你基本上已经找到了动态规划的方法。

 

       有了上面的这几点,我们就可以写出动态规划的转移方程式,现在我们来写出对应这个问题的方程式,如果用gold[mineNum]表示第mineNum个金矿能够挖出的金子数,用peopleNeeded[mineNum]表示挖第mineNum个金矿需要的人数,用函数f(people,mineNum)表示当有people个人和编号为0、1、2、3、……、mineNum的金矿时能够得到的最大金子数的话,f(people,mineNum)等于什么呢?或者说f(people,mineNum)的转移方程是怎样的呢?

 

       答案是:

 当mineNum = 0且people >= peopleNeeded[mineNum]时 f(people,mineNum) = gold[mineNum]

       当mineNum = 0且people < peopleNeeded[mineNum]时 f(people,mineNum) = 0

       当mineNum != 0时 f(people,mineNum) = f(people-peopleNeeded[mineNum], mineNum-1) + gold[mineNum]与f(people, mineNum-1)中的较大者,前两个式子对应动态规划的“边界”,后一个式子对应动态规划的“最优子结构”请读者弄明白后再继续往下看。

 

 

 

 

 

 

----第二节----动态规划的优点--------

      

       现在我假设读者你已经搞清楚了为什么动态规划是正确的方法,但是我们为什么需要使用动态规划呢?请先继续欣赏这个故事:

 

       国王得知他的两个手下使用了和他相同的方法去解决交代给他们的问题后,不但没有认为他的两个大臣在偷懒,反而很高兴,因为他知道,他的大臣必然会找更多的人一起解决这个问题,而更多的人会找更更多的人,这样他这个聪明的方法就会在不经意间流传开来,而全国人民都会知道这个聪明的方法是他们伟大的国王想出来的,你说国王能不高兴吗?

 

       但是国王也有一些担忧,因为他实在不知道这个“工程”要动用到多少人来完成,如果帮助他解决这个问题的人太多的话那么就太劳民伤财了。“会不会影响到今年的收成呢?”国王在心里想着这个问题,于是他请来了整个国家里唯一的两个数学天才,一个叫做小天,另一个叫做小才。

 

       国王问小天:“小天啊,我发觉这个问题有点严重,我知道其实这可以简单的看成一个组合问题,也就是从十个金矿中选取若干个金矿进行开采,看看哪种组合得到的金子最多,也许用组合方法会更好一些。你能告诉我一共有多少种组合情况吗?”

 

       “国王陛下,如果用组合方法的话一共要考虑2的10次方种情况,也就是1024种情况。”小天思考了一会回答到。

 

       “嗯……,如果每一种情况我交给一个人去计算能得到的金子数的话,那我也要1024个人,其实还是挺多的。”国王好像再次感觉到了自己的方法是正确的。

 

       国王心理期待着小才能够给它一个更好的答案,问到:“小才啊,那么你能告诉我用我的那个方法总共需要多少人吗?其实,我也计算过,好像需要的人数是1+2+4+8+16+32+64+……,毕竟每一个人的确都需要找另外两个人来帮助他们……”

 

       不辜负国王的期待,小才微笑着说到:“亲爱的国王陛下,其实我们并不需要那么多人,因为有很多问题其实是相同的,而我们只需要为每一个不同的问题使用一个人力便可。”

 

       国王高兴的问到:“此话如何讲?”

 

       “打个比方,如果有一个人需要知道1000个人和3个金矿可以开采出多少金子,同时另一个人也需要知道1000个人和3个金矿可以开采出多少金子的话,那么他们可以去询问相同的一个人,而不用各自找不同的人浪费人力了。”

      

       国王思考着说到:“嗯,很有道理,如果问题是一样的话那么就不需要去询问两个不同的人了,也就是说一个不同的问题仅需要一个人力,那么一共有多少个不同的问题呢?”   

 

       “因为每个问题的人数可以从0取到10000,而金矿数可以从0取到10,所以最多大约有10000 * 10 等于100000个不同的问题。” 小才一边算着一边回答。

 

       “什么?十万个问题?十万个人力?”国王有点失望。

 

       “请国王放心,事实上我们需要的人力远远小于这个数的,因为不是每一个问题都会遇到,也许我们仅需要一、两百个人力就可以解决这个问题了,这主要和各个金矿所需要的人数有关。” 小才立刻回答到。

 

       故事的最后,自然是国王再一次向他的臣民们证明了他是这个国家里最聪明的人,现在我们通过故事的第二部分来考虑动态规划的另外两个思考点。

 

       思考动态规划的第五点----做备忘录:

       正如上面所说的一样,当我们遇到相同的问题时,我们可以问同一个人。讲的通俗一点就是,我们可以把问题的解放在一个变量中,如果再次遇到这个问题就直接从变量中获得答案,因此每一个问题仅会计算一遍,如果不做备忘的话,动态规划就没有任何优势可言了。             

 

       思考动态规划的第六点----时间分析:

       正如上面所说,如果我们用穷举的方法,至少需要2^n个常数时间,因为总共有2^n种情况需要考虑,如果在背包问题中,包的容量为1000,物品数为100,那么需要考虑2^100种情况,这个数大约为10的30次方。

 

       而如果用动态规划,最多大概只有1000*100 = 100000个不同的问题,这和10的30次方比起来优势是很明显的。而实际情况并不会出现那么多不同的问题,比如在金矿模型中,如果所有的金矿所需人口都是1000个人,那么问题总数大约只有100个。

 

       非正式地,我们可以很容易得到动态规划所需时间,如果共有questionCount个相同的子问题,而每一个问题需要面对chooseCount种选择时,我们所需时间就为questionCount * chooseCount个常数。在金矿模型中,子问题最多有大概people * n 个(其中people是用于开采金矿的总人数,n是金矿的总数),因此questionCount = people * n,而就像国王需要考虑是采用左部下的结果还是采用右部下的结果一样,每个问题面对两个选择,因此chooseCount = 2,所以程序运行时间为 T = O(questionCount * chooseCount) =O(people * n),别忘了实际上需要的时间小于这个值,根据所遇到的具体情况有所不同。

 

       这就是动态规划的魔力,它减少了大量的计算,因此我们需要动态规划!

 

                    

 

 

      

 

----第三节----动态规划的思考角度----------

      

       那么什么是动态规划呢?我个人觉得,如果一个解决问题的方法满足上面六个思考点中的前四个,那么这个方法就属于动态规划。而在思考动态规划方法时,后两点同样也是需要考虑的。

 

       面对问题要寻找动态规划的方法,首先要清楚一点,动态规划不是算法,它是一种方法,它是在一件事情发生的过程中寻找最优值的方法,因此,我们需要对这件事情所发生的过程进行考虑。而通常我们从过程的最后一步开始考虑,而不是先考虑过程的开始。

 

       打个比方,上面的挖金矿问题,我们可以认为整个开采过程是从西至东进行开采的(也就是从第0座开始),那么总有面对最后一座金矿的时候(第9座),对这座金矿不外乎两个选择,开采与不开采,在最后一步确定时再去确定倒数第二步,直到考虑第0座金矿(过程的开始)。

 

       而过程的开始,也就是考虑的最后一步,就是边界。

 

       因此在遇到一个问题想用动态规划的方法去解决时,不妨先思考一下这个过程是怎样的,然后考虑过程的最后一步是如何选择的,通常我们需要自己去构造一个过程,比如后面的练习。

 

 

 

 

 

 

 

----第四节----总结-------

 

       那么遇到问题如何用动态规划去解决呢?根据上面的分析我们可以按照下面的步骤去考虑:

 

       1、构造问题所对应的过程。

       2、思考过程的最后一个步骤,看看有哪些选择情况。

       3、找到最后一步的子问题,确保符合“子问题重叠”,把子问题中不相同的地方设置为参数。

       4、使得子问题符合“最优子结构”。

       5、找到边界,考虑边界的各种处理方式。

       6、确保满足“子问题独立”,一般而言,如果我们是在多个子问题中选择一个作为实施方案,而不会同时实施多个方案,那么子问题就是独立的。

       7、考虑如何做备忘录。

       8、分析所需时间是否满足要求。

       9、写出转移方程式。

      

 

 

 

 

----第五节----练习-------

 

       题目一:买书

       有一书店引进了一套书,共有3卷,每卷书定价是60元,书店为了搞促销,推出一个活动,活动如下:

      

       如果单独购买其中一卷,那么可以打9.5折。

       如果同时购买两卷不同的,那么可以打9折。

       如果同时购买三卷不同的,那么可以打8.5折。

      

       如果小明希望购买第1卷x本,第2卷y本,第3卷z本,那么至少需要多少钱呢?(x、y、z为三个已知整数)。

 

       当然,这道题完全可以不用动态规划来解,但是现在我们是要学习动态规划,因此请想想如何用动态规划来做?

 

       答案:

 

       1、过程为一次一次的购买,每一次购买也许只买一本(这有三种方案),或者买两本(这也有三种方案),或者三本一起买(这有一种方案),最后直到买完所有需要的书。

       2、最后一步我必然会在7种购买方案中选择一种,因此我要在7种购买方案中选择一个最佳情况。

       3、子问题是,我选择了某个方案后,如何使得购买剩余的书能用最少的钱?并且这个选择不会使得剩余的书为负数。母问题和子问题都是给定三卷书的购买量,求最少需要用的钱,所以有“子问题重叠”,问题中三个购买量设置为参数,分别为i、j、k。

       4、的确符合。

       5、边界是一次购买就可以买完所有的书,处理方式请读者自己考虑。

       6、每次选择最多有7种方案,并且不会同时实施其中多种,因此方案的选择互不影响,所以有“子问题独立”。

       7、我可以用minMoney[i][j][k]来保存购买第1卷i本,第2卷j本,第3卷k本时所需的最少金钱。

       8、共有x * y * z 个问题,每个问题面对7种选择,时间为:O( x * y * z * 7) =  O( x * y * z )。

       9、用函数MinMoney(i,j,k)来表示购买第1卷i本,第2卷j本,第3卷k本时所需的最少金钱,那么有:

              MinMoney(i,j,k)=min(s1,s2,s3,s4,s5,s6,s7),其中s1,s2,s3,s4,s5,s6,s7分别为对应的7种方案使用的最少金钱:

              s1 = 60 * 0.95 + MinMoney(i-1,j,k)

              s2 = 60 * 0.95 + MinMoney(i,j-1,k)

              s3 = 60 * 0.95 + MinMoney(i,j,k-1)

              s4 = (60 + 60) * 0.9 + MinMoney(i-1,j-1,k)

              s5 = (60 + 60) * 0.9 + MinMoney(i-1,j,k-1)

              s6 = (60 + 60) * 0.9 + MinMoney(i-1,j,k-1)

              s7 = (60 + 60 + 60) * 0.85 + MinMoney(i-1,j-1,k-1)

 

      

      

 

 

----第六节----代码参考------

 

 

       下面提供金矿问题的程序源代码帮助读者理解,并提供测试数据给大家练习。

 

       输入文件名为“beibao.in”,因为这个问题实际上就是背包问题,所以测试数据文件名就保留原名吧。

       输入文件第一行有两个数,第一个是国王可用用来开采金矿的总人数,第二个是总共发现的金矿数。

       输入文件的第2至n+1行每行有两个数,第i行的两个数分别表示第i-1个金矿需要的人数和可以得到的金子数。

 

       输出文件仅一个整数,表示能够得到的最大金子数。

 

       输入样例:

       100 5

       77 92

       22 22

       29 87

       50 46

       99 90

 

       输出样例:

       133

      

       参考代码如下:

/*=========程序信息========对应题目:01背包之金矿模型使用语言:c++使用编译器:Visual Studio 2005.NET使用算法:动态规划算法运行时间:O(people * n) [people是人数,n是金矿数]作者:贵州大学05级 刘永辉 昵称:SDJL编写时间:2008年8月联系QQ:44561907E-Mail:44561907@qq.com获得更多文章请访问我的博客:www.cnblogs.com/sdjl如果发现BUG或有写得不好的地方请发邮件告诉我:)转载请保留此部分信息:)*/#include "stdafx.h"#include <iostream>#include <fstream>using namespace std;const int max_n = 100;//程序支持的最多金矿数const int max_people = 10000;//程序支持的最多人数int n;//金矿数int peopleTotal;//可以用于挖金子的人数int peopleNeed[max_n];//每座金矿需要的人数int gold[max_n];//每座金矿能够挖出来的金子数int maxGold[max_people][max_n];//maxGold[i][j]保存了i个人挖前j个金矿能够得到的最大金子数,等于-1时表示未知//初始化数据 void init(){    ifstream inputFile("beibao.in");    inputFile>>peopleTotal>>n;    for(int i=0; i<n; i++)        inputFile>>peopleNeed[i]>>gold[i];    inputFile.close();                for(int i=0; i<=peopleTotal; i++)        for(int j=0; j<n; j++)            maxGold[i][j] = -1;//等于-1时表示未知 [对应动态规划中的“做备忘录”]        }//获得在仅有people个人和前mineNum个金矿时能够得到的最大金子数,注意“前多少个”也是从0开始编号的int GetMaxGold(int people, int mineNum){    //申明返回的最大金子数    int retMaxGold;    //如果这个问题曾经计算过  [对应动态规划中的“做备忘录”]    if(maxGold[people][mineNum] != -1)    {        //获得保存起来的值        retMaxGold = maxGold[people][mineNum];    }    else if(mineNum == 0)//如果仅有一个金矿时 [对应动态规划中的“边界”]    {        //当给出的人数足够开采这座金矿        if(people >= peopleNeed[mineNum])        {                //得到的最大值就是这座金矿的金子数            retMaxGold = gold[mineNum];        }        else//否则这唯一的一座金矿也不能开采        {            //得到的最大值为0个金子            retMaxGold = 0;        }    }    else if(people >= peopleNeed[mineNum])//如果给出的人够开采这座金矿 [对应动态规划中的“最优子结构”]    {        //考虑开采与不开采两种情况,取最大值        retMaxGold = max(GetMaxGold(people - peopleNeed[mineNum],mineNum -1) + gold[mineNum],                                        GetMaxGold(people,mineNum - 1));    }    else//否则给出的人不够开采这座金矿 [对应动态规划中的“最优子结构”]    {        //仅考虑不开采的情况        retMaxGold  = GetMaxGold(people,mineNum - 1);    }        //做备忘录        maxGold[people][mineNum] = retMaxGold;    return retMaxGold;}int _tmain(int argc, _TCHAR* argv[]){    //初始化数据    init();    //输出给定peopleTotal个人和n个金矿能够获得的最大金子数,再次提醒编号从0开始,所以最后一个金矿编号为n-1    cout<<GetMaxGold(peopleTotal,n-1);    system("pause");    return 0;}

我自己也写了一点理解,放在代码注释里。

#include<iostream> using namespace std; int main() { int p,n; int np[200];  // need people  int gg[200];  // get gode;... int dp[200][200]; while(cin>>p>>n) { for(int i = 1;i <= n;i ++ ){ cin>>np[i]>>gg[i]; } for(int i = 1; i <= n; i ++){ for(int j = p; j >= 0; j--){ if(j == 0) dp[i][j]=0; else if(j>=np[i]) dp[i][j]=max(dp[i-1][j-np[i]]+gg[i],dp[i-1][j]);  //站在第i座金矿上,手里有j个人,此时 要想知道自己能开采的金矿最大数就要问自己的左右大臣,给左大臣开采次金矿的人数去开采前面的金矿,给右大臣不开采此金矿的人数去开采前面的金矿  else dp[i][j]=dp[i-1][j];  //开采当前的金矿所需的人数不够,那么此金矿肯定不开采,只需让一个人拿着现有的人去开采前面的金矿就行了。  } } cout<<dp[n][p]<<endl;  } return 0; }

简介(入门)

什么是动态规划,我们要如何描述它?

动态规划算法通常基于一个递推公式及一个或多个初始状态。 当前子问题的解将由上一次子问题的解推出。使用动态规划来解题只需要多项式时间复杂度, 因此它比回溯法、暴力法等要快许多。

现在让我们通过一个例子来了解一下DP的基本原理。

首先,我们要找到某个状态的最优解,然后在它的帮助下,找到下一个状态的最优解。

“状态”代表什么及如何找到它?

“状态"用来描述该问题的子问题的解。原文中有两段作者阐述得不太清楚,跳过直接上例子。

如果我们有面值为1元、3元和5元的硬币若干枚,如何用最少的硬币凑够11元? (表面上这道题可以用贪心算法,但贪心算法无法保证可以求出解,比如1元换成2元的时候)

首先我们思考一个问题,如何用最少的硬币凑够i元(i<11)?为什么要这么问呢? 两个原因:1.当我们遇到一个大问题时,总是习惯把问题的规模变小,这样便于分析讨论。 2.这个规模变小后的问题和原来的问题是同质的,除了规模变小,其它的都是一样的, 本质上它还是同一个问题(规模变小后的问题其实是原问题的子问题)。

好了,让我们从最小的i开始吧。当i=0,即我们需要多少个硬币来凑够0元。 由于1,3,5都大于0,即没有比0小的币值,因此凑够0元我们最少需要0个硬币。 (这个分析很傻是不是?别着急,这个思路有利于我们理清动态规划究竟在做些什么。) 这时候我们发现用一个标记来表示这句“凑够0元我们最少需要0个硬币。”会比较方便, 如果一直用纯文字来表述,不出一会儿你就会觉得很绕了。那么, 我们用d(i)=j来表示凑够i元最少需要j个硬币。于是我们已经得到了d(0)=0, 表示凑够0元最小需要0个硬币。当i=1时,只有面值为1元的硬币可用, 因此我们拿起一个面值为1的硬币,接下来只需要凑够0元即可,而这个是已经知道答案的, 即d(0)=0。所以,d(1)=d(1-1)+1=d(0)+1=0+1=1。当i=2时, 仍然只有面值为1的硬币可用,于是我拿起一个面值为1的硬币, 接下来我只需要再凑够2-1=1元即可(记得要用最小的硬币数量),而这个答案也已经知道了。 所以d(2)=d(2-1)+1=d(1)+1=1+1=2。一直到这里,你都可能会觉得,好无聊, 感觉像做小学生的题目似的。因为我们一直都只能操作面值为1的硬币!耐心点, 让我们看看i=3时的情况。当i=3时,我们能用的硬币就有两种了:1元的和3元的( 5元的仍然没用,因为你需要凑的数目是3元!5元太多了亲)。 既然能用的硬币有两种,我就有两种方案。如果我拿了一个1元的硬币,我的目标就变为了: 凑够3-1=2元需要的最少硬币数量。即d(3)=d(3-1)+1=d(2)+1=2+1=3。 这个方案说的是,我拿3个1元的硬币;第二种方案是我拿起一个3元的硬币, 我的目标就变成:凑够3-3=0元需要的最少硬币数量。即d(3)=d(3-3)+1=d(0)+1=0+1=1. 这个方案说的是,我拿1个3元的硬币。好了,这两种方案哪种更优呢? 记得我们可是要用最少的硬币数量来凑够3元的。所以, 选择d(3)=1,怎么来的呢?具体是这样得到的:d(3)=min{d(3-1)+1, d(3-3)+1}。

OK,码了这么多字讲具体的东西,让我们来点抽象的。从以上的文字中, 我们要抽出动态规划里非常重要的两个概念:状态和状态转移方程。

上文中d(i)表示凑够i元需要的最少硬币数量,我们将它定义为该问题的"状态", 这个状态是怎么找出来的呢?我在另一篇文章 动态规划之背包问题(一)中写过: 根据子问题定义状态。你找到子问题,状态也就浮出水面了。 最终我们要求解的问题,可以用这个状态来表示:d(11),即凑够11元最少需要多少个硬币。 那状态转移方程是什么呢?既然我们用d(i)表示状态,那么状态转移方程自然包含d(i), 上文中包含状态d(i)的方程是:d(3)=min{d(3-1)+1, d(3-3)+1}。没错, 它就是状态转移方程,描述状态之间是如何转移的。当然,我们要对它抽象一下,

d(i)=min{ d(i-vj)+1 },其中i-vj >=0,vj表示第j个硬币的面值;

有了状态和状态转移方程,这个问题基本上也就解决了。当然了,Talk is cheap,show me the code!

伪代码如下:

下图是当i从0到11时的解:

从上图可以得出,要凑够11元至少需要3枚硬币。

此外,通过追踪我们是如何从前一个状态值得到当前状态值的, 可以找到每一次我们用的是什么面值的硬币。比如,从上面的图我们可以看出, 最终结果d(11)=d(10)+1(面值为1),而d(10)=d(5)+1(面值为5),最后d(5)=d(0)+1 (面值为5)。所以我们凑够11元最少需要的3枚硬币是:1元、5元、5元。

注意:原文中这里本来还有一段的,但我反反复复读了几遍, 大概的意思我已经在上文从i=0到i=3的分析中有所体现了。作者本来想讲的通俗一些, 结果没写好,反而更不好懂,所以这段不翻译了。

初级

上面讨论了一个非常简单的例子。现在让我们来看看对于更复杂的问题, 如何找到状态之间的转移方式(即找到状态转移方程)。 为此我们要引入一个新词叫递推关系来将状态联系起来(说的还是状态转移方程)

OK,上例子,看看它是如何工作的。

一个序列有N个数:A[1],A[2],…,A[N],求出最长非降子序列的长度。 (讲DP基本都会讲到的一个问题LIS:longest increasing subsequence)

正如上面我们讲的,面对这样一个问题,我们首先要定义一个“状态”来代表它的子问题, 并且找到它的解。注意,大部分情况下,某个状态只与它前面出现的状态有关, 而独立于后面的状态。

让我们沿用“入门”一节里那道简单题的思路来一步步找到“状态”和“状态转移方程”。 假如我们考虑求A[1],A[2],…,A[i]的最长非降子序列的长度,其中i<N, 那么上面的问题变成了原问题的一个子问题(问题规模变小了,你可以让i=1,2,3等来分析) 然后我们定义d(i),表示前i个数中以A[i]结尾的最长非降子序列的长度。OK, 对照“入门”中的简单题,你应该可以估计到这个d(i)就是我们要找的状态。 如果我们把d(1)到d(N)都计算出来,那么最终我们要找的答案就是这里面最大的那个。 状态找到了,下一步找出状态转移方程。

为了方便理解我们是如何找到状态转移方程的,我先把下面的例子提到前面来讲。 如果我们要求的这N个数的序列是:

5,3,4,8,6,7

根据上面找到的状态,我们可以得到:(下文的最长非降子序列都用LIS表示)

  • 前1个数的LIS长度d(1)=1(序列:5)
  • 前2个数的LIS长度d(2)=1(序列:3;3前面没有比3小的)
  • 前3个数的LIS长度d(3)=2(序列:3,4;4前面有个比它小的3,所以d(3)=d(2)+1)
  • 前4个数的LIS长度d(4)=3(序列:3,4,8;8前面比它小的有3个数,所以 d(4)=max{d(1),d(2),d(3)}+1=3)

OK,分析到这,我觉得状态转移方程已经很明显了,如果我们已经求出了d(1)到d(i-1), 那么d(i)可以用下面的状态转移方程得到:

d(i) = max{1, d(j)+1},其中j<i,A[j]<=A[i]

用大白话解释就是,想要求d(i),就把i前面的各个子序列中, 最后一个数不大于A[i]的序列长度加1,然后取出最大的长度即为d(i)。 当然了,有可能i前面的各个子序列中最后一个数都大于A[i],那么d(i)=1, 即它自身成为一个长度为1的子序列。

分析完了,上图:(第二列表示前i个数中LIS的长度, 第三列表示,LIS中到达当前这个数的上一个数的下标,根据这个可以求出LIS序列)

Talk is cheap, show me the code:

#include <iostream>using namespace std;int lis(int A[], int n){    int *d = new int[n];    int len = 1;    for(int i=0; i<n; ++i){        d[i] = 1;        for(int j=0; j<i; ++j)            if(A[j]<=A[i] && d[j]+1>d[i])                d[i] = d[j] + 1;        if(d[i]>len) len = d[i];    }    delete[] d;    return len;}int main(){    int A[] = {        5, 3, 4, 8, 6, 7    };    cout<<lis(A, 6)<<endl;    return 0;}

该算法的时间复杂度是O(n2 ),并不是最优的解法。 还有一种很巧妙的算法可以将时间复杂度降到O(nlogn),网上已经有各种文章介绍它, 这里就不再赘述。传送门: LIS的O(nlogn)解法。 此题还可以用“排序+LCS”来解,感兴趣的话可自行Google。

练习题

无向图G有N个结点(1<N<=1000)及一些边,每一条边上带有正的权重值。 找到结点1到结点N的最短路径,或者输出不存在这样的路径。

提示:在每一步中,对于那些没有计算过的结点, 及那些已经计算出从结点1到它的最短路径的结点,如果它们间有边, 则计算从结点1到未计算结点的最短路径。

尝试解决以下来自topcoder竞赛的问题:

  • ZigZag - 2003 TCCC Semifinals 3
  • BadNeighbors - 2004 TCCC Round 4
  • FlowerGarden - 2004 TCCC Round 1

中级

接下来,让我们来看看如何解决二维的DP问题。

平面上有N*M个格子,每个格子中放着一定数量的苹果。你从左上角的格子开始, 每一步只能向下走或是向右走,每次走到一个格子上就把格子里的苹果收集起来, 这样下去,你最多能收集到多少个苹果。

解这个问题与解其它的DP问题几乎没有什么两样。第一步找到问题的“状态”, 第二步找到“状态转移方程”,然后基本上问题就解决了。

首先,我们要找到这个问题中的“状态”是什么?我们必须注意到的一点是, 到达一个格子的方式最多只有两种:从左边来的(除了第一列)和从上边来的(除了第一行)。 因此为了求出到达当前格子后最多能收集到多少个苹果, 我们就要先去考察那些能到达当前这个格子的格子,到达它们最多能收集到多少个苹果。 (是不是有点绕,但这句话的本质其实是DP的关键:欲求问题的解,先要去求子问题的解)

经过上面的分析,很容易可以得出问题的状态和状态转移方程。 状态S[i][j]表示我们走到(i, j)这个格子时,最多能收集到多少个苹果。那么, 状态转移方程如下:

S[i][j]=A[i][j] + max(S[i-1][j], if i>0 ; S[i][j-1], if j>0)

其中i代表行,j代表列,下标均从0开始;A[i][j]代表格子(i, j)处的苹果数量。

S[i][j]有两种计算方式:1.对于每一行,从左向右计算,然后从上到下逐行处理;2. 对于每一列,从上到下计算,然后从左向右逐列处理。 这样做的目的是为了在计算S[i][j]时,S[i-1][j]和S[i][j-1]都已经计算出来了。

伪代码如下:

以下两道题来自topcoder,练习用的。

  • AvoidRoads - 2003 TCO Semifinals 4
  • ChessMetric - 2003 TCCC Round 4

中高级

这一节要讨论的是带有额外条件的DP问题。

以下的这个问题是个很好的例子。

无向图G有N个结点,它的边上带有正的权重值。

你从结点1开始走,并且一开始的时候你身上带有M元钱。如果你经过结点i, 那么你就要花掉S[i]元(可以把这想象为收过路费)。如果你没有足够的钱, 就不能从那个结点经过。在这样的限制条件下,找到从结点1到结点N的最短路径。 或者输出该路径不存在。如果存在多条最短路径,那么输出花钱数量最少的那条。 限制:1<N<=100 ; 0<=M<=100 ; 对于每个i,0<=S[i]<=100;正如我们所看到的, 如果没有额外的限制条件(在结点处要收费,费用不足还不给过),那么, 这个问题就和经典的迪杰斯特拉问题一样了(找到两结点间的最短路径)。 在经典的迪杰斯特拉问题中, 我们使用一个一维数组来保存从开始结点到每个结点的最短路径的长度, 即M[i]表示从开始结点到结点i的最短路径的长度。然而在这个问题中, 我们还要保存我们身上剩余多少钱这个信息。因此,很自然的, 我们将一维数组扩展为二维数组。M[i][j]表示从开始结点到结点i的最短路径长度, 且剩余j元。通过这种方式,我们将这个问题规约到原始的路径寻找问题。 在每一步中,对于已经找到的最短路径,我们找到它所能到达的下一个未标记状态(i,j), 将它标记为已访问(之后不再访问这个结点),并且在能到达这个结点的各个最短路径中, 找到加上当前边权重值后最小值对应的路径,即为该结点的最短路径。 (写起来真是绕,建议画个图就会明了很多)。不断重复上面的步骤, 直到所有的结点都访问到为止(这里的访问并不是要求我们要经过它, 比如有个结点收费很高,你没有足够的钱去经过它,但你已经访问过它) 最后Min[N-1][j]中的最小值即是问题的答案(如果有多个最小值, 即有多条最短路径,那么选择j最大的那条路径,即,使你剩余钱数最多的最短路径)。

伪代码:

下面有几道topcoder上的题以供练习:

  • Jewelry - 2003 TCO Online Round 4
  • StripePainter - SRM 150 Div 1
  • QuickSums - SRM 197 Div 2
  • ShortPalindromes - SRM 165 Div 2

高级

以下问题需要仔细的揣摩才能将其规约为可用DP解的问题。

问题:StarAdventure - SRM 208 Div 1:

给定一个M行N列的矩阵(M*N个格子),每个格子中放着一定数量的苹果。 你从左上角的格子开始,只能向下或向右走,目的地是右下角的格子。 你每走过一个格子,就把格子上的苹果都收集起来。然后你从右下角走回左上角的格子, 每次只能向左或是向上走,同样的,走过一个格子就把里面的苹果都收集起来。 最后,你再一次从左上角走到右下角,每过一个格子同样要收集起里面的苹果 (如果格子里的苹果数为0,就不用收集)。求你最多能收集到多少苹果。

注意:当你经过一个格子时,你要一次性把格子里的苹果都拿走。

限制条件:1 < N, M <= 50;每个格子里的苹果数量是0到1000(包含0和1000)。

如果我们只需要从左上角的格子走到右下角的格子一次,并且收集最大数量的苹果, 那么问题就退化为“中级”一节里的那个问题。将这里的问题规约为“中级”里的简单题, 这样一来会比较好解。让我们来分析一下这个问题,要如何规约或是修改才能用上DP。 首先,对于第二次从右下角走到左上角得出的这条路径, 我们可以将它视为从左上角走到右下角得出的路径,没有任何的差别。 (即从B走到A的最优路径和从A走到B的最优路径是一样的)通过这种方式, 我们得到了三条从顶走到底的路径。对于这一点的理解可以稍微减小问题的难度。 于是,我们可以将这3条路径记为左,中,右路径。对于两条相交路径(如下图):

在不影响结果的情况下,我们可以将它们视为两条不相交的路径:

这样一来,我们将得到左,中,右3条路径。此外,如果我们要得到最优解, 路径之间不能相交(除了左上角和右下角必然会相交的格子)。因此对于每一行y( 除了第一行和最后一行),三条路径对应的x坐标要满足:x1[y] < x2[y] < x3[y]。 经过这一步的分析,问题的DP解法就进一步地清晰了。让我们考虑行y, 对于每一个x1[y-1],x2[y-1]和x3[y-1],我们已经找到了能收集到最多苹果数量的路径。 根据它们,我们能求出行y的最优解。现在我们要做的就是找到从一行移动到下一行的方式。 令Max[i][j][k]表示到第y-1行为止收集到苹果的最大数量, 其中3条路径分别止于第i,j,k列。对于下一行y,对每个Max[i][j][k] 都加上格子(y,i),(y,j)和(y,k)内的苹果数量。因此,每一步我们都向下移动。 我们做了这一步移动之后,还要考虑到,一条路径是有可能向右移动的。 (对于每一个格子,我们有可能是从它上面向下移动到它, 也可能是从它左边向右移动到它)。为了保证3条路径互不相交, 我们首先要考虑左边的路径向右移动的情况,然后是中间,最后是右边的路径。 为了更好的理解,让我们来考虑左边的路径向右移动的情况,对于每一个可能的j,k对(j<k), 对每个i(i<j),考虑从位置(i-1,j,k)移动到位置(i,j,k)。处理完左边的路径, 再处理中间的路径,最后处理右边的路径。方法都差不多。

用于练习的topcoder题目:

  • MiniPaint - SRM 178 Div 1

其它

当阅读一个题目并且开始尝试解决它时,首先看一下它的限制。 如果要求在多项式时间内解决,那么该问题就很可能要用DP来解。遇到这种情况, 最重要的就是找到问题的“状态”和“状态转移方程”。(状态不是随便定义的, 一般定义完状态,你要找到当前状态是如何从前面的状态得到的, 即找到状态转移方程)如果看起来是个DP问题,但你却无法定义出状态, 那么试着将问题规约到一个已知的DP问题(正如“高级”一节中的例子一样)。

后记

看完这教程离DP专家还差得远,好好coding才是王道。

收集这些只是为了以后可以复习,不做商业用途。。。。
0 0