C++——动态规划入门 笔记1

来源:互联网 发布:无人机路径规划软件 编辑:程序博客网 时间:2024/06/06 15:00

其实一直没分清动态规划和贪心以及递归的区别,然而这周的作业是动态规划专题(╯‵□′)╯︵┻━┻,我用贪心写,不知道WA了多少次(╥╯^╰╥)。看到别人写的动态规划的题解还看不懂,╮(╯▽╰)╭。

然后今天在B站上/*看动画片的时候*/发现有很多up主上传了关于动态规划的视频,怎么说呢,豁然开朗(注:这里不是打广告,B站上出了鬼畜还是有很多东西的,毕竟连考研数学都有(`´ー))。

首先,援引百度百科上的介绍/*大家可以无视这一段,毕竟百度百科是本着让本来就懂的人看的,对于我这种完全不懂的小白并没有什么用*/:

动态规划(dynamicprogramming)运筹学的一个分支,是求解决策过程(decisionprocess)最优化的数学方法。20世纪50年代初美国数学家R.E.Bellman等人在研究多阶段决策过程(multistepdecision process)的优化问题时,提出了著名的最优化原理(principleof optimality),把多阶段过程转化为一系列单阶段问题,利用各阶段之间的关系,逐个求解,创立了解决这类过程优化问题的新方法——动态规划。1957年出版了他的名著《DynamicProgramming》,这是该领域的第一本著作。

动态规划一般可分为线性动规,区域动规,树形动规,背包动规四类。

举例:

线性动规:拦截导弹,合唱队形,挖地雷,建学校,剑客决斗等;

区域动规:石子合并,加分二叉树,统计单词个数,炮兵布阵等;

树形动规:贪吃的九头龙,二分查找树,聚会的欢乐,数字三角形等;

背包问题:01背包问题,完全背包问题,分组背包问题,二维背包,装箱问题,挤牛奶(同济ACM1132题)等;

应用实例:

最短路径问题项目管理,网络流优化等;

POJ动态规划题目列表:

容易:
  1018,1050,1083,1088,1125,1143,1157,1163,1178,1179,1189,1191,1208,1276,1322,1414,1456,1458,1609,1644,1664,1690,1699,1740,1742,1887,1926,1936,1952,1953,1958,1959,1962,1975,1989,2018,2029,2039,2063,2081,2082,2181,2184,2192,2231,2279,2329,2336,2346,2353,2355,2356,2385,2392,2424

不易:
  1019,1037,1080,1112,1141,1170,1192,1239,1655,1695,1707,1733(区间减法加并查集),1737,1837,1850,1920(加强版汉罗塔),1934(全部最长公共子序列),1964(最大矩形面积,O(n*m)算法),2138,2151,2161,2178

推荐:
  1015,1635,1636,1671,1682,1692,1704,1717,1722,1726,1732,1770,1821,1853,1949,2019,2127,2176,2228,2287,2342,2374,2378,2384,2411

基本结构

多阶段决策问题中,各个阶段采取的决策,一般来说是与时间有关的,决策依赖于当前状态,又随即引起状态的转移,一个决策序列就是在变化的状态中产生出来的,故有动态的含义,称这种解决多阶段决策最优化问题的方法为动态规划方法。

动态规划基本模型

根据上例分析和动态规划的基本概念,可以得到动态规划的基本模型如下:

(1)确定问题的决策对象。(2)对决策过程划分阶段。(3)对各阶段确定状态变量 (4)根据状态变量确定费用函数和目标函数。 (5)建立各阶段状态变量的转移过程,确定状态转移方程。

状态转移方程的一般形式:

一般形式: U:状态; X:策略
  顺推:f[Uk]=opt{f[Uk-1]+L[Uk-1,Xk-1]}其中,L[Uk-1,Xk-1]状态Uk-1通过策略Xk-1到达状态Uk的费用初始f[U1];结果:f[Un]

倒推:
  f[Uk]=opt{f[Uk+1]+L[Uk,Xk]}
  L[Uk,Xk]状态Uk通过策略Xk到达状态Uk+1的费用
  初始f[Un];结果:f(U1)

动态规划适用条件

任何思想方法都有一定的局限性,超出了特定条件,它就失去了作用。同样,动态规划也并不是万能的。适用动态规划的问题必须满足最优化原理和无后效性。

1.最优化原理(最优子结构性质)最优化原理可这样阐述:一个最优化策略具有这样的性质,不论过去状态和决策如何,对前面的决策所形成的状态而言,余下的诸决策必须构成最优策略。简而言之,一个最优化策略的子策略总是最优的。一个问题满足最优化原理又称其具有最优子结构性质。

2.无后效性将各阶段按照一定的次序排列好之后,对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。换句话说,每个状态都是过去历史的一个完整总结。这就是无后向性,又称为无后效性。

3.子问题的重叠性动态规划将原来具有指数级时间复杂度的搜索算法改进成了具有多项式时间复杂度的算法。其中的关键在于解决冗余,这是动态规划算法的根本目的。动态规划实质上是一种以空间换时间的技术,它在实现的过程中,不得不存储产生过程中的各种状态,所以它的空间复杂度要大于其它的算法。

——以上全部来自百度百科(链接https://baike.baidu.com/item/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/529408?fr=aladdin

然后回归正题

 

其实归根结底,动态规划与贪心和递归还是有一定关系的。

 

举例:

斐波拉切数列

大家对这个数列应该都不陌生,先粗略得列一下该数列的前几项:

 1 12 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181……

这个数列的特点是从第三项起,每一项都是前两项之和。

所以,理所当然的,我们会想到递归解法

代码如下:

int fab(int n){if(n == 1 || n == 2)//即第一项和第二项的值都为1{return 1;}else//如果n>2,那么每一项都是前两项之和,但是前两项万一没有算,就会再往前找两项{return fab(n - 1) + fab(n - 2);//直到找到了n==1和n==2}}

   这个就是最经典的递归解法。那么,既然问题解决了,为什么还要讨论动态规划呢?

原因很简单,举个栗子,如果我们要算fab(6),我们需要干什么?

由递归的知识,我们知道,要想知道fab(6),我们要建立如下解答树:

fab(6)

                        

                   fab(5)                             fab(4)

                                                        

           fab(4)          fab(3)              fab(3)          fab(2)

           ↙↘            ↙↘               ↙↘

       fab(3)  fab(2)  fab(2)  fab(1)      fab(2)  fab(1)

我们可能觉得,这也不麻烦啊,但是,这才到fab(6),如果是fab(7),fab(8),……,fab(n)呢?通过对解答树的观察,我们可以很容易发现,有很多数被算了不止一次,当n足够大的时候,递归的时间复杂度会很大,达到O(2^n),很容易TLE,而这也是我们不愿意看到的。

因此,我们需要一种方法——即每个数只算一次,这个数会被计算机记住(无论计算机用什么办法记住),而在之后的计算中,这个数会被直接调用,不需多次运算,如果可以实现的话,时间复杂度会显著降低,从O(2^n)降至O(n)

一下代码来自:http://m.blog.csdn.net/u011499425/article/details/52705544

动态规划的备忘录方法:

int fab(int n, int m[]){if(m[n]){return m[n];}else{m[n] = fab(n - 1, m) + fab(n - 2, m);return m[n];}}int memFab(int n){int m[SIZE];int i;m[1] = 1;m[2] = 1;for(i = 3; i < SIZE; ++i){m[i] = 0;}return fab(n, m);}

动态规划的迭代方法:

int fab(int n){int f1, f2, f3, i;if(n == 1 || n == 2){return 1;}else{f1 = 1;f2 = 1;for(i = 2; i < n; ++i){f3 = f1 + f2;f1 = f2;f2 = f3;}return f3;}}

-------------------------------------------以上(有轻微改动)

顺便附上另一个讨论递归和动归时间的:https://segmentfault.com/a/1190000007927865

给定一个数列,长度为N,求这个数列的最长上升(递增)子数列(LIS)的长度.
1 7 2 8 3 4为例。这个数列的最长递增子数列是 1 2 3 4,长度为4
次长的长度为3, 包括 1 7 8; 1 2 3.

据说,这个问题是讨论DP必备的问题。

这个题英文版应该是这个//因为恰好有这个的代码,所以偷个懒

Longest Ordered Subsequence

A numericsequence ofai is ordered if a1 < a2< ... <aN. Let the subsequence of the given numericsequence ( a1,a2, ..., aN)be any sequence ( ai1,ai2, ..., aiK),where 1 <= i1 <i2 < ... < iK<= N. For example, sequence (1, 7, 3, 5, 9, 4, 8) has orderedsubsequences, e. g., (1, 7), (3, 4, 8) and many others. All longest orderedsubsequences are of length 4, e. g., (1, 3, 5, 8).

Your program, when given the numeric sequence, must find the length of itslongest ordered subsequence.

Input

The first lineof input file contains the length of sequence N. The second line contains theelements of sequence - N integers in the range from 0 to 10000 each, separatedby spaces. 1 <= N <= 1000

Output

Output file mustcontain a single integer - the length of the longest ordered subsequence of thegiven sequence.

Sample Input

7

1 7 3 5 9 4 8

Sample Output

4

先放代码:

 

#include<iostream>#include<cstring>#include<string>#include<algorithm>using namespace std;int a[1005],dp[1005],n;int LIS()  {      int i,j,ans,m;      dp[1] = 1;      ans = 1;  //无论如何,至少满足要求的数列长度为1    for(i = 2;i<=n;i++)  //两个循环——比较的关键步骤    {          m = 0;          for(j = 1;j<i;j++)          {              if(dp[j]>m && a[j]<a[i])//先将第一项与m比较若第一项>0且满足在dp[i]之前较小的有dp[j]            m = dp[j];//将dp[j]的值赋给m,之后就是新的dp[j]与dp[i]的比较了        }        dp[i] = m+1;        if(dp[i]>ans)        ans = dp[i];//得到最长子序列    }      return ans;}int main(){    int i;    while(~scanf("%d",&n))    {        for(i = 1;i<=n;i++)        scanf("%d",&a[i]);        printf("%d\n",LIS());    }      return 0;}

一开始,我做这题的想法是很单纯的,就是先输入一个数列,然后从第一项开始与后面各项比较,如果后面有某项比第一项大,就计数+1,然后把这个某项按第一项相同操作办法直到最后一项,然而本地过了,提交好像是WA还是TLE之类的记不清了。

其实,这道题就是一个裸的一维DP…………(╯‵□′)╯︵┻━┻

最后附上一个在360doc上看到的引入的例子,个人觉得写得不错(链接:http://www.360doc.com/content/13/0601/00/8076359_289597587.shtml

动态规划算法通常基于一个递推公式及一个或多个初始状态。当前子问题的解将由上一次子问题的解推出。使用动态规划来解题只需要多项式时间复杂度,因此它比回溯法、暴力法等要快许多。

现在让我们通过一个例子来了解一下DP的基本原理。

首先,我们要找到某个状态的最优解,然后在它的帮助下,找到下一个状态的最优解。

状态代表什么及如何找到它?

状态"用来描述该问题的子问题的解。原文中有两段作者阐述得不太清楚,跳过直接上例子。

如果我们有面值为1元、3元和5元的硬币若干枚,如何用最少的硬币凑够11元? (表面上这道题可以用贪心算法,但贪心算法无法保证可以求出解,比如1元换成2元的时候)

首先我们思考一个问题,如何用最少的硬币凑够i(i<11)?为什么要这么问呢?两个原因:1.当我们遇到一个大问题时,总是习惯把问题的规模变小,这样便于分析讨论。 2.这个规模变小后的问题和原来的问题是同质的,除了规模变小,其它的都是一样的,本质上它还是同一个问题(规模变小后的问题其实是原问题的子问题)

好了,让我们从最小的i开始吧。当i=0,即我们需要多少个硬币来凑够0元。由于135都大于0,即没有比0小的币值,因此凑够0元我们最少需要0个硬币。 (这个分析很傻是不是?别着急,这个思路有利于我们理清动态规划究竟在做些什么。)这时候我们发现用一个标记来表示这句凑够0元我们最少需要0个硬币。会比较方便,如果一直用纯文字来表述,不出一会儿你就会觉得很绕了。那么,我们用d(i)=j来表示凑够i元最少需要j个硬币。于是我们已经得到了d(0)=0,表示凑够0元最小需要0个硬币。当i=1时,只有面值为1元的硬币可用,因此我们拿起一个面值为1的硬币,接下来只需要凑够0元即可,而这个是已经知道答案的,即d(0)=0。所以,d(1)=d(1-1)+1=d(0)+1=0+1=1。当i=2时,仍然只有面值为1的硬币可用,于是我拿起一个面值为1的硬币,接下来我只需要再凑够2-1=1元即可(记得要用最小的硬币数量),而这个答案也已经知道了。所以d(2)=d(2-1)+1=d(1)+1=1+1=2。一直到这里,你都可能会觉得,好无聊,感觉像做小学生的题目似的。因为我们一直都只能操作面值为1的硬币!耐心点,让我们看看i=3时的情况。当i=3时,我们能用的硬币就有两种了:1元的和3元的( 5元的仍然没用,因为你需要凑的数目是3元!5元太多了亲)。既然能用的硬币有两种,我就有两种方案。如果我拿了一个1元的硬币,我的目标就变为了:凑够3-1=2元需要的最少硬币数量。即d(3)=d(3-1)+1=d(2)+1=2+1=3。这个方案说的是,我拿31元的硬币;第二种方案是我拿起一个3元的硬币,我的目标就变成:凑够3-3=0元需要的最少硬币数量。即d(3)=d(3-3)+1=d(0)+1=0+1=1.这个方案说的是,我拿13元的硬币。好了,这两种方案哪种更优呢?记得我们可是要用最少的硬币数量来凑够3元的。所以,选择d(3)=1,怎么来的呢?具体是这样得到的:d(3)=min{d(3-1)+1, d(3-3)+1}

OK,码了这么多字讲具体的东西,让我们来点抽象的。从以上的文字中,我们要抽出动态规划里非常重要的两个概念:状态和状态转移方程。

上文中d(i)表示凑够i元需要的最少硬币数量,我们将它定义为该问题的"状态",这个状态是怎么找出来的呢?我在另一篇文章动态规划之背包问题(一)中写过:根据子问题定义状态。你找到子问题,状态也就浮出水面了。最终我们要求解的问题,可以用这个状态来表示:d(11),即凑够11元最少需要多少个硬币。那状态转移方程是什么呢?既然我们用d(i)表示状态,那么状态转移方程自然包含d(i),上文中包含状态d(i)的方程是:d(3)=min{d(3-1)+1, d(3-3)+1}。没错,它就是状态转移方程,描述状态之间是如何转移的。当然,我们要对它抽象一下,

d(i)=min{ d(i-vj)+1 },其中i-vj>=0vj表示第j个硬币的面值;

此外,通过追踪我们是如何从前一个状态值得到当前状态值的,可以找到每一次我们用的是什么面值的硬币。比如,从上面的图我们可以看出,最终结果d(11)=d(10)+1(面值为1),而d(10)=d(5)+1(面值为5),最后d(5)=d(0)+1 (面值为5)。所以我们凑够11元最少需要的3枚硬币是:1元、5元、5元。

注意:原文中这里本来还有一段的,但我反反复复读了几遍,大概的意思我已经在上文从i=0i=3的分析中有所体现了。作者本来想讲的通俗一些,结果没写好,反而更不好懂,所以这段不翻译了。



原创粉丝点击