动态规划 订阅
动态规划(Dynamic Programming,DP)是运筹学的一个分支,是求解决策过程最优化的过程。20世纪50年代初,美国数学家贝尔曼(R.Bellman)等人在研究多阶段决策过程的优化问题时,提出了著名的最优化原理,从而创立了动态规划。动态规划的应用极其广泛,包括工程技术、经济、工业生产、军事以及自动化控制等领域,并在背包问题、生产经营问题、资金管理问题、资源分配问题、最短路径问题和复杂系统可靠性问题等中取得了显著的效果 [1]  。 展开全文
动态规划(Dynamic Programming,DP)是运筹学的一个分支,是求解决策过程最优化的过程。20世纪50年代初,美国数学家贝尔曼(R.Bellman)等人在研究多阶段决策过程的优化问题时,提出了著名的最优化原理,从而创立了动态规划。动态规划的应用极其广泛,包括工程技术、经济、工业生产、军事以及自动化控制等领域,并在背包问题、生产经营问题、资金管理问题、资源分配问题、最短路径问题和复杂系统可靠性问题等中取得了显著的效果 [1]  。
信息
运    用
求解决策过程(decision process)最优化的数学方法
外文名
dynamic programming
简    称
DP
中文名
动态规划
学    科
运筹学
第一本著作
《Dynamic Programming》
动态规划简介
动态规划问世以来,在经济管理、生产调度、工程技术和最优控制等方面得到了广泛的应用。例如最短路线、库存管理、资源分配、设备更新、排序、装载等问题,用动态规划方法比用其它方法求解更为方便 [2]  。虽然动态规划主要用于求解以时间划分阶段的动态过程的优化问题,但是一些与时间无关的静态规划(如线性规划、非线性规划),只要人为地引进时间因素,把它视为多阶段决策过程,也可以用动态规划方法方便地求解 [2]  。在现实生活中,有一类活动的过程,由于它的特殊性,可将过程分成若干个互相联系的阶段,在它的每一阶段都需要作出决策,从而使整个过程达到最好的活动效果。因此各个阶段决策的选取不能任意确定,它依赖于当前面临的状态,又影响以后的发展。当各个阶段决策确定后,就组成一个决策序列,因而也就确定了整个过程的一条活动路线.这种把一个问题看作是一个前后关联具有链状结构的多阶段过程就称为多阶段决策过程,这种问题称为多阶段决策问题。在多阶段决策问题中,各个阶段采取的决策,一般来说是与时间有关的,决策依赖于当前状态,又随即引起状态的转移,一个决策序列就是在变化的状态中产生出来的,故有“动态”的含义,称这种解决多阶段决策最优化的过程为动态规划方法 [3]  。动态规划算法通常用于求解具有某种最优性质的问题。在这类问题中,可能会有许多可行解。每一个解都对应于一个值,我们希望找到具有最优值的解。动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间。我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划法的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式 [4]  。1. 多阶段决策问题如果一类活动过程可以分为若干个互相联系的阶段,在每一个阶段都需作出决策(采取措施),一个阶段的决策确定以后,常常影响到下一个阶段的决策,从而就完全确定了一个过程的活动路线,则称它为多阶段决策问题 [5]  。各个阶段的决策构成一个决策序列,称为一个策略。每一个阶段都有若干个决策可供选择,因而就有许多策略供我们选取,对应于一个策略可以确定活动的效果,这个效果可以用数量来确定。策略不同,效果也不同,多阶段决策问题,就是要在可以选择的那些策略中间,选取一个最优策略,使在预定的标准下达到最好的效果 [5]  。2.动态规划问题中的术语阶段:把所给求解问题的过程恰当地分成若干个相互联系的阶段,以便于求解,过程不同,阶段数就可能不同.描述阶段的变量称为阶段变量。在多数情况下,阶段变量是离散的,用k表示。此外,也有阶段变量是连续的情形。如果过程可以在任何时刻作出决策,且在任意两个不同的时刻之间允许有无穷多个决策时,阶段变量就是连续的 [5]  。状态:状态表示每个阶段开始面临的自然状况或客观条件,它不以人们的主观意志为转移,也称为不可控因素。在上面的例子中状态就是某阶段的出发位置,它既是该阶段某路的起点,同时又是前一阶段某支路的终点 [5]  。无后效性:我们要求状态具有下面的性质:如果给定某一阶段的状态,则在这一阶段以后过程的发展不受这阶段以前各段状态的影响,所有各阶段都确定时,整个过程也就确定了。换句话说,过程的每一次实现可以用一个状态序列表示,在前面的例子中每阶段的状态是该线路的始点,确定了这些点的序列,整个线路也就完全确定。从某一阶段以后的线路开始,当这段的始点给定时,不受以前线路(所通过的点)的影响。状态的这个性质意味着过程的历史只能通过当前的状态去影响它的未来的发展,这个性质称为无后效性 [5]  。决策:一个阶段的状态给定以后,从该状态演变到下一阶段某个状态的一种选择(行动)称为决策。在最优控制中,也称为控制。在许多问题中,决策可以自然而然地表示为一个数或一组数。不同的决策对应着不同的数值。描述决策的变量称决策变量,因状态满足无后效性,故在每个阶段选择决策时只需考虑当前的状态而无须考虑过程的历史 [5]  。决策变量的范围称为允许决策集合 [5]  。策略:由每个阶段的决策组成的序列称为策略。对于每一个实际的多阶段决策过程,可供选取的策略有一定的范围限制,这个范围称为允许策略集合 [5]  。允许策略集合中达到最优效果的策略称为最优策略 [5]  。给定k阶段状态变量x(k)的值后,如果这一阶段的决策变量一经确定,第k+1阶段的状态变量x(k+1)也就完全确定,即x(k+1)的值随x(k)和第k阶段的决策u(k)的值变化而变化,那么可以把这一关系看成(x(k),u(k))与x(k+1)确定的对应关系,用x(k+1)=Tk(x(k),u(k))表示。这是从k阶段到k+1阶段的状态转移规律,称为状态转移方程 [5]  。最优化原理:作为整个过程的最优策略,它满足:相对前面决策所形成的状态而言,余下的子策略必然构成“最优子策略” [5]  。最优性原理实际上是要求问题的最优策略的子策略也是最优 [5]  。多阶段决策问题中,各个阶段采取的决策,一般来说是与时间有关的,决策依赖于当前状态,又随即引起状态的转移,一个决策序列就是在变化的状态中产生出来的,故有“动态”的含义,称这种解决多阶段决策最优化问题的方法为动态规划方法 [6]  。任何思想方法都有一定的局限性,超出了特定条件,它就失去了作用。同样,动态规划也并不是万能的。适用动态规划的问题必须满足最优化原理和无后效性 [7]  。1、最优化原理(最优子结构性质)最优化原理可这样阐述:一个最优化策略具有这样的性质,不论过去状态和决策如何,对前面的决策所形成的状态而言,余下的诸决策必须构成最优策略。简而言之,一个最优化策略的子策略总是最优的。一个问题满足最优化原理又称其具有最优子结构性质 [7]  。2、无后效性将各阶段按照一定的次序排列好之后,对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。换句话说,每个状态都是过去历史的一个完整总结。这就是无后向性,又称为无后效性 [7]  。3、子问题的重叠性动态规划算法的关键在于解决冗余,这是动态规划算法的根本目的。动态规划实质上是一种以空间换时间的技术,它在实现的过程中,不得不存储产生过程中的各种状态,所以它的空间复杂度要大于其他的算法。选择动态规划算法是因为动态规划算法在空间上可以承受,而搜索算法在时间上却无法承受,所以我们舍空间而取时间 [7]  。
收起全文
精华内容
参与话题
问答
  • 动态规划

    千次阅读 多人点赞 2019-06-30 21:30:12
  • 算法基础入门:90分钟搞定动态规划

    千人学习 2019-12-29 10:11:22
    动态规划(Dynamic programming,简称DP)很多人都觉得是比较难以理解和掌握的一种算法,为了应付面试更多的时候程序员会选择直接死记硬背斐波那楔数列或者背包问题的源码,其实只要认真学习、彻底理解,动态规划并...
  • 教你彻底学会动态规划——入门篇

    万次阅读 多人点赞 2015-08-11 13:26:41
    动态规划相信大家都知道,动态规划算法也是新手在刚接触算法设计时很苦恼的问题,有时候觉得难以理解,但是真正理解之后,就会觉得动态规划其实并没有想象中那么难。网上也有很多关于讲解动态规划的文章,大多都是...

        动态规划相信大家都知道,动态规划算法也是新手在刚接触算法设计时很苦恼的问题,有时候觉得难以理解,但是真正理解之后,就会觉得动态规划其实并没有想象中那么难。网上也有很多关于讲解动态规划的文章,大多都是叙述概念,讲解原理,让人觉得晦涩难懂,即使一时间看懂了,发现当自己做题的时候又会觉得无所适从。我觉得,理解算法最重要的还是在于练习,只有通过自己练习,才可以更快地提升。话不多说,接下来,下面我就通过一个例子来一步一步讲解动态规划是怎样使用的,只有知道怎样使用,才能更好地理解,而不是一味地对概念和原理进行反复琢磨。

        首先,我们看一下这道题(此题目来源于北大POJ):

        数字三角形(POJ1163)

       

        在上面的数字三角形中寻找一条从顶部到底边的路径,使得路径上所经过的数字之和最大。路径上的每一步都只能往左下或 右下走。只需要求出这个最大和即可,不必给出具体路径。 三角形的行数大于1小于等于100,数字为 0 - 99

        输入格式:

        5      //表示三角形的行数    接下来输入三角形

        7

        3   8

        8   1   0

        2   7   4   4

        4   5   2   6   5

        要求输出最大和

        接下来,我们来分析一下解题思路:

        首先,肯定得用二维数组来存放数字三角形

        然后我们用D( r, j) 来表示第r行第 j 个数字(r,j从1开始算)

        我们用MaxSum(r, j)表示从D(r,j)到底边的各条路径中,最佳路径的数字之和。

        因此,此题的最终问题就变成了求 MaxSum(1,1)

        当我们看到这个题目的时候,首先想到的就是可以用简单的递归来解题:

        D(r, j)出发,下一步只能走D(r+1,j)或者D(r+1, j+1)。故对于N行的三角形,我们可以写出如下的递归式:   

    if ( r == N)                
    	MaxSum(r,j) = D(r,j)  
    else      
    	MaxSum( r, j) = Max{ MaxSum(r+1,j), MaxSum(r+1,j+1) } + D(r,j) 

        根据上面这个简单的递归式,我们就可以很轻松地写出完整的递归代码: 

    #include <iostream>  
    #include <algorithm> 
    #define MAX 101  
    using namespace std; 
    int D[MAX][MAX];  
    int n;  
    int MaxSum(int i, int j){    
    	if(i==n)  
    		return D[i][j];    
    	int x = MaxSum(i+1,j);    
    	int y = MaxSum(i+1,j+1);    
    	return max(x,y)+D[i][j];  
    }
    int main(){    
    	int i,j;    
    	cin >> n;    
    	for(i=1;i<=n;i++)   
    		for(j=1;j<=i;j++)        
    			cin >> D[i][j];    
    	cout << MaxSum(1,1) << endl;  
    }      

        对于如上这段递归的代码,当我提交到POJ时,会显示如下结果:

       

        对的,代码运行超时了,为什么会超时呢?

        答案很简单,因为我们重复计算了,当我们在进行递归时,计算机帮我们计算的过程如下图:

        

        就拿第三行数字1来说,当我们计算从第2行的数字3开始的MaxSum时会计算出从1开始的MaxSum,当我们计算从第二行的数字8开始的MaxSum的时候又会计算一次从1开始的MaxSum,也就是说有重复计算。这样就浪费了大量的时间。也就是说如果采用递规的方法,深度遍历每条路径,存在大量重复计算。则时间复杂度为 2的n次方,对于 n = 100 行,肯定超时。 

        接下来,我们就要考虑如何进行改进,我们自然而然就可以想到如果每算出一个MaxSum(r,j)就保存起来,下次用到其值的时候直接取用,则可免去重复计算。那么可以用n方的时间复杂度完成计算。因为三角形的数字总数是 n(n+1)/2

        根据这个思路,我们就可以将上面的代码进行改进,使之成为记忆递归型的动态规划程序: 

    #include <iostream>  
    #include <algorithm> 
    using namespace std;
     
    #define MAX 101
      
    int D[MAX][MAX];    
    int n;  
    int maxSum[MAX][MAX];
     
    int MaxSum(int i, int j){      
    	if( maxSum[i][j] != -1 )         
    		return maxSum[i][j];      
    	if(i==n)   
    		maxSum[i][j] = D[i][j];     
    	else{    
    		int x = MaxSum(i+1,j);       
    		int y = MaxSum(i+1,j+1);       
    		maxSum[i][j] = max(x,y)+ D[i][j];     
    	}     
    	return maxSum[i][j]; 
    } 
    int main(){    
    	int i,j;    
    	cin >> n;    
    	for(i=1;i<=n;i++)   
    		for(j=1;j<=i;j++) {       
    			cin >> D[i][j];       
    			maxSum[i][j] = -1;   
    		}    
    	cout << MaxSum(1,1) << endl; 
    } 

        当我们提交如上代码时,结果就是一次AC

       

        虽然在短时间内就AC了。但是,我们并不能满足于这样的代码,因为递归总是需要使用大量堆栈上的空间,很容易造成栈溢出,我们现在就要考虑如何把递归转换为递推,让我们一步一步来完成这个过程。

        我们首先需要计算的是最后一行,因此可以把最后一行直接写出,如下图:

       

        现在开始分析倒数第二行的每一个数,现分析数字2,2可以和最后一行4相加,也可以和最后一行的5相加,但是很显然和5相加要更大一点,结果为7,我们此时就可以将7保存起来,然后分析数字7,7可以和最后一行的5相加,也可以和最后一行的2相加,很显然和5相加更大,结果为12,因此我们将12保存起来。以此类推。。我们可以得到下面这张图:

       

        然后按同样的道理分析倒数第三行和倒数第四行,最后分析第一行,我们可以依次得到如下结果:

       

       

        上面的推导过程相信大家不难理解,理解之后我们就可以写出如下的递推型动态规划程序: 

    #include <iostream>  
    #include <algorithm> 
    using namespace std; 
    
    #define MAX 101  
    
    int D[MAX][MAX];   
    int n;  
    int maxSum[MAX][MAX]; 
    int main(){    
    	int i,j;    
    	cin >> n;    
    	for(i=1;i<=n;i++)   
    		for(j=1;j<=i;j++)        
    			cin >> D[i][j];   
    	for( int i = 1;i <= n; ++ i )     
    		maxSum[n][i] = D[n][i];   
    	for( int i = n-1; i>= 1;  --i )     
    		for( int j = 1; j <= i; ++j )         
    			maxSum[i][j] = max(maxSum[i+1][j],maxSum[i+1][j+1]) + D[i][j];    
    	cout << maxSum[1][1] << endl;  
    } 

         我们的代码仅仅是这样就够了吗?当然不是,我们仍然可以继续优化,而这个优化当然是对于空间进行优化,其实完全没必要用二维maxSum数组存储每一个MaxSum(r,j),只要从底层一行行向上递推,那么只要一维数组maxSum[100]即可,即只要存储一行的MaxSum值就可以。

         对于空间优化后的具体递推过程如下:

       

       

       

       

       

       

        接下里的步骤就按上图的过程一步一步推导就可以了。进一步考虑,我们甚至可以连maxSum数组都可以不要,直接用D的第n行直接替代maxSum即可。但是这里需要强调的是:虽然节省空间,但是时间复杂度还是不变的。

        依照上面的方式,我们可以写出如下代码:    

     

    #include <iostream>  
    #include <algorithm> 
    using namespace std; 
    
    #define MAX 101  
    
    int D[MAX][MAX];  
    int n; 
    int * maxSum; 
    
    int main(){    
    	int i,j;    
    	cin >> n;    
    	for(i=1;i<=n;i++)   
    		for(j=1;j<=i;j++)        
    			cin >> D[i][j];   
    	maxSum = D[n]; //maxSum指向第n行    
    	for( int i = n-1; i>= 1;  --i )     
    		for( int j = 1; j <= i; ++j )       
    			maxSum[j] = max(maxSum[j],maxSum[j+1]) + D[i][j];    
    	cout << maxSum[1] << endl;  
    }

     

     

     

     

     

        接下来,我们就进行一下总结:

        递归到动规的一般转化方法

        递归函数有n个参数,就定义一个n维的数组,数组的下标是递归函数参数的取值范围,数组元素的值是递归函数的返回值,这样就可以从边界值开始, 逐步填充数组,相当于计算递归函数值的逆过程。

        动规解题的一般思路

        1. 将原问题分解为子问题

    •     把原问题分解为若干个子问题,子问题和原问题形式相同或类似,只不过规模变小了。子问题都解决,原问题即解决(数字三角形例)。
    •     子问题的解一旦求出就会被保存,所以每个子问题只需求 解一次。

        2.确定状态

    •     在用动态规划解题时,我们往往将和子问题相关的各个变量的一组取值,称之为一个“状 态”。一个“状态”对应于一个或多个子问题, 所谓某个“状态”下的“值”,就是这个“状 态”所对应的子问题的解。
    •     所有“状态”的集合,构成问题的“状态空间”。“状态空间”的大小,与用动态规划解决问题的时间复杂度直接相关。 在数字三角形的例子里,一共有N×(N+1)/2个数字,所以这个问题的状态空间里一共就有N×(N+1)/2个状态。

        整个问题的时间复杂度是状态数目乘以计算每个状态所需时间。在数字三角形里每个“状态”只需要经过一次,且在每个状态上作计算所花的时间都是和N无关的常数。

        3.确定一些初始状态(边界状态)的值

        以“数字三角形”为例,初始状态就是底边数字,值就是底边数字值。

        4. 确定状态转移方程

         定义出什么是“状态”,以及在该“状态”下的“值”后,就要找出不同的状态之间如何迁移――即如何从一个或多个“值”已知的 “状态”,求出另一个“状态”的“值”(递推型)。状态的迁移可以用递推公式表示,此递推公式也可被称作“状态转移方程”。

        数字三角形的状态转移方程:

       
     

        能用动规解决的问题的特点

        1) 问题具有最优子结构性质。如果问题的最优解所包含的 子问题的解也是最优的,我们就称该问题具有最优子结 构性质。

        2) 无后效性。当前的若干个状态值一旦确定,则此后过程的演变就只和这若干个状态的值有关,和之前是采取哪种手段或经过哪条路径演变到当前的这若干个状态,没有关系。

     

    好久没看博客发现这篇文章现在已经这么火热了,看了一下评论发现不少人对这篇文章都比较有兴趣,我当初写这篇文章是受到了Coursera上面一门算法课程的启发,大家有兴趣可以去听听这门课程数据结构与算法

     

    展开全文
  • 问题描述 有n个物品,它们有各自的体积和价值,现有给定容量的背包,如何让背包里装入的物品具有最大的价值总和? 为方便讲解和理解,下面讲述的例子均先用具体的数字代入,即:eg:number=4,capacity=8 ...

    问题描述

    有n个物品,它们有各自的体积和价值,现有给定容量的背包,如何让背包里装入的物品具有最大的价值总和?

    为方便讲解和理解,下面讲述的例子均先用具体的数字代入,即:eg:number=4,capacity=8

    i(物品编号) 1 2 3 4
    w(体积) 2 3 4 5
    v(价值) 3 4 5 6

     

    总体思路

    根据动态规划解题步骤(问题抽象化、建立模型、寻找约束条件、判断是否满足最优性原理、找大问题与小问题的递推关系式、填表、寻找解组成)找出01背包问题的最优解以及解组成,然后编写代码实现。

    动态规划的原理

    动态规划与分治法类似,都是把大问题拆分成小问题,通过寻找大问题与小问题的递推关系,解决一个个小问题,最终达到解决原问题的效果。但不同的是,分治法在子问题和子子问题等上被重复计算了很多次,而动态规划则具有记忆性,通过填写表把所有已经解决的子问题答案纪录下来,在新问题里需要用到的子问题可以直接提取,避免了重复计算,从而节约了时间,所以在问题满足最优性原理之后,用动态规划解决问题的核心就在于填表,表填写完毕,最优解也就找到。

    最优性原理是动态规划的基础,最优性原理是指“多阶段决策过程的最优决策序列具有这样的性质:不论初始状态和初始决策如何,对于前面决策所造成的某一状态而言,其后各阶段的决策序列必须构成最优策略”。

    背包问题的解决过程

    在解决问题之前,为描述方便,首先定义一些变量:Vi表示第 i 个物品的价值,Wi表示第 i 个物品的体积,定义V(i,j):当前背包容量 j,前 i 个物品最佳组合对应的价值,同时背包问题抽象化(X1,X2,…,Xn,其中 Xi 取0或1,表示第 i 个物品选或不选)。

    1、建立模型,即求max(V1X1+V2X2+…+VnXn);

    2、寻找约束条件,W1X1+W2X2+…+WnXn<capacity;

    3、寻找递推关系式,面对当前商品有两种可能性:

    • 包的容量比该商品体积小,装不下,此时的价值与前i-1个的价值是一样的,即V(i,j)=V(i-1,j);
    • 还有足够的容量可以装该商品,但装了也不一定达到当前最优价值,所以在装与不装之间选择最优的一个,即V(i,j)=max{V(i-1,j),V(i-1,j-w(i))+v(i)}。

    其中V(i-1,j)表示不装,V(i-1,j-w(i))+v(i) 表示装了第i个商品,背包容量减少w(i),但价值增加了v(i);

    由此可以得出递推关系式:

    • j<w(i)      V(i,j)=V(i-1,j)
    • j>=w(i)     V(i,j)=max{V(i-1,j),V(i-1,j-w(i))+v(i)}

    这里需要解释一下,为什么能装的情况下,需要这样求解(这才是本问题的关键所在!):

    可以这么理解,如果要到达V(i,j)这一个状态有几种方式?

    肯定是两种,第一种是第i件商品没有装进去,第二种是第i件商品装进去了。没有装进去很好理解,就是V(i-1,j);装进去了怎么理解呢?如果装进去第i件商品,那么装入之前是什么状态,肯定是V(i-1,j-w(i))。由于最优性原理(上文讲到),V(i-1,j-w(i))就是前面决策造成的一种状态,后面的决策就要构成最优策略。两种情况进行比较,得出最优。

    4、填表,首先初始化边界条件,V(0,j)=V(i,0)=0;

    然后一行一行的填表:

    • 如,i=1,j=1,w(1)=2,v(1)=3,有j<w(1),故V(1,1)=V(1-1,1)=0;
    • 又如i=1,j=2,w(1)=2,v(1)=3,有j=w(1),故V(1,2)=max{ V(1-1,2),V(1-1,2-w(1))+v(1) }=max{0,0+3}=3;
    • 如此下去,填到最后一个,i=4,j=8,w(4)=5,v(4)=6,有j>w(4),故V(4,8)=max{ V(4-1,8),V(4-1,8-w(4))+v(4) }=max{9,4+6}=10……

    所以填完表如下图:

    5、表格填完,最优解即是V(number,capacity)=V(4,8)=10。

     

    代码实现

    为了和之前的动态规划图可以进行对比,尽管只有4个商品,但是我们创建的数组元素由5个。

    #include<iostream>
    using namespace std;
    #include <algorithm>
    
    int main()
    {
    	int w[5] = { 0 , 2 , 3 , 4 , 5 };			//商品的体积2、3、4、5
    	int v[5] = { 0 , 3 , 4 , 5 , 6 };			//商品的价值3、4、5、6
    	int bagV = 8;					        //背包大小
    	int dp[5][9] = { { 0 } };			        //动态规划表
    
    	for (int i = 1; i <= 4; i++) {
    		for (int j = 1; j <= bagV; j++) {
    			if (j < w[i])
    				dp[i][j] = dp[i - 1][j];
    			else
    				dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);
    		}
    	}
    
    	//动态规划表的输出
    	for (int i = 0; i < 5; i++) {
    		for (int j = 0; j < 9; j++) {
    			cout << dp[i][j] << ' ';
    		}
    		cout << endl;
    	}
    
    	return 0;
    }

     

    背包问题最优解回溯

    通过上面的方法可以求出背包问题的最优解,但还不知道这个最优解由哪些商品组成,故要根据最优解回溯找出解的组成,根据填表的原理可以有如下的寻解方式:

    • V(i,j)=V(i-1,j)时,说明没有选择第i 个商品,则回到V(i-1,j);
    • V(i,j)=V(i-1,j-w(i))+v(i)时,说明装了第i个商品,该商品是最优解组成的一部分,随后我们得回到装该商品之前,即回到V(i-1,j-w(i));
    • 一直遍历到i=0结束为止,所有解的组成都会找到。

    就拿上面的例子来说吧:

    • 最优解为V(4,8)=10,而V(4,8)!=V(3,8)却有V(4,8)=V(3,8-w(4))+v(4)=V(3,3)+6=4+6=10,所以第4件商品被选中,并且回到V(3,8-w(4))=V(3,3);
    • 有V(3,3)=V(2,3)=4,所以第3件商品没被选择,回到V(2,3);
    • 而V(2,3)!=V(1,3)却有V(2,3)=V(1,3-w(2))+v(2)=V(1,0)+4=0+4=4,所以第2件商品被选中,并且回到V(1,3-w(2))=V(1,0);
    • 有V(1,0)=V(0,0)=0,所以第1件商品没被选择。

     

    代码实现

    背包问题最终版详细代码实现如下:

    #include<iostream>
    using namespace std;
    #include <algorithm>
    
    int w[5] = { 0 , 2 , 3 , 4 , 5 };			//商品的体积2、3、4、5
    int v[5] = { 0 , 3 , 4 , 5 , 6 };			//商品的价值3、4、5、6
    int bagV = 8;					        //背包大小
    int dp[5][9] = { { 0 } };			        //动态规划表
    int item[5];					        //最优解情况
    
    void findMax() {					//动态规划
    	for (int i = 1; i <= 4; i++) {
    		for (int j = 1; j <= bagV; j++) {
    			if (j < w[i])
    				dp[i][j] = dp[i - 1][j];
    			else
    				dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - w[i]] + v[i]);
    		}
    	}
    }
    
    void findWhat(int i, int j) {				//最优解情况
    	if (i >= 0) {
    		if (dp[i][j] == dp[i - 1][j]) {
    			item[i] = 0;
    			findWhat(i - 1, j);
    		}
    		else if (j - w[i] >= 0 && dp[i][j] == dp[i - 1][j - w[i]] + v[i]) {
    			item[i] = 1;
    			findWhat(i - 1, j - w[i]);
    		}
    	}
    }
    
    void print() {
    	for (int i = 0; i < 5; i++) {			//动态规划表输出
    		for (int j = 0; j < 9; j++) {
    			cout << dp[i][j] << ' ';
    		}
    		cout << endl;
    	}
    	cout << endl;
    
    	for (int i = 0; i < 5; i++)			//最优解输出
    		cout << item[i] << ' ';
    	cout << endl;
    }
    
    int main()
    {
    	findMax();
    	findWhat(4, 8);
    	print();
    
    	return 0;
    }

     

    展开全文
  • 动态规划!!!动态规划!!!

    千次阅读 多人点赞 2017-08-03 10:35:42
    动态规划

    我一定要把动态规划搞定!!!

    0、动态规划必备知识:

    http://www.hawstein.com/posts/dp-novice-to-advanced.html

    1、最长非降子序列的长度

    (这是属于一维动态规划的问题)

    题目:一个序列有N个数:A[1],A[2],…,A[N],求出最长非降子序列的长度。

    思路:dp[i]表示前i个数中以A[i]结尾的最长非降子序列的长度;想要求dp[i],就把i前面的各个子序列中, 最后一个数不大于A[i]的序列长度加1,然后取出最大的长度即为dp[i]。最有数组dp[]中最大的数,即为最长非降子序列的长度。

    代码:

    import java.util.Scanner;
    
    public class Solution {
    
        public static void main(String[] args) {
    
            Scanner sc = new Scanner(System.in);
            while(sc.hasNext()){
                int n = sc.nextInt();
                int[] A = new int[n];
                for(int i=0; i<n; i++){
                    A[i] = sc.nextInt();
                }
    
                int result = 1;
                //dp[i]表示前i个数中以A[i]结尾的最长非降子序列的长度
                int[] dp = new int[n];
                for(int i=0; i<n; i++){
                    dp[i] = 1;
                    //把i前面的各个子序列中, 最后一个数不大于A[i]的序列长度加1,然后取出最大的长度即为dp[i]
                    for(int j=0; j<i; j++){
                        if(A[j]<A[i]){
                            dp[i] = Math.max(dp[i], dp[j]+1);
                        }
                    }
    
                    result = Math.max(result, dp[i]);
                }
    
                System.out.println(result);
            }
    
        }
    
    }

    2、跳石板

    这里写图片描述

    思路:https://www.nowcoder.com/questionTerminal/4284c8f466814870bae7799a07d49ec8

    代码:

    import java.util.Scanner;
    
    public class Solution {
    
        public static void main(String[] args) {
    
            Scanner sc = new Scanner(System.in);
            while(sc.hasNext()){
                int N = sc.nextInt();
                int M = sc.nextInt();
    
                int[] dp = new int[100001];
                for(int i=N; i<=M; i++){
                    dp[i] = Integer.MAX_VALUE;      //表示不可到达
                }
                dp[N] = 0;
                for(int i=N; i<M; i++){
                    if(dp[i]==Integer.MAX_VALUE){
                        continue;
                    }
                    //计算约数
                    for(int j=2; j*j<=i; j++){
                        if(i%j==0){
                            if((i+j)<=M){
                                dp[i+j] = Math.min(dp[i+j], dp[i]+1);
                            }
    
                            if((i+i/j)<=M){
                                dp[i+i/j] = Math.min(dp[i+i/j], dp[i]+1);
                            }
                        }
                    }
    
                }
    
                int result = -1;
                if(dp[M]!=Integer.MAX_VALUE){
                    result = dp[M];
                }
    
                System.out.println(result);
    
            }
    
        }
    
    }

    3、直方图包含的最大面积

    这里写图片描述

    这里写图片描述
    (参考: http://blog.csdn.net/li563868273/article/details/51121169

    代码:

    public int countArea(int[] A, int n) {
    
            int[][] dp = new int[n][n];
            //初始值
            for(int i=0; i<n; i++){
                dp[i][i] = A[i];
            }
    
            for(int k=1; k<n; k++){
                for(int i=0; (i+k)<n; i++){
    
                    //得到该区间的最小值
                    int min = A[i];
                    for(int j=i; j<=(i+k); j++){
                        min = ((A[j]<min)?A[j]:min);
                    }
    
                    //比较3种情况,得到最大值
                    dp[i][i+k] = Math.max(dp[i+1][i+k], dp[i][i+k-1]);
                    dp[i][i+k] = Math.max(min*(k+1), dp[i][i+k]);
                }
            }
    
            return dp[0][n-1];
    
        }

    4、路径问题

    这里写图片描述

    思路:(参考“必备知识”中的二维DP问题)

    代码:

    public int countPath(int[][] map, int n, int m) {
    
            int startX = 0;
            int startY = 0;
            int endX = 0;
            int endY = 0;
    
            for(int i=0; i<n; i++){
                for(int j=0; j<m; j++){
                    if(map[i][j] == 1){
                        startX = j;
                        startY = i;
                    }else if(map[i][j] == 2){
                        endX = j;
                        endY = i;
                    }
                }
            }
            //确定方向 
            int dirX = (startX<endX ? 1 : -1);
            int dirY = (startY<endY ? 1 : -1);
    
            int[][] count = new int[n][m];
            count[startY][startX] = 1;
    
            //确定竖边界的初始值
            for(int i=startY+dirY; i!=(endY+dirY); i+=dirY){
                if(map[i][startX]==-1){
                    count[i][startX] = 0;
                }else{
                    count[i][startX] = count[i-dirY][startX];  
                }
            }
            //确定横边界的初始值 
            for(int j=startX+dirX; j!=(endX+dirX); j+=dirX){
                if(map[startY][j]==-1){
                    count[startY][j] = -1;
                }else{
                    count[startY][j] = count[startY][j-dirX];
                }
            }
    
    
            for(int i=startY+dirY; i!=(endY+dirY); i+=dirY){
                for(int j=startX+dirX; j!=(endX+dirX); j+=dirX){
    
                    if(map[i][j]==-1){
                        count[i][j] = 0;
                    }else{
                        count[i][j] = count[i-dirY][j] + count[i][j-dirX];
                    }
    
                }
            }
    
            return count[endY][endX];
    
        }

    5、合唱团

    这里写图片描述
    这里写图片描述

    题目来源:网易2017
    https://www.nowcoder.com/practice/661c49118ca241909add3a11c96408c8?tpId=85&tqId=29830&tPage=1&rp=1&ru=/ta/2017test&qru=/ta/2017test/question-ranking

    解题思路:
    http://blog.csdn.net/fcxxzux/article/details/52138964

    
    import java.util.Scanner;
    
    public class Main {
    
        public static void main(String[] args) {
    
            Scanner sc = new Scanner(System.in);
            while(sc.hasNext()){
                int n = sc.nextInt();
                int[] a = new int[n+1];
                for(int i=1; i<=n; i++){
                    a[i] = sc.nextInt();
                }
                int k = sc.nextInt();
                int d = sc.nextInt();
    
                //dpMax[i][j]表示以第i个人为最后一个,已经选择了j个人了
                long[][] dpMax = new long[n+1][k+1];
                long[][] dpMin = new long[n+1][k+1];
    
                long result = 0;
                for(int i=1; i<=n; i++){
                    dpMax[i][1] = dpMin[i][1] = a[i];
                    for(int j=2; j<=k; j++){
    
                        if(i<j){
                            dpMax[i][j] = dpMin[i][j] = 0;
                        }else{
                            for(int m=i-1; m>=Math.max(i-d, 1); m--){
                                dpMax[i][j] = Math.max(dpMax[i][j], Math.max(dpMax[m][j-1]*a[i], dpMin[m][j-1]*a[i]));
                                dpMin[i][j] = Math.min(dpMin[i][j], Math.min(dpMax[m][j-1]*a[i], dpMin[m][j-1]*a[i]));
                            }
                        }
    
                    }
    
                    result = Math.max(result, dpMax[i][k]);
                }
    
                System.out.println(result);
            }
        }
    
    
    }

    6、剪气球

    这里写图片描述

    题目来源:http://exercise.acmcoder.com/quesexcuse?paperId=213

    思路:在计算dp[i+1]时,我们需要考虑第i+1个数可以和前面哪些数分到一起组成连续的子数组。(参考:http://blog.csdn.net/jacky_chenjp/article/details/63684427

    代码:

    import java.util.Scanner;
    
    public class Main {
    
        public static void main(String[] args) {
    
            Scanner sc = new Scanner(System.in);
            while(sc.hasNext()){
                int n = sc.nextInt();
                int[] a = new int[n];
                for(int i=0; i<n; i++){
                    a[i] = sc.nextInt();
                }
    
                int[] dp = new int[n+1];
                dp[0] = 1;
                for(int i=1; i<=n; i++){
                    int[] count = new int[10];
                    for(int j=i-1; j>=0; j--){
                        count[a[j]]++;
                        if(count[a[j]]>1){
                            break;
                        }else{
                            dp[i] = (dp[i]+dp[j])%1000000007;
                        }
                    }
    
                }
    
                System.out.println(dp[n]);  
            }
    
        }
    
    }

    7、最长公共子括号序列(来自: 牛客网)

    这里写图片描述

    展开全文
  • 算法-动态规划 Dynamic Programming--从菜鸟到老鸟

    万次阅读 多人点赞 2017-07-15 22:58:29
    前言最近在牛客网上做了几套公司的真题,发现有关动态规划(Dynamic Programming)算法的题目很多。相对于我来说,算法里面遇到的问题里面感觉最难的也就是动态规划(Dynamic Programming)算法了,于是花了好长时间...
  • 文章目录1.序2.动态规划的基本概念[^1]3.动态规划算法的基本思想[^2]4....这篇文章主要介绍动态规划算法的基本思想、使用动态规划算法求解问题的基本步骤、动态规划算法的两个基本要素以及一些经典的动态规划问题。...

空空如也

1 2 3 4 5 ... 20
收藏数 98,107
精华内容 39,242
关键字:

动态规划