动态规划详解

个人见解:1.动态规划实现了把问题拆分成多个子问题,然后求解,子问题有解后,问题自然迎刃而解;

2.动态规划实现了子问题的状态的迁移,保存每个状态值,递推出答案,但不记录每种状态的求解方法。

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

    首先,我们看一下这道题(此题目来源于北大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行的三角形,我们可以写出如下的递归式:   

[cpp] view plain copy
  1. if ( r == N)                  
  2.     MaxSum(r,j) = D(r,j)    
  3. else        
  4.     MaxSum( r, j) = Max{ MaxSum(r+1,j), MaxSum(r+1,j+1) } + D(r,j)   

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

[cpp] view plain copy
  1. #include <iostream>    
  2. #include <algorithm>   
  3. #define MAX 101    
  4. using namespace std;   
  5. int D[MAX][MAX];    
  6. int n;    
  7. int MaxSum(int i, int j){      
  8.     if(i==n)    
  9.         return D[i][j];      
  10.     int x = MaxSum(i+1,j);      
  11.     int y = MaxSum(i+1,j+1);      
  12.     return max(x,y)+D[i][j];    
  13. }  
  14. int main(){      
  15.     int i,j;      
  16.     cin >> n;      
  17.     for(i=1;i<=n;i++)     
  18.         for(j=1;j<=i;j++)          
  19.             cin >> D[i][j];      
  20.     cout << MaxSum(1,1) << endl;    
  21. }        

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

    

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

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

    

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

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

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

[cpp] view plain copy
  1. #include <iostream>    
  2. #include <algorithm>   
  3. using namespace std;  
  4.    
  5. #define MAX 101  
  6.     
  7. int D[MAX][MAX];      
  8. int n;    
  9. int maxSum[MAX][MAX];  
  10.    
  11. int MaxSum(int i, int j){        
  12.     if( maxSum[i][j] != -1 )           
  13.         return maxSum[i][j];        
  14.     if(i==n)     
  15.         maxSum[i][j] = D[i][j];       
  16.     else{      
  17.         int x = MaxSum(i+1,j);         
  18.         int y = MaxSum(i+1,j+1);         
  19.         maxSum[i][j] = max(x,y)+ D[i][j];       
  20.     }       
  21.     return maxSum[i][j];   
  22. }   
  23. int main(){      
  24.     int i,j;      
  25.     cin >> n;      
  26.     for(i=1;i<=n;i++)     
  27.         for(j=1;j<=i;j++) {         
  28.             cin >> D[i][j];         
  29.             maxSum[i][j] = -1;     
  30.         }      
  31.     cout << MaxSum(1,1) << endl;   
  32. }   

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

    

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

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

    

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

    

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

    

    

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

[cpp] view plain copy
  1. #include <iostream>    
  2. #include <algorithm>   
  3. using namespace std;   
  4.   
  5. #define MAX 101    
  6.   
  7. int D[MAX][MAX];     
  8. int n;    
  9. int maxSum[MAX][MAX];   
  10. int main(){      
  11.     int i,j;      
  12.     cin >> n;      
  13.     for(i=1;i<=n;i++)     
  14.         for(j=1;j<=i;j++)          
  15.             cin >> D[i][j];     
  16.     forint i = 1;i <= n; ++ i )       
  17.         maxSum[n][i] = D[n][i];     
  18.     forint i = n-1; i>= 1;  --i )       
  19.         forint j = 1; j <= i; ++j )           
  20.             maxSum[i][j] = max(maxSum[i+1][j],maxSum[i+1][j+1]) + D[i][j];      
  21.     cout << maxSum[1][1] << endl;    
  22. }   

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

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

    

    

    

    

    

    

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

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

[cpp] view plain copy
  1. #include <iostream>    
  2. #include <algorithm>   
  3. using namespace std;   
  4.   
  5. #define MAX 101    
  6.   
  7. int D[MAX][MAX];    
  8. int n;   
  9. int * maxSum;   
  10.   
  11. int main(){      
  12.     int i,j;      
  13.     cin >> n;      
  14.     for(i=1;i<=n;i++)     
  15.         for(j=1;j<=i;j++)          
  16.             cin >> D[i][j];     
  17.     maxSum = D[n]; //maxSum指向第n行      
  18.     forint i = n-1; i>= 1;  --i )       
  19.         forint j = 1; j <= i; ++j )         
  20.             maxSum[j] = max(maxSum[j],maxSum[j+1]) + D[i][j];      
  21.     cout << maxSum[1] << endl;    
  22. }  

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

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

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

    动规解题的一般思路

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

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

    2.确定状态

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

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

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

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

    4. 确定状态转移方程

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

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

    
  

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

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

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


转载于:https://www.cnblogs.com/timeboy/p/9464413.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/466305.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

学习阶段总结

学习阶段总结 这几天思考了很多东西&#xff0c;感觉很有必要总结一下学习的东西了。从起初进步大学到步入社会已经一年有余&#xff0c;在大学感觉学也学了&#xff0c;玩了玩了。所以不存在什么很大的遗憾。之前零零散散的也学了不少东西了&#xff0c;感觉有必要对自己做一…

我的奶奶

突然想起我奶奶&#xff0c;是突然发现&#xff0c;距离奶奶去世已经有快十年&#xff0c;这十年时间&#xff0c;我也再也没有见过奶奶。奶奶刚去世的前几年&#xff0c;有时候会梦到奶奶跟我说话&#xff0c;她总是会很耐心的告诉我很多道理。奶奶从小到大都没有指责过我&…

CPtrArray、CObArray类

CPtrArray https://baike.baidu.com/item/CPtrArray CObArray类 https://baike.baidu.com/item/CObArray CPtrArray类支持void指针数组。CPtrArray的成员函数类似于CObArray类的成员函数。 外文名 CPtrArray 支 持 void指针数组 成员函数 CObArray类的成员函数 操作符 …

WPF应用程序内存泄漏的一些原因

原文&#xff1a;Finding Memory Leaks in WPF-based applications There are numbers of blogs that folks wrote about memory leaks in Microsoft .Net Framework managed code and unmanaged code based applications. In this blog I wanted to: Show coding practices th…

[转]Eclipse RCP应用系统开发方法与实战2-- 定制应用程序窗口属性

5.1.4 定制应用程序窗口属性 向导生成的应用程序主界面并不能满足要求&#xff0c;存在很多问题&#xff0c;例如&#xff0c;主界面运行时没有自动居中&#xff0c;主界面大小没有固 定&#xff0c;主窗口标题栏文字应该是“高校经费测算系统”。再看看图5-3、图5-4的标题栏有…

bootloader启动流程分析

bootloader启动流程分析 1、Bootloader的概念和作用 Bootloader是嵌入式系统的引导加载程序&#xff0c;它是系统上电后运行的第一段程序。在完成对系统的初始化任务之后&#xff0c;它会将Flash中的Linux内核拷贝到 RAM 中去&#xff0c;跳转到内核的第一条指令处继续执行&…

赢在中国 - 史玉柱经典语录

1.要重视建立销售手册2.凡是先做试点&#xff0c;风险才低。3.找到自己的细分市场。4.不要总想着同竞争对手对立&#xff0c;而是要想办法让自己弥补竞争对手的不足。5. 如果没有价格上的优势与技术上的绝对优势&#xff0c;千万不要进入红海市场&#xff0c;否则你会必输无疑&…

Utility.cpp的函数

C 对文件夹的安全权限操作 https://blog.csdn.net/zipper9527/article/details/6256459 https://blog.csdn.net/sz76211822/article/details/73199181 ZeroMemory https://baike.baidu.com/item/ZeroMemory/961326?fraladdin GetComputerName https://baike.baidu.com/item/G…

类中调用界面ActiveX控件报错当前线程不在单线程单元中因此无法实例化 ActiveX 控件的解决办法...

解决办法是Form类中定义一个静态的ActiveX对象,在formload中将界面上的ActiveX对象赋值给新定义的对象,类中访问该静态对象即可. public static AxClientDriver_NTLib.AxClientDriverCtrl com new AxClientDriver_NTLib.AxClientDriverCtrl(); private void Form1_Load(object…

我最近在学微信小程序开发,一起吗?

从当初的一夜成名&#xff0c;到今天火爆的市场占有率&#xff0c;微信小程序已走过 4 个年头。据今年 1 月阿拉丁发布的报告显示&#xff0c;微信小程序 2020 年 DAU 已破 4 亿&#xff0c;其总数超 380 万。最近有读者问我说&#xff0c;发哥&#xff0c;之前自己一直是个人…

回数

回数是指从左到右读和从右到左读都是一样的数&#xff0c;请利用filter()滤掉非回数(打印出1000内所有回数) def is_palindrome(n): return str(n)str(n)[::-1] # str(n)[::-1]代表字符串从后往前读&#xff0c;也代表字符串的翻转 output filter(is_palindrome,range(1,10…

初始化COM库

初始化COM库 CoCreateInstance //在本机中只创建一个对象 https://baike.baidu.com/item/CoCreateInstance QueryInterface 客户可以通过此函数来查询某个组件是否支持某个特定的接口 https://baike.baidu.com/item/QueryInterface SetNotifyWindow //注册一个窗口来处理事件通…

WebService大讲堂之Axis2(2):复合类型数据的传递

在实际的应用中&#xff0c;不仅需要使用WebService来传递简单类型的数据&#xff0c;有时也需要传递更复杂的数据&#xff0c;这些数据可以被称为复合类型的数据。数组与类&#xff08;接口&#xff09;是比较常用的复合类型。在Axis2中可以直接使用将WebService方法的参数或返…

Linux虚拟摄像头vivid配置

总述最近在看摄像头驱动&#xff0c;需要配置虚拟摄像头的驱动&#xff0c;但是教程里面是linux2.6内核的&#xff0c;实际电脑的是Ubuntu16&#xff0c;内核是linux4.15版本&#xff0c;从2.6到4.15内核好多文件发生了变化&#xff0c;所以我们该怎么操作呢&#xff1f;下面就…

第一次工作报告

要求 1. 对源文件&#xff08;*.txt,*.cpp,*.h,*.cs,*.html,*.js,*.java,*.py,*.php等&#xff09;统计字符数、单词数、行数、词频&#xff0c;统计结果以指定格式输出到默认文件中&#xff0c;以及其他扩展功能&#xff0c;并能够快速地处理多个文件。 2. 使用性能…

TQ210——S5PV210 uboot顶层mkconfig分析

################################################################################################# # SHELL常用内部参数&#xff1a;   # $# —— 传递给程序的总的参数数目 # $? —— 上一个代码或者shell程序在shell中退出的情况&#xff0c;如果正常退出则返回0&am…

音频系统底层API

IAudioEndpointVolume IAudioEndpointVolume接口表示进出音频端点设备的音频流的音量控制。 客户端通过调用参数iid设置为REFIID IID_IAudioEndpointVolume的IMMDevice :: Activate方法获取对端点设备的IAudioEndpointVolume接口的引用。 IAudioEndpointVolume编程调节Win7/Wi…

早睡早起

这两天考虑着以前工作的一些问题。有一点非常需要改正。就是作息时间不规律。经常睡的很晚。把前两年锻炼身体的老本吃的差不多了。该是“存钱”的时候了。新的工作依旧很繁忙&#xff0c;事情很多&#xff0c;但是身体调整绝对不能放松的。所以还是决定从明天开始早起&#xf…

树莓派Pico的一些有趣的基本实验

▲ 实验电路板Raspberry PI Pico小型MCU模块&#xff0c;以其价格低廉&#xff0c;功能丰富&#xff0c;开发方便为很多非电子类专业的同学进行创意项目原型设计提供了方便的平台。下面的一些实验给CDIE课程设计同学们制作的一些基本演示实验。▌01 PI Pico实验板在 RASPBERRY …

Gabor变换

gabor变换分析非平稳信号有弊端主要是时频窗宽度固定 小波wavelet a小窗窄a大窗宽 窗口或短时傅氏变换在时域上和频域上的局部化程度主要由窗函数g(x)的有效时宽Δt 和有效带宽Δf 决定。Δt 和Δf 越小, 表明局部性越好。但Heisenberg 测不准原理指出[ 6] :Δt 和Δf 不可能任…