目录

动规解题的一般思路

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

动归的常用形式

例题

例题一:最长公共子序列

例题二:最大子段和

例题三:最长上升子序列(最长单调递增)

例题四:数字三角形

例题五 0-1背包问题

应用题

应用一:合唱队形(最长递增、递减序列)

应用二:数塔问题(数字三角形)


动规解题的一般思路

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

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

2.确定状态

  • 将和子问题相关的各个变量的一组取值,称之为一个“状态”。一个“状态”对应于一个或多个子问题,所谓某个“状态”下的“值”就是这个“状态”所对应的子问题的解。
  • 所有“状态”的集合,构成问题的“状态空间”,“状态空间”的大小,与用动态规划解决问题的时间复杂度直接相关。
  • 整个问题的时间复杂度是状态数目乘以计算每个状态所需时间。

在数字三角形的例子里,一共有N*(N+1)/2个数字,所以这个问题的状态空间里一共就有N* (N+1)/2个状态。数字三角形中每个“状态”只需要经过一次,且在每个状态上作计算所花的时间都是和N无关的常数。

用动态规划解题,经常碰到的情况是,K个整型变量能构成一个状态(如数字三角形中的行号和列号这两个变量构成“状态”)。如果这K个整型变量的取值范围分别是N1, N2, ..... Nk,那么我们就可以用一个K维的数组array[N1] [N2.]....Nk]来存储各个状态的“值”。这个值”未必就是一个整数或浮点数,可能是需要一个结构才能表示的,那么array就可以是一个结构数组。一个“状态”下的‘‘值’通常会是一个或多个子问题的解。

3.确定一些初始状态(边界状态)的值
以“数字三角形”为例,初始状态就是底边数字,值就是底边数字值。

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

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

  • 问题具有最优子结构性质。即问题的子问题的解也是最优的。
  • 无后效性。当前的若干个状态值一旦确定,则此后过程的演变就只和这若干个状态的值有关,和之前是采取哪种手段或经过哪条路径演变到当前的这若干个状态,没有关系。

动归的常用形式

  • 递归型

    • 优点:直观,容易编写
    • 缺点:可能会因递归层数太深导致爆栈,函数调用带来额外时间开销。无法使用滚动数组节省空间。总体来说,比递推型慢。
  • 递推型
    • 效率高,有可能使用滚动数组节省空间

例题

例题一:最长公共子序列

1.描述

给出两个字符串,求最长的公共子序列的长度。

公共子序列:子序列中的每个字符都能在两个原串中找到,而且每个字符的先后顺序和原串中的先后顺序一致。

2.样例输入:

abcfbc abfcab
programming
contest
abcdmnp

3.样例输出:

4

2

0

(1)找出子问题

输入两个串s1 ,s2,
设MaxLen(i,j)表示:s1的左边i个字符形成的子串,与s2左边的j个字符形成的子串的最长公共子序列的长度(i,j从0开始)

(2)确定状态
MaxLen(i,j)就是本题的“状态”
假定len1 = strlen(s1),len2 = strlen(s2)那么题目就是要求MaxLen(len1,len2)

(3)状态转移方程

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;
elseMaxLen(i,j) = Max(MaxLen(i,j-1),MaxLen(i-1,j));
  • 当s1[i]=s2[j]时,找出s1[i-1]和s2[j-1]的最长公共子序列,然后在其尾部加上s1[i](=s2[j])即可得s1和s2一个最长公共子序列。
  • 当s1[i]≠s2[j]时,必须解两个子问题,即找出s1[i-1]和s2[j]的一个最长公共子序列及s1[i]和s2[j-1]的一个最长公共子序列。这两个公共子序列中较长者为s1和s2的一个最长公共子序列。

4.代码
时间复杂度O(mn),m,n是两个字串长度

#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 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;elsemaxLen[i][j]=max(maxLen[i][j-1],maxLen[i-1][j]);}cout<<maxLen[length1][length2]<<endl;}return 0;
}

例题二:最大子段和

1.描述

由n个整数(包含负整数)组成的序列a1,a2,...,an,求该序列子段和的最大值。

规定当所有整数均为负值时定义其最大子段和为0。

否则所求的最大值为

例如,当(a1,a2, ……a7,a8)=(1,-3, 7,8,-4,12, -10,6)时, 最大子段和为:7+8-4+12=23

2.思路

设b[j]是从a[1]到a[j]位置的最大子段和(必须以a[j]结尾)

当b[j-1]>0时b[j]=b[j-1]+a[j],否则b[j]=a[j]

 

1

2

3

4

5

6

a[i]

-2

11

-4

13

-5

-2

b(初值=0)

-2

11

7

20

15

13

sum

0

11

11

20

20

20

3.代码

#include <iostream>
#include <algorithm>
using namespace std;
const int MAX=10000;
int main()
{int n;cin>>n;int a[MAX],b[MAX];for(int i=1;i<=n;i++){cin>>a[i];b[i]=0;//以a[i]为结尾的最大子段和}for(int i=1;i<=n;i++){if(b[i-1]>0)b[i]=b[i-1]+a[i];elseb[i]=a[i];}int m=*max_element(b+1,b+n+1);cout<<max(m,0)<<endl;return 0;
}

例题三:最长上升子序列(最长单调递增)

1.描述

一个数的序列ai,当a1 < a2 < ... < ag的时候,我们称这个序列是上升的。比如,对于序列(1,7,3,5,9,4,8),有它的一些上升子序列,如(1,7),(3,4,8)等等。这些子序列中最长的长度是4,比如子序列(1,3,5,8)。

2.输入

第一行是序列的长度N (1 <= N<=1000)。给出序列中的N个整数,这些整数的取值范围都在0到10000。

3.输出

最长上升子序列的长度

4.输入样例

7

1 7 3 5 9 4 8

5.输出样例

4

6.思路

(1)子问题

求以a[k] (k=1,2,3…N) 为终点的最长上升子序列的长度

一个上升子序列中最右边的那个数,称为该子序列的“终点”。

虽然这个子问题和原问题形式上并不完全一样,但是只要这N个子问题都解决了,那么这N个子问题的解中,最大的那个就是整个问题的解。

(2)确定状态

子问题只和一个变量——数字的位置相关。

因此序列中数的位置k就是“状态”,而状态k对应的“值”,就是以a[k]为“终点”的最长上升子序列的长度。状态一共有N个。

(4)状态转移方程

maxLen (k) 表示以a[k]为“终点”的最长上升子序列的长度,那么:

初始状态: maxLen (1) = 1

  • maxLen(k)=max{maxLen(i):1<=i<k且a[i]<a[k]且k≠1}+1。

    • maxLen(k)就是起点在a[k]左边,终点数值小于a[k],且长度最大的那个上升子序列的长度再加1。此处的的1就是a[k]本身,因为a[k]左边任何终点小于a[k]的子序列,加上a[k]后就能形成一个更长的上升子序列。
  • 若找不到这样的i,则maxLen(k) = 1

为了保证上升子序列尽可能的长,那么就有 dp[ i ]  尽可能的大, 但是在保证 dp[ i ] 尽可能大的基础上,还必须满足序列的上升; 所以 dp[ i ] = max ( 1 , dp[ j ] + 1 ) {  j < i && a[j] < a[i] } 。

  • 这里的1是:当 a[i] 前面的数都比他大的时候,他自己为一个子序列;
  • dp[ j ] + 1 指的是: 当第 i 个数前面有一个 第 j 个数满足 a[j]  <  a[i]  并且 j < i 这时候就说明 a[i] 可以接在 a[j] 后增加子序列的长度。

将 j 从 1 遍历到 i - 1,在这之间,找出尽可能大的dp[ i ]即为最长上升子序列的长度

dp[n] 不一定是最长的子序列,n是数的个数,例如序列 [ 2, 3, 4, 5, 1 ],dp[5] = 1(由子序列[1]构成),然而 dp[4] = 4(由子序列 [2,3,4,5] 构成) )

举个例子:一个序列(7, 9, 6, 10, 7, 1, 3)分别为 (a1, a2, a3, a4, a5, a6, a7)

  • 最开始a1 = 7,  令dp[ 1 ] = 1;
  • 再看a2 = 9,令dp[ 2 ] = 1,那么需要检查a2前面的元素是否有比他小的
    • 因为 a1 < a2 而且 dp[ 1 ] + 1 > dp[ 2 ], 所以dp[ 2 ] = dp[ 1 ] + 1 == 2;
  • 再看a3 = 6,令 dp[ 3 ] = 1, 那么需要检查a3前面的元素a1、a2是否有比他小的
    • 一看没有,那么到目前为止,子序列就是他自己。
  • 再看a4 = 10,令 dp[ 4 ] = 1,那么需要依次检查前面的元素 a1与 a2与 a3是否有比他小的 ,
    • 先看a1,a1<a4,而且dp[ 1 ] + 1 > dp[ 4 ],所以dp[ 4 ] = dp[ 1 ] + 1 == 2,说明此时 a1 与 a4 可以构成一个长度为 2 的上升子序列
    • 再看a2 , a2 < a4,而且dp[ 2 ] + 1 == 3 > dp[ 4 ] == 2,所以dp[ 4 ] = dp[ 2 ] + 1 == 3,  即a4承接在a2后面比承接在a1后更好,承接在a2后面的序列为:a1 a2 a4 ,构成一个长度为 3 的上升子序列
    • 然后再来看 a3 , a3 < a4 但是 d[ 3 ] + 1 == 2  < dp[ 4 ] == 3 ,  所以呢就不能把a4加在a3的后面 。

7.代码:

max(a,b):返回a,b两者之间的较大值
 max_element(r, r+6):返回数组r中[0, 6)之间的最大值的迭代器

#include <iostream>
#include <algorithm>
using namespace std;const int MAX=1010;
int a[MAX];
int maxLen[MAX];
int main()
{int n;cin>>n;for(int i=1;i<=n;i++){cin>>a[i];maxLen[i]=1;}//每次求以第i个数为终点的最长上升子序列的长度for(int i=2;i<=n;i++){//查看以第j个数为终点的最长上升子序列for(int j=1;j<i;j++)if(a[i]>a[j])maxLen[i]=max(maxLen[i],maxLen[j]+1);}cout<<*max_element(maxLen+1,maxLen+n+1);return 0;
}

例题四:数字三角形

1.描述

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

2.输入格式:
 5//三角形行数。下面是三角形

3.输出格式:

30//路径最大和

用二维数组的下三角存放数字三角形(数字三角形的最后一行可以直接存储到D[i][j]中)
D[i][j] :第i行第j个数字(i,j从1开始算)
MaxSum(i, j):从D[i][j]到底边的各条路径长
问题转化为求MaxSum(1,1)

D[i][j]出发,下一步只能走D[i+1][j]或者D[i+1][j+1]

(1)故对于N行的三角形,运用递归:

if (i==N)MaxSum(i,j) = D[i][j];//最后一行
elseMaxSum(i,j) = Max{MaxSum(i+1,j),MaxSum(i+1,j+1)}+ D[i][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;return 0;
}

但这个程序里有大量重复计算,时间复杂度为2^n

(2)改进:

如果每算出一个MaxSum(i,j)就保存起来,下次用到其值的时候直接取用,则可免去重复计算。因为三角形的数字总数是n*(n+1)/2,那么可以用O(n^2)时间完成计算。

#include <iostream>
#include <algorithm>#define MAX 101
using namespace std;int D[MAX][MAX];
int maxSum[MAX][MAX];
int n;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;return 0;
}

(3)递归转为递推

递推过程:

  • 格子中的数字代表此数到底边的路径最大值,可知最后一行到底边的最大值是数字本身
  • 倒数第二行中的第一个数字2,到底边路径的最大值就是max(4,5)+2,即7,同理可算出其它格子中的数字

空间优化:

没必要用二维maxSum数组存储每一个MaxSum(rj),只要从底层一行行向上递推,那么只要一维数组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];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;return 0;
}

例题五 0-1背包问题

详解在我的另一篇博客

应用题

应用一:合唱队形(最长递增、递减序列)

1.描述

N位同学站成一排,音乐老师要请其中的(N-K)位同学出列,使得剩下的K位同学排成合唱队形。合唱队形是指这样的一种队形:设K位同学从左到右依次编号为1,2…,K,他们的身高分别为T1,T2,…,TK,  则他们的身高满足T1<...<Ti>Ti+1>…>TK(1<=i<=K)。已知所有N位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。

2.输入

输入的第一行是一个整数N(2<=N<=100),表示同学的总数。 第二有N个整数,用空格分隔,第i个整数Ti(130<=Ti<=230)是第i位同学的身高(厘米)。

3.输出

输出最少需要几位同学出列。

4.样例输入

8
186 186 150 200 160 130 197 220

5.样例输出

4

6.思路

选中队列中的一个学生,以该学生为核心,分别求出其左侧的最长递增子序列和其右侧的最长递减子序列,两者相加,再减去1就是以该同学为中心的合唱队的人数

我们只需把每个学生都作为中心遍历一遍,就能得出人数最多的合唱队形,再把总人数减去合唱人数就是需要剔除的人数。

7.代码

#include <iostream>
#include <algorithm>
#include <cstdio>using namespace std;
const int MAX=110;
int hign[MAX];
int maxLen[MAX];
int minLen[MAX];
int s1[MAX]={0};
int s2[MAX]={0};
int main()
{int n,sum=0;scanf("%d",&n);for(int i=1;i<=n;i++){scanf("%d",&hign[i]);maxLen[i]=1;minLen[i]=1;}//以第k个学生为中心,k=1~nfor(int k=2;k<=n;k++){for(int i=2;i<=k;i++){for(int j=1;j<i;j++)if(hign[i]>hign[j])maxLen[i]=max(maxLen[i],maxLen[j]+1);s1[i]=max(maxLen[i],s1[i]);}for(int i=k+1;i<=n;i++){for(int j=k;j<i;j++)if(hign[i]<hign[j])minLen[i]=max(minLen[i],minLen[j]+1);s2[i]=max(minLen[i],s2[i]);}}for(int i=1;i<=n;i++)sum=max(s1[i]+s2[i]-1,sum);printf("%d",n-sum);return 0;
}

应用二:数塔问题(数字三角形)

1.描述

设有一个三角形的数塔,顶点为根结点,每个结点有一个整数值。从顶点出发,可以向左走或向右走,如图所示:

若要求从根结点开始,请找出一条路径,使路径之和最大,只要输出路径的和。

2.输入

第一行为n(n<50),表示数塔的层数
从第2行至n+1行,每行有若干个数据,表示数塔中的数值。

3.输出

最大的路径值。

4.样例输入

5
13
11  8
12  7  26
6  14  15  8
12  7  13  24  11

5.样例输出

86

6.思路

用一个二维数组a存储数塔的原始数据(其实我们只使用数组a一半的空间,一个下三角矩阵)

用一个二维数组dp存储每一次决策过程中的结果(也是一个下三角矩阵)

初始化dp,将a的最后一层拷贝到dp中:dp[n][j] = a[n][j] (j = 1, 2, …, n) 其中,n为数塔的层数

递归填上dp数组的值,dp[i][j] = max(dp[i+1][j], dp[i+1][j+1]) + a[i][j],最后的结果保存在dp[0][0]

7.代码

#include <iostream>
#include <cstdio>
using namespace std;const int MAX=55;
int a[MAX][MAX];
int dp[MAX][MAX];
int main()
{int n,tmax;scanf("%d",&n);for(int i=1;i<=n;i++)for(int j=1;j<=i;j++)scanf("%d",&a[i][j]);//初始化dp数组最后一行for(int i=1;i<=n;i++)dp[n][i]=a[n][i];//递归从下到上求dp数组for(int i=n;i>0;i--){for(int j=1;j<=i;j++){tmax=max(dp[i+1][j],dp[i+1][j+1]);dp[i][j]=tmax+a[i][j];}}printf("%d",dp[1][1]);return 0;
}

动态规划(DP)小结相关推荐

  1. 第4课 防卫导弹(第十章 动态规划--DP)

    //progream p10_04 /* 第4课 防卫导弹(第十章 动态规划--DP)  (<聪明人的游戏--信息学探秘 提高篇>) https://blog.csdn.net/weixi ...

  2. 动态规划dp(带模板题の超易懂版):01背包,完全背包,分组背包,多重背包,混合背包

    动态规划dp(带模板题の超易懂版):01背包,完全背包,分组背包,多重背包 01背包 && 完全背包 && 分组背包 の 视频教程:https://www.bilibi ...

  3. 【习题详解】动态规划DP:硬币游戏 蛋糕 游荡的奶牛 决斗

    动态规划DP 硬币 蛋糕塔 游荡的奶牛 格斗 硬币 题目描述 农夫约翰的奶牛喜欢玩硬币游戏,因此他发明了一种称为"Xoinc"的两人硬币游戏. 初始时,一个有N(5 <= N ...

  4. 动态规划: dp+递推——确定动态矩阵dp含义,确定每个状态下面临的选择和对结果值影响,选择符合题意的作为结果存储在dp中

    1.动态规划:每一个状态一定是由之前的状态推导出来的,通过总结归纳发现递推关系 2.解决动态规划问题的步骤: 确定dp数组(dp table)以及下标的含义: 每个单元内 题目所求的值,一维.二维 确 ...

  5. 省选+NOI 第一部分 动态规划DP

    期望概率DP [整理]简单的数学期望和概率DP [整理]简单的数学期望和概率DP - nimphy - 博客园 期望&概率dp总结 期望&概率dp总结_十分残念的博客-CSDN博客 期 ...

  6. dpresult在python中什么意思_动态规划(DP)的整理-Python描述

    今天整理了一下关于动态规划的内容,道理都知道,但是python来描述的方面参考较少,整理如下,希望对你有所帮助,实验代码均经过测试. 请先好好阅读如下内容--什么是动态规划? 摘录于<算法图解& ...

  7. 动态规划 背包问题小结 0-1背包(采药 九度第101题) 完全背包(Piggy-Bank POJ 1384) 多重背包(珍惜现在,感恩生活 九度第103题)

    本小结介绍0-1背包.完全背包以及多重背包问题 记忆要点: 0-1背包:二维数组情况下,顺序遍历体积或者倒序均可以                降维情况下需倒序遍历体积 完全背包:数组降维+顺序遍历 ...

  8. 动态规划(DP)的整理-Python描述

    今天整理了一下关于动态规划的内容,道理都知道,但是python来描述的方面参考较少,整理如下,希望对你有所帮助,实验代码均经过测试. 请先好好阅读如下内容–什么是动态规划? 摘录于<算法图解&g ...

  9. Datawhale编程——动态规划DP

    0-1背包问题 问题:有n个物品,第i个物品价值为vi,重量为wi,其中vi和wi均为非负数,背包的容量为W,W为非负数.现需要考虑如何选择装入背包的物品,使装入背包的物品总价值最大. 针对这个经典的 ...

  10. 《强化学习》中的 时序差分学习 Temporal-Difference Learning (基于与动态规划 DP 、蒙特卡洛方法 MC 的对比)

    前言: 学习了 Sutton 的<强化学习(第二版)>中时序差分学习的"预测"部分内容.前两章中,书介绍了 动态规划 与 蒙特卡洛方法 ,我们从二者与 时序差分学习 的 ...

最新文章

  1. ASP.NET多种不同页面间数据传递的方法
  2. 高速旋转中隐藏着许多鲜为人知的秘密,这些你都知道吗?
  3. 卸载idea_IDEA 不为人知的 5 个骚技巧!真香!
  4. linux 如何查看fb中分辨率_西门子S71200,如何在FB块中使用操作定时器?
  5. 这个是我学习的所有sql语句的写法
  6. paip.输入法编程----一级汉字1000个
  7. html5新年拜年模板,关于春节拜年日记模板合集7篇
  8. Linux下安装字体后刷新字体
  9. 微信手机号授权解密失败问题现象和解决方法: getPhoneNumber
  10. 在UniApp的H5项目中,生成二维码和扫描二维码的操作处理
  11. 如何用云服务器进行深度学习
  12. 合宙air105上手记录
  13. iOS 手机安装Charles证书失败解决方法
  14. python bytes与str之间的转换 hex的字符转换
  15. 人工智能实战第三次作业 焦宇恒
  16. Android 安卓状态栏工具类(沉浸式、字体颜色等)
  17. Android作业:设计一个能在图片上涂鸦的程序
  18. Chameleon跨端框架——一个理想主义团队的开源作品
  19. keras的简单介绍
  20. Gitee第三方登录

热门文章

  1. Rouge的安装与使用
  2. 一周 Go World 新鲜事-2019W11
  3. Chinese New Year
  4. windows下CUDA的卸载,怒推!!!亲测可以!!!
  5. [译]5步分析pdf文件
  6. 2019北航夏令营保研经验及体会
  7. Linux 指令学习之ping
  8. 彻底搞懂递归的时间复杂度
  9. FHQ Treap 详解
  10. C语言常用的math函数