2020/2/261第九课动态规划(I)综合实践考核数字三角形1、问题描述上图给出了一个数字三角形。从三角形的顶部到底部有很多条不同的路径。对于每条路径,把路径上面的数加起来可以得到一个和,和最大的路径称为最佳路径。你的任务就是求出最佳路径上的数字之和。注意:路径上的每一步只能从一个数走到下一层上和它最近的左边的数或者右边的数。问题描述输入数据输入的第一行是一个整数N(1N=100),给出三角形的行数。下面的N行给出数字三角形。数字三角形上的数的范围都在0和100之间。输出要求输出最大的和。问题描述输入样例5738810274445265输出样例302、解题思路这道题目可以用递归的方法解决。基本思路是:以D(r,j)表示第r行第j个数字(r,j都从1开始算),以MaxSum(r,j)代表从第r行的第j个数字到底边的最佳路径的数字之和,则本题是要求MaxSum(1,1)。从某个D(r,j)出发,显然下一步只能走D(r+1,j)或者D(r+1,j+1)。如果走D(r+1,j),那么得到的MaxSum(r,j)就是MaxSum(r+1,j)+D(r,j);如果走D(r+1,j+1),那么得到的MaxSum(r,j)就是MaxSum(r+1,j+1)+D(r,j)。所以,选择往哪里走,就看MaxSum(r+1,j)和MaxSum(r+1,j+1)哪个更大了。3、参考程序I#includestdio.h#defineMAX_NUM100intD[MAX_NUM+10][MAX_NUM+10];intN;intMaxSum(intr,intj){if(r==N)returnD[r][j];intnSum1=MaxSum(r+1,j);intnSum2=MaxSum(r+1,j+1);if(nSum1nSum2)returnnSum1+D[r][j];returnnSum2+D[r][j];}3、参考程序Iintmain(void){intm;scanf(%d,&N);for(inti=1;i=N;i++)for(intj=1;j=i;j++)scanf(%d,&D[i][j]);printf(%d,MaxSum(1,1));return0;}提交结果:TimeLimitExceed程序I分析上面的程序,效率非常低,在N值并不大,比如N=100的时候,就慢得几乎永远算不出结果了。为什么会这样呢?是因为过多的重复计算。我们不妨将对MaxSum函数的一次调用称为一次计算。那么,每次计算MaxSum(r,j)的时候,都要计算一次MaxSum(r+1,j+1),而每次计算MaxSum(r,j+1)的时候,也要计算一次MaxSum(r+1,j+1)。重复计算因此产生。在题目中给出的例子里,如果我们将MaxSum(r,j)被计算的次数都写在位置(r,j),那么就能得到右面的三角形:111121133114641738810274445265程序分析从上图可以看出,最后一行的计算次数总和是16,倒数第二行的计算次数总和是8。不难总结出规律,对于N行的三角形,总的计算次数是2^0+2^1+2^2+…+2^(N-1)=2^N-1。当N=100时,总的计算次数是一个让人无法接受的大数字。既然问题出在重复计算,那么解决的办法,当然就是,一个值一旦算出来,就要记住,以后不必重新计算。即第一次算出MaxSum(r,j)的值时,就将该值存放起来,下次再需要计算MaxSum(r,j)时,直接取用存好的值即可,不必再次调用MaxSum进行函数递归计算了。这样,每个MaxSum(r,j)都只需要计算1次即可,那么总的计算次数(即调用MaxSum函数的次数)就是三角形中的数字总数,即1+2+3+…+N=N(N+1)/2。如何存放计算出来的MaxSum(r,j)值呢?显然,用一个二维数组aMaxSum[N][N]就能解决。aMaxSum[r][j]就存放MaxSum(r,j)的计算结果。下次再需要MaxSum(r,j)的值时,不必再调用MaxSum函数,只需直接取aMaxSum[r][j]的值即可。4、参考程序II#includestdio.h#includememory.h#defineMAX_NUM100intD[MAX_NUM+10][MAX_NUM+10];intN;intaMaxSum[MAX_NUM+10][MAX_NUM+10];intMaxSum(intr,intj){if(r==N)returnD[r][j];if(aMaxSum[r+1][j]==-1)//如果MaxSum(r+1,j)没有计算过aMaxSum[r+1][j]=MaxSum(r+1,j);if(aMaxSum[r+1][j+1]==-1)aMaxSum[r+1][j+1]=MaxSum(r+1,j+1);if(aMaxSum[r+1][j]aMaxSum[r+1][j+1])returnaMaxSum[r+1][j]+D[r][j];returnaMaxSum[r+1][j+1]+D[r][j];}4、参考程序IIintmain(void){intm;scanf(%d,&N);//将aMaxSum全部置成-1,开始时所有的MaxSum(r,j)都没有算过memset(aMaxSum,-1,sizeof(aMaxSum));for(inti=1;i=N;i++)for(intj=1;j=i;j++)scanf(%d,&D[i][j]);printf(%d,MaxSum(1,1));return0;}程序II分析这种将一个问题分解为子问题递归求解,并且将中间结果保存以避免重复计算的办法,就叫做“动态规划”。动态规划通常用来求最优解,能用动态规划解决的求最优解问题,必须满足,最优解的每个局部解也都是最优的。以上题为例,最佳路径上面的每个数字到底部的那一段路径,都是从该数字出发到达到底部的最佳路径。实际上,递归的思想在编程时未必要实现为递归函数。在上面的例子里,有递推公式:D[r][j]rNaMaxSum[r][j]Max(aMaxSum[r1][j],aMaxSum[r1][j1])D[r][j]其他因此,不需要写递归函数,从aMaxSum[N-1]这一行元素开始向上逐行递推,就能求得aMaxSum[1][1]的值了。5、参考程序IIIintD[MAX_NUM+10][MAX_NUM+10];intN;intaMaxSum[MAX_NUM+10][MAX_NUM+10];intmain(void){inti,j;scanf(%d,&N);for(i=1;i=N;i++)for(j=1;j=i;j++)scanf(%d,&D[i][j]);for(j=1;j=N;j++)aMaxSum[N][j]=D[N][j];for(i=N;i1;i--)for(j=1;ji;j++){if(aMaxSum[i][j]aMaxSum[i][j+1])aMaxSum[i-1][j]=aMaxSum[i][j]+D[i-1][j];elseaMaxSum[i-1][j]=aMaxSum[i][j+1]+D[i-1][j];}printf(%d,aMaxSum[1][1]);return0;}思考题:上面的几个程序只算出了最佳路径的数字之和。如果要求输出最佳路径上的每个数字,该怎么办?动态规划解题的一般思路许多求最优解的问题可以用动态规划来解决。首先要把原问题分解为若干个子问题。注意单纯的递归往往会导致子问题被重复计算,用动态规划的方法,子问题的解一旦求出就要被保存,所以每个子问题只需求解一次。子问题经常和原问题形式相似,有时甚至完全一样,只不过规模从原来的n变成了n-1,或从原来的n×m变成了n×(m-1)……等等。找到子问题,就意味着找到了将整个问题逐渐分解的办法。分解下去,直到最底层规模最小的的子问题可以一目了然地看出解。每一层子问题的解决,会导致上一层子问题的解决,逐层向上,就会导致最终整个问题的解决。如果从最底层的子问题开始,自底向上地推导出一个个子问题的解,那么编程的时候就不需要写递归函数。动态规划解题的一般思路用动态规划解题时,将和子问题相关的各个变量的一组取值,称之为一个“状态”。一个“状态”对应于一个或多个子问题,所谓某个“状态”下的“值”,就是这个“状态”所对应的子问题的解。比如数字三角形,子问题就是“从位于(r,j)数字开始,到底边路径的最大和”。这个子问题和两个变量r和j相关,那么一个“状态”,就是r,j的一组取值,即每个数字的位置就是一个“状态”。该“状态”所对应的“值”,就是从该位置的数字开始,到底边的最佳路径上的数字之和。定义出什么是“状态”,以及在该“状态”下的“值”后,就要找出不同的状态之间如何迁移――即如何从一个或多个“值”已知的“状态”,求出另一个“状态”的“值”。状态的迁移可以用递推公式表示,此递推公式也可被称作“状态转移方程”。动态规划解题的一般思路用动态规划解题,如何寻找“子问题”,定义“状态”,“状态转移方程”是什么样的,并没有一定之规,需要具体问题具体分析,题目做多了就会有感觉。甚至,对于同一个问题,分解成子问题的办法可能不止一种,因而“状态”也可以有不同的定义方法。不同的“状态”定义方法可能会导致时间、空间效率上的区别。D[r][j]rNaMaxSum[r][j]Max(aMaxSum[r1][j],aMaxSum[r1][j1])D[r][j]其他最长上升子序列1、问题描述一个数的序列bi,当b1b2...bS的时候,我们称这个序列是上升的。对于给定的一个序列(a1,a2,...,aN),我们可以得到一些上升的子序列(ai1,ai2,...,aiK),这里1=i1i2...iK=N。比如,对于序列(1,7,3,5,9,4,8),有它的一些上升子序列,如(1,7),(3,4,8)等等。这些子序列中最长的长度是4,比如子序列(1,3,5,8).你的任务,就是对于给定的序列,求出最长上升子序列的长度。问题描述输入数据输入的第一行是序列的长度N(1=N=1000)。第二行给出序列中的N个整数,这些整数的取值范围都在0到10000。输出要求最长上升子序列的长度。输入样例71735948输出样例42、解题思路如何把这个问题分解成子问题呢?经过分析,发现“求以ak(k=1,2,3…N)为终点的最长上升子序列的长度”是个好的子问题――这里把一个上升子序列中最右边的那个数,称为该子序列的“终点”。虽然这个子问题和原问题形式上并不完全一样,但是只要这N个子问题都解决了,那么这N个子问题的解中,最大的那个就是整个问题的解。由上所述的子问题只和一个变量相关,就是数字的位置。因此序列中数的位置k就是“状态”,而状态k对应的“值”,就是以ak做为“终点”的最长上升子序列的长度。这个问题的状态一共有N个。状态定义出来后,转移方程就不难想了。2、解题思路假定MaxLen(k)表示以ak做为“终点”的最长上升子序列的长度,那么:MaxLen(1)=1MaxLen(k)=Max{MaxLen(i):1ik且aiak且k≠1}+1这个状态转移方程的意思就是,MaxLen(k)的值,就是在ak左边,“终点”数值小于ak,且长度最大的那个上升子序列的长度再加1。因为ak左边任何“终点”小于ak的子序列,加上ak后就能形成一个更长的上升子序列。实际实现的时候,可以不必编写递归函数,因为从MaxLen(1)就能推算出MaxLen(2),有了MaxLen(1)和MaxLen(2)就能推算出MaxLen(3)……3、参考程序intb[MAX_N+10];intaMaxLen[MAX_N+10];intmain(void){inti,j,N;scanf(%d,&N);for(i=1;i=N;i++)scanf(%d,&b[i]);aMax