教你彻底学会动态规划——进阶篇
在我的上一篇文章中已经详细讲解了动态规划的原理和如何使用动态规划解题。本篇文章,我将继续通过例子来让大家更加熟练地使用动态规划算法。
话不多说,来看如下例题,也是在动态规划里面遇到过的最频繁的一个题,本题依然来自于北大POJ:
最长公共子序列(POJ1458)
给出两个字符串,求出这样的一个最长的公共子序列的长度:子序列中的每个字符都能在两个原串中找到, 而且每个字符的先后顺序和原串中的先后顺序一致。
Sample Input
abcfbc abfcab
programming contest
abcd mnp
Sample Output
4
2
0
解题思路:设输入的两个串为s1,s2, 设MaxLen(i,j)表示::s1的左边i个字符形成的子串,与s2左边的j个字符形成的子串的最长公共子序列的长度(i,j从0 开始算),则MaxLen(i,j) 就是本题的“状态” (如果还不懂“状态”是什么意思,可以参考我上一篇文章)
假定 len1 = strlen(s1),len2 = strlen(s2),那么题目就是要求 MaxLen(len1,len2)
显然:
MaxLen(n,0) = 0 ( n= 0…len1)
MaxLen(0,n) = 0 ( n=0…len2)
于是,我们可以得到如下的递推公式:
if ( s1[i-1] == s2[j-1] ) //s1的最左边字符是s1[0] MaxLen(i,j) = MaxLen(i-1,j-1) + 1;
else MaxLen(i,j) = Max(MaxLen(i,j-1),MaxLen(i-1,j) );
时间复杂度O(mn) m,n是两个字串长度
S1[i-1]!= s2[j-1]时,MaxLen(S1,S2)不会比MaxLen(S1,S2j-1) 和MaxLen(S1i-1,S2)两者之中任何一个小,也不会比两者都大。
通过上面的分析,我们很简单就可以写出如下的代码:
#include <iostream>
#include <cstring>
using namespace std; char sz1[1000];
char sz2[1000];
int maxLen[1000][1000];
int main(){ while( cin >> sz1 >> sz2 ) { int length1 = strlen( sz1); int length2 = strlen( sz2); int nTmp; int i,j; for( i = 0;i <= length1; i ++ ) maxLen[i][0] = 0; for( j = 0;j <= length2; j ++ ) maxLen[0][j] = 0; for( i = 1;i <= length1;i ++ ) { for( j = 1; j <= length2; j ++ ){ if( sz1[i-1] == sz2[j-1] ) maxLen[i][j] = maxLen[i-1][j-1] + 1; else maxLen[i][j] = max(maxLen[i][j-1],maxLen[i-1][j]); } } cout << maxLen[length1][length2] << endl; } return 0;
}
然后提交我们的代码,一次AC。
接下来我们再来看一道典型的例题:
最长上升子序列(百练2757)
一个数的序列ai,当a1 < a2 < ... < aS的时候,我们称这个序列是上升的。对于给定的一个序列(a1, a2, ..., aN),我们可以得到一些上升的子序列(ai1, ai2, ..., aiK),这里1 <= i1 < i2 < ... < 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。
输出要求
最长上升子序列的长度。
输入样例
7
1 7 3 5 9 4 8
输出样例
4
解题思路
1.找子问题
“求序列的前n个元素的最长上升子序列的长度”是个子问题,但这样分解子问题,不具有“无后效性”,因为假设F(n) = x,但可能有多个序列满足F(n) = x。有的序列的最后一个元素比 an+1小,则加上an+1就能形成更长上 升子序列;有的序列最后一个元素不比an+1小……以后的事情受如何达到状态n的影响,不符合“无后效性” ,因此我们必须换一种思路来解决此问题。
“求以ak(k=1, 2, 3…N)为终点的最长上升子序列的长度”,一个上升子序列中最右边的那个数,称为该子序列的 “终点”。虽然这个子问题和原问题形式上并不完全一样,但是只要这N个子问题都解决了,那么这N个子问题的解中, 最大的那个就是整个问题的解。
2.确定状态
子问题只和一个变量—— 数字的位置相关。因此序列中数的位置k就是“状态”,而状态 k 对应的“值”,就是以ak做为“终点”的最长上升子序列的长度。 状态一共有N个。
3.找出状态转移方程
maxLen (k)表示以ak做为“终点”的
最长上升子序列的长度那么:
初始状态:maxLen (1) = 1
maxLen (k) = max { maxLen (i):1<=i < k 且 ai < ak且 k≠1 } + 1 若找不到这样的i,则maxLen(k) = 1
maxLen(k)的值,就是在ak左边,“终点”数值小于ak ,且长度最大的那个上升子序列的长度再加1。因为ak左边任何“终点”小于ak的子序列,加上ak后就能形成一个更长的上升子序列。
有了这个思路,我们就可以很轻松地写出代码了。然而,即使到了这里,我们依然还能从两个方向解决这道题,我们可以将它们分别称为“人人为我”递推型动归和“我为人人”递推型动归 。请看下面的讲解:
“人人为我”递推型动归
状态i的值Fi由若干个值 已知的状态值Fk,Fm,..Fy 推出,如求和,取最大值
根据这个方向,我们不难写出如下代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std; const int MAXN =1010;
int a[MAXN]; int maxLen[MAXN]; int main(){ int N; cin >> N; for( int i = 1;i <= N;++i){ cin >> a[i]; maxLen[i] = 1; } for( int i = 2; i <= N; ++i){ //每次求以第i个数为终点的最长上升子序列的长度 for( int j = 1; j < i; ++j) //察看以第j个数为终点的最长上升子序列 if( a[i] > a[j] ) maxLen[i] = max(maxLen[i],maxLen[j]+1); } cout << * max_element(maxLen+1,maxLen + N + 1 ); return 0;
} //时间复杂度O(N2)
下面是我这段代码的提交结果:
“我为人人”递推型动归
状态i的值Fi在被更新(不一定是 最终求出)的时候,依据Fi去更 新(不一定是最终求出)和状态i 相关的其他一些状态的值 Fk,Fm,..Fy
根据这个方向,我们又可以写出如下代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std; const int MAXN =1010;
int a[MAXN];
int maxLen[MAXN]; int main(){ int N; cin >> N; for( int i = 1;i <= N;++i){ cin >> a[i]; maxLen[i] = 1; } for( int i = 1; i <= N; ++i) for( int j = i + 1; j <= N; ++j ) //看看能更新哪些状态的值 if( a[j] > a[i] ) maxLen[j] = max(maxLen[j],maxLen[i]+1); cout << * max_element(maxLen+1,maxLen + N + 1 ); return 0;
} //时间复杂度O(N2)
下面是我这段代码的提交结果:
接下来,就要进行一个总结了:
动规的三种形式
1)记忆递归型
优点:只经过有用的状态,没有浪费。递推型会查看一些 没用的状态,有浪费。
缺点:可能会因递归层数太深导致栈溢出,函数调用带来额外时间开销。总体来说,比递推型慢。
2) “我为人人”递推型
没有什么明显的优势,有时比较符合思考的习惯。个别特殊题目中会比“人人为我”型节省空间。
3)“人人为我”递推型
在选取最优备选状态的值Fm,Fn,…Fy时, 有可能有好的算法或数据结构可以用来显 著降低时间复杂度。
教你彻底学会动态规划——进阶篇相关推荐
- 教你彻底学会动态规划——入门篇
动态规划相信大家都知道,动态规划算法也是新手在刚接触算法设计时很苦恼的问题,有时候觉得难以理解,但是真正理解之后,就会觉得动态规划其实并没有想象中那么难.网上也有很多关于讲解动态规划的文章,大多都是叙 ...
- 笨办法学python3_月底送书!入门Python都在学的“笨办法”,出进阶篇了!
你们知道吗?每10个将Python作为自己的入门语言的程序员里,就有8个是读着<"笨办法"学Python>的书成长的!这位拥有近20年的编程和写作经验的IT书籍作家-- ...
- [安全攻防进阶篇] 二.如何学好逆向分析、逆向路线推荐及吕布传游戏逆向案例
从2019年7月开始,我来到了一个陌生的专业--网络空间安全.初入安全领域,是非常痛苦和难受的,要学的东西太多.涉及面太广,但好在自己通过分享100篇"网络安全自学"系列文章,艰难 ...
- 我的.NET书架 (进阶篇)
我的.NET书架 (进阶篇) 小气的神 2003-01-06 Article Type: OverView 难度等级:1/9 版本:1.20 对于已经熟悉和习惯.NET Framework开发环境的开 ...
- 搞懂正则表达式之进阶篇
在上一篇博文里我们学习了基础的正则表达式,学会这些还不足以应对工作学习,现在开始学习进阶篇的正则表达式. 目录 1.分组 2.或者 3.分组回溯 4.先行断言 5.后行断言 1.分组 在正则表达式中提 ...
- java中怎样导入图片6_java程序员进阶篇之必须掌握的6张思维导图!
原标题:java程序员进阶篇之必须掌握的6张思维导图! <java程序员的自我修养>的作者是MindMaster思维导图社区用户芒果,作者整理了一系列学习java程序的笔记,方便java程 ...
- [安全攻防进阶篇] 四.逆向分析之条件语句和循环语句源码还原及流程控制逆向
从2019年7月开始,我来到了一个陌生的专业--网络空间安全.初入安全领域,是非常痛苦和难受的,要学的东西太多.涉及面太广,但好在自己通过分享100篇"网络安全自学"系列文章,艰难 ...
- [安全攻防进阶篇] 一.什么是逆向分析、逆向分析应用及经典扫雷游戏逆向
从2019年7月开始,我来到了一个陌生的专业--网络空间安全.初入安全领域,是非常痛苦和难受的,要学的东西太多.涉及面太广,但好在自己通过分享100篇"网络安全自学"系列文章,艰难 ...
- 使用Python进行任务调度(进阶篇)
在上一篇文章使用Python完美管理和调度你的多个任务中,介绍了使用Python+schedule管理和调度任务的入门方法,本文继续介绍任务调度进阶篇. 问题描述:启动多个任务之后,由于种种原因,可能 ...
最新文章
- 软件包管理(rpmyum)
- 什么是php伪静态规则,php设置伪静态规则编写 问题
- 安卓重要组件#1--ListView创建及基本的使用方法
- 初识sparklyr—电影数据分析
- python3.8.2安装教程-在服务器上安装python3.8.2环境
- CSS中常用的样式语法
- 构架、框架、设计模式之间的关系简述
- 两届诺贝尔文学奖得主将同时公布
- TensorFlow中数据读取—如何载入样本
- 父子域+主辅域控的搭建
- 还在被网络上各种关于单片机行业的收入搞的眼花缭乱而烦恼吗
- Word文档,英文字母间隔非常大,解决方法
- NFS 服务端配置流程
- 计算机专业今日份例句
- loss下降auc下降_从基本原理到梯度下降算法:零基础也能看懂的神经网络教程...
- 切蛋糕问题【小学二年级奥数】
- linux yum仓库命令,linux 自定义yum仓库、repo文件 yum命令
- PDF办公技巧之PDF怎么删除其中一页
- pclint使用静态检测代码内存使用错误
- 真阳率(true positive rate)、假阳率(false positive rate),AUC,ROC