浅显易懂的动态规划入门

为了引出动态规划的基本思想,请看下面的例子:

题目描述:

斐波那契数列是数学中常见的数列,也叫兔子数列,它满足:a[1]=1,a[2]=1,a[n]=a[n-1]+a[n-2](n>2),输入n,输出a[n] mod 10000007的值。(n<=100000)。

输入样例:

3

4

5

输出样例:

2

3

5

【算法分析】

看到题目以后,我们可以很轻松的写出两个版本的代码,一个是递推的代码,一个是递归的代码。其中,递归的代码如下:

/*
prob:fib-递归
author:aqx
lang:c++
*/
#include <iostream>
#define mod 10000007
using namespace std;
int f(int x)
{
	if (x<=2)
		return 1;
	else
		return (f(x-1)+f(x-2))%mod;
}
int main()
{
	int n;
	while (cin>>n)
	{
		cout<<f(n)<<endl;
	}
}

这一段代码看起来没有问题,但是运行起来却非常慢,当n=100的时候已经无法在有限的时间内得到结果,来看当n=5的例子:

当n=5的时候,使用递归的思路计算,f(3)被重复的调用了2次,f(2)被重复的调用了3次,而f(i)无论被调用多少次,它的返回值都是相同的,

因此,进行了很多次无用的计算。如果能够在第一次计算f(i)的时候,把f(i)的结果记录下来,下一次调用的时候,直接返回f(i)的值,就可以避免很多次冗余的计算。这样一来,把冗余的计算都省去,程序的效率得到了质的提升。

【程序实现】

/*
prob:fib-记忆化
author:aqx
lang:c++
*/
#include <iostream>
#define mod 10000007
using namespace std;
int a[100001];
int f(int x)
{
	if (a[x]!=0) return a[x];
	if (x<=2)
		return a[x]=1;
	else
		return a[x]=(f(x-1)+f(x-2))%mod;
}
int main()
{
	int n;
	while (cin>>n)
	{
		cout<<f(n)<<endl;
	}
}

斐波那契数列的求法,从严格意义上来说,可能并不算是动态规划,但是却和动态规划有着千丝万缕的联系:

1.利用多余的空间记录了重复状态的计算结果,避免了冗余的计算

2.有状态转移方程(在这里是f[i]=f[i-1]+f[i-2],i>=3)

3.任何一个状态只与确定的前几项直接相关

动态规划中的一些常用名词的简单解释如下:

状态:状态是用来描述一个确定的自然状况或者是客观条件,例如在斐波那契数列中,每一个a[i]就是一个确定的状态。

阶段:把所给求解问题的过程恰当地分成若干个相互联系的阶段,以便于求解,过程不同,阶段数就可能不同.描述阶段的变量称为阶段变量。例如在斐波那契数列中,5和4就是一个阶段,5和3也是一个阶段。

无后效性:给定某一个状态,这个状态的结果只受它前一阶段的状态的影响,而不受到它前2、3…个阶段状态的影响。例如在斐波那契数列中,a[5]的值只受到a[4]和a[3]的影响,而不受到其他a[i]的影响。

决策:决策描述的是从一个状态转移到另一个状态的行动或者选择。例如,a[5]=a[4]+a[3],这就是一种行动的过程。

状态转移:状态转移和决策很类似,它是描述在决策的过程中,两个状态之间的关系。例如在斐波那契数列中,a[i]=a[i-1]+a[i-2](i>2),这就是一个状态转移方程。

以下是一道例题:

魔族密码(Vijos-p1028)

题目描述:

风之子刚走进他的考场,就……

花花:当当当当~~偶是魅力女皇——花花!!^^(华丽出场,礼炮,鲜花)

风之子:我呕……(杀死人的眼神)快说题目!否则……-_-###

花花:……咦~~好冷~~我们现在要解决的是魔族的密码问题(自我陶醉:搞不好魔族里面还会有人用密码给我和菜虫写情书咧,哦活活,当然是给我的比较多拉*^_^*)。魔族现在使用一种新型的密码系统。每一个密码都是一个给定的仅包含小写字母的英文单词表,每个单词至少包含1个字母,至多75个字母。如果在一个由一个词或多个词组成的表中,除了最后一个以外,每个单词都被其后的一个单词所包含,即前一个单词是后一个单词的前缀,则称词表为一个词链。例如下面单词组成了一个词链:

i

int

integer

但下面的单词不组成词链:

integer

intern

现在你要做的就是在一个给定的单词表中取出一些词,组成最长的词链,就是包含单词数最多的词链。将它的单词数统计出来,就得到密码了。

风之子:密码就是最长词链所包括的单词数阿……

花花:活活活,还有,这些文件的格式是,第一行为单词表中的单词数N(1<=N<=2000),下面每一行有一个单词,按字典顺序排列,中间也没有重复的单词咧!!你要提交的文件中只要在第一行输出密码就行啦^^看你长得还不错,给你一个样例吧:

输入样例:

5

i

int

integer

intern

internet

输出样例:

4

样例解释:

i->int->intern->internet

算法分析】

这道题目就是一道典型的线性动态规划,为了更好的理解它,我们首先来看它的搜索实现。如果用搜索算法来写这道题目,写起来是非常容易的,代码如下:

/*
prob:vijos-p1028-dfs
author:aqx
lang:c++
*/
#include <iostream>
#include <string.h>
using namespace std;
int n;
string s[2005];
int dp[2005];
int ans=0;
bool can(int i,int j)
{
	if (i==0) return true;
	if (s[j].find(s[i])==0) return true;
	return false;
}
//i指的是当前搜索到了哪一个字符串,
//step指的是当前一共接了几个字符串
int dfs(int i,int step)
{
	ans=max(ans,step);
	for (int j=i+1;j<=n;j++)
	if (can(i,j))
	{
		dfs(j,step+1);
	}
	return 0;
}
int main()
{
	while (cin>>n)
	{
		ans=0;
		memset(dp,0,sizeof(dp));
		for (int i=1;i<=n;i++)
		{
			cin>>s[i];
		}
		dfs(0,0);
		cout<<ans<<endl;
	}
}

但是这样的代码交上去只能得到90分。(在这个数据规模下能得到90分已经非常难得,实际竞赛中,这样的算法只能得到50分甚至更少)因此,其中必然存在着可以优化的内容。

看这样一个例子:

输入是:i,it,in,int,inter,internet

同样是inter结尾的单词序列,搜索的过程中会遇到有以下几种可能:

i->inter

in->inter

int->inter

i->in->inter

i->int->inter

in->int->inter

i->in->int->inter

他们都是以inter结尾的单词序列,之后能接的最长序列长度是一样的,假设从inter之后接出来的最长单词序列长度是x,而接到inter结尾的每一种情况的单词长度是a[i]的话,那么,每一种情况下的单词序列长度就是a[i]+x。对于本题,我们只关注最优解,而对于所有以inter结尾的单词序列,x都是相同的,那么,必然只有最大的a[i]才有可能产生最优解!其他的状态都是无用的,我们不应该去计算它!

因此,可以这样来定义一个状态:

dp[i]表示以第i个单词结尾的单词序列的最长长度。例如,dp[5]表示以inter结尾的单词序列中,最长的那一个序列的长度。显然,dp[5]=4。

之后,我们来考虑任何两个状态之间的联系:

对于一个dp[i]来说,哪些dp[j]和它相关呢?拿inter来举例子,以inter结尾的单词序列,只和i,in,int结尾的单词序列有关。更普遍的描述是:和i有关的每一个j满足:

1.j<i

2.s[j]是s[i]的前缀

当j可以接在i的前面时,dp[j]和dp[i]的联系,请看下面的图表:


i


1


2


3


4


5


6


dp[i]


1


1


2


3


4


5

对于dp[5],它与dp[1],dp[3],dp[4]都有联系,也就是说,以第5个字符串结尾的单词序列,可以由以第1、3、4个字符串结尾的单词序列接上第五个单词,组成更长的一个单词序列,因此:

dp[5]=dp[i]+1………………………………i=1,3,4

更普遍的:

dp[i]=dp[j]+1………………………………j<i且s[j]是s[i]的前缀

而我们只关心这其中最大的那一个,因此,需要在这个方程上做轻微的改动:

dp[i]=max(dp[i],dp[j]+1)………………………j<i且s[j]是s[i]的前缀

还需要注意初始状态:

dp[i]=1

因为每一个字符串自己也是一个单词序列。

有了初始状态、状态和状态转移方程,这就是一个完整的动态规划模型了。

动态规划的常用实现方式有递推和递归两种,本题使用递推的方式实现起来更容易,效率更高。

递推又分为顺推和逆推,本题将使用两种写法实现,方便读者更好的理解动态规划

/*
prob:vijos-p1028-逆推
author:aqx
lang:c++
*/
#include <iostream>
#include <string.h>
using namespace std;
int n;
string s[2005];
int dp[2005];
bool can(int i,int j)
{
	if (s[j].find(s[i])==0) return true;
	return false;
}
int main()
{
	while (cin>>n)
	{
		memset(dp,0,sizeof(dp));
		for (int i=1;i<=n;i++)
		{
			cin>>s[i];
		}
		int ans=0;
		//对任何一个i,枚举所有可以接在它之前的字符串j
		for (int i=1;i<=n;i++)
		{
			dp[i]=1;
			for (int j=1;j<i;j++)
			if (can(j,i))
			{
				dp[i]=max(dp[i],dp[j]+1);
			}
			ans=max(ans,dp[i]);
		}
		cout<<ans<<endl;
	}
}
/*
prob:vijos-p1028-顺推
author:aqx
lang:c++
*/
#include <iostream>
#include <string.h>
using namespace std;
int n;
string s[2005];
int dp[2005];
bool can(int i,int j)
{
	if (s[j].find(s[i])==0) return true;
	return false;
}
int main()
{
	while (cin>>n)
	{
		memset(dp,0,sizeof(dp));
		for (int i=1;i<=n;i++)
		{
			cin>>s[i];
		}
		int ans=0;
		//对任何一个i,枚举它可以接在哪些字符串之前
		for (int i=1;i<=n;i++)
		{
			if (dp[i]==0) dp[i]=1;
			for (int j=i+1;j<=n;j++)
			if (can(i,j))
			{
				dp[j]=max(dp[j],dp[i]+1);
			}
			ans=max(ans,dp[i]);
		}
		cout<<ans<<endl;
	}
}

两种实现方式略有不同,时间复杂度都是O(N2

这道题本质上是一类非常典型的线性动态规划算法:最长上升子序列。最长上升子序列的模型是:一个数的序列bi,当b1 < b2 < ... < bS的时候,我们称这个序列是上升的。对于给定的一个序列(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).你的任务,就是对于给定的序列,求出最长上升子序列的长度。

实际上最长上升子序列还有更好的解法,此处暂不涉及。

时间: 2024-08-02 14:33:55

浅显易懂的动态规划入门的相关文章

动态规划入门

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

[转]很特别的一个动态规划入门教程

很特别的一个动态规划入门教程 今天在网上看到一个讲动态规划的文章,是以01背包为例的,这文章和书上的讲解非常不一样,令我眼前一亮,于是转载一下下--- (说明一下,本人非常痛恨教材公式定理漫天飞,实际的讲解却讲得非常枯涩难懂,这种中国式的教育已经延绵了几千年了,现在中国的教材还是这个样子,讲清楚 些明白些就那么难么?高中有个老师讲的一句话一直觉得很有道理:“教得会天才不是真本事,能把博士生的东西讲到小学生都会用那才是真水平.”) 附上原文地址: http://www.cnblogs.com/sd

很特别的一个动态规划入门教程

很特别的一个动态规划入门教程 (2016-03-10 17:05:48) 转载▼ 标签: cpp 动态规划 分类: Cpp精选 很特别的一个动态规划入门教程 今天在网上看到一个讲动态规划的文章,是以01背包为例的,这文章和书上的讲解非常不一样,令我眼前一亮,于是转载一下下---(说明一下,本人非常痛恨教材公式定理漫天飞,实际的讲解却讲得非常枯涩难懂,这种中国式的教育已经延绵了几千年了,现在中国的教材还是这个样子,讲清楚些明白些就那么难么?高中有个老师讲的一句话一直觉得很有道理:“教得会天才不是真

动态规划入门戳进来

学动态规划自然要从数字三角形开始起步,那么我们就先从数字三角形开始. 数字三角形题目:有一个由非负整数组成的三角形,第一行只有一个数,除了最下行之外的每个数的左下方和右下方各有一个数,如下图所示: 1 3 2 4 10 1 4 3 2 20 从第一行的数开始,每次可以往下或往右下走一格,直到走到最下行,把沿途经过的数全部加起来.如何走才能使这个和最大? 知道回溯法么(请参看:八皇后与回溯法),你会发现这是一个动态的决策问题:每次有两种选择--向左或是向右,每一步决策又影响到后面的决策,如果用贪心

由LCS到编辑距离—动态规划入门—算法学习笔记

一切计算机问题,解决方法可以归结为两类:分治和封装.分治是减层,封装是加层. 动态规划问题同样可以用这种思路,分治. 它可以划分为多个子问题解决,那这样是不是用简单的递归就完成了?也许是的,但是这样会涉及太多的不便的操作.因为子问题有重叠! 针对这种子问题有重叠的情况的解决,就是提高效率的关键. 所以动态规划问题可以总结为:最优子结构和重叠子问题. 解决这个子问题的方式的关键就是:memoization,备忘录. 动态规划算法分以下4个步骤: 描述最优解的结构 递归定义最优解的值 按自底向上的方

hdu2084动态规划入门题----数塔

原题:数塔 这个是动态规划入门题,比较简单. 题意是: 一个数字组成的三角形,从上到下找一条路径,使这条路径上数字之和最大. 解题思路,就是要从下往上看.举个例子: 如果你从上到下走到了第4行第1个数,也就是2,那么接下来有两个数可以走19和7,而你必然会选择19. 所以就可以根据这个思路更新上面一行的数.把2更新成2+19=21.18更新成18+10=28,9更新成9+10=19,5更新成5+16=21 重复上面的思路最后第一行累加出来的就是最大值了. 思路很简单,最简单的实现就是你也开一个二

史上最浅显易懂的RxJava入门教程

RxJava是一个神奇的框架,用法很简单,但内部实现有点复杂,代码逻辑有点绕.我读源码时,确实有点似懂非懂的感觉.网上关于RxJava源码分析的文章,源码贴了一大堆,代码逻辑绕来绕去的,让人看得云里雾里的.既然用拆轮子的方式来分析源码比较难啃,不如换种方式,以造轮子的方式,将源码中与性能.兼容性.扩展性有关的代码剔除,留下核心代码带大家揭秘 RxJava 的实现原理. 什么是RxJava ? Rx是Reactive Extensions的简写,翻译为响应的扩展.也就是通过由一方发出信息,另一方响

动态规划入门(一)

2017-09-01 11:29:43 writer:pprp 看sprout台湾大学acm教学视频的第一部分: 里边涉及到四道小例题 感觉很好就拿来写了写: 题意还有代码说明都在代码中: 1.最基础的骨牌问题: /* @param:dp 入门 @writer:pprp @declare:最经典最简单的dp @begin:9:00 @end:10:00 @date:2017/9/1 */ #include <bits/stdc++.h> using namespace std; //未优化的最

白话算法之【动态规划入门】

什么是动态规划? 动态规划(Dynamic Programming,所以我们简称动态规划为DP)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法.20世纪50年代初美国数学家R.E.Bellman等人在研究多阶段决策过程(multistep decision process)的优化问题时,提出了著名的最优化原理(principle of optimality),把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解,创立了解决这类过程优化问题的新