【动态规划】五千字的入门博客,一篇文章讲完动态规划

【动态规划】五千字的入门博客,一篇文章讲完动态规划,第1张

动态规划之经典线性递归

​ 动态规划(英语:Dynamic programming,简称DP)是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。

​ 动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。

​ 动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再根据子问题的解以得出原问题的解。

​ 以上定义来源于维基百科,老规矩,直接来做板子题。

以下共四道板子题:

1.数字金字塔

2.最长上升子序列

3.最长公共子序列

4.最简单的01背包

板子题1-金字塔路径: 题目描述

观察下面的数字金字塔。

写一个程序来查找从最高点到底部任意处结束的路径,使路径经过数字的和最大。每一步可以走到左下方的点也可以到达右下方的点。

        7 
      3   8 
    8   1   0 
  2   7   4   4 
4   5   2   6   5 

在上面的样例中,从 7→3→8→7→5 的路径产生了最大

输入格式

第一个行一个正整数 r ,表示行的数目。

后面每行为这个数字金字塔特定行包含的整数。

输出格式

单独的一行,包含那个可能得到的最大的和。

输入
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5 
**输出 **
30
说明/提示

【数据范围】
对于 100% 的数据,1≤r≤1000,所有输入在 [0,100] 范围内。


思路解析

这道题要求的是从金字塔顶端到底端的最长路径。

主要思路就是动态规划。

上文提到,动态规划的核心思路是把一个大问题分解成若干个简单的、可重复的子问题。

以这道题为例,所谓的大问题是“获取最短路径”,而小问题则是“选择数字,并使得本次选择能使总数字最小”。

(本题并不适用于贪心,虽然贪心也要把大问题分解为若干个小问题,但是贪心的核心条件是,“每一步不能对后一步有影响”,这也就是“贪心选择性质”。)

        7 #如果按照贪心,我们从7开始,就是7-8-1-7-5,比7-3-8-7-5要小。
      3   8 
    8   1   0 
  2   7   4   4 
4   5   2   6   5 

我们先从7试试。

如果从7开始,第一步可以选择(3,8)。

但是,下一步是从3开始还是从8开始呢?答案是不一定,在这个 *** 作的时候还不能确定。

刚刚已经分析过了,这个题不具备贪心选择性质,因此我们需要把选择的结果作为一个状态保存下来。

肯定不能保存在7这个位置上,一个位置咋可能保存俩信息,所以保存在3,8上。

那继续分析,比如用倒数第二层的7来看。

这个位置的下一层(也就是倒数第一层)应该选择谁?显然是5。

所以,从最后一层起,每层都选取它下两个数中大的那一个。(因为无论选择哪一个,都不影响后续,后续都是从这个数开始)

如果我们最后一层开始,那么到7这个位置就好选了:选择(3,8)这两个位置中,从其自身开始的路径最大的位置。

接下来说一说代码。

我们用dp[i][j]来标记数组中第i位,并且从倒数第二排开始遍历,使得每一位数值等于其下端两位数字的最大值。

也就是说,dp[i][j] += max( dp[i+1][j], dp[i+1][j+1])

输入输出部分
#5			#第一个数字为行数
#7			#第一行,1个元素
#3 8		#第二行,2个元素
#8 1 0		#第三行,3个元素
#2 7 4 4	#第四行,4个元素
#4 5 2 6 5 	#第五行,5个元素
#输入格式,第一个部分为行数
n = int(input())
#而后输入n行元素,我们用列表来存储
ls = []
for i in range(n):
    ls.append( list( map(int, input().split() ) ) )
#好了,到这里,数据已经以
#ls = [[7], [3,8], [8,1,0], [2,7,4,4], [4,5,2,6,5]]的形式存储了。
#
#
#代码部分
#
#
#然后考虑输出,由于我们的思路是从下到上,所以最后ls[0][0]的位置就应该是答案
print(ls[0][0])
核心代码
for i in range(n - 2, -1, -1):
#外层循环,倒数第二层到0层
    for j in range(i+1):
        #内层循环,第一个元素到本层最后一个
        ls[i][j] += max(ls[i+1][j], ls[i+1][j+1])#状态转移方程
汇总代码
n = int(input())
ls = []
for i in range(n):
    ls.append( list( map(int, input().split() ) ) )
for i in range(n - 2, -1, -1):
    for j in range(i+1):
        ls[i][j] += max(ls[i+1][j], ls[i+1][j+1])
print(ls[0][0])
评测链接

【洛谷P1216】https://www.luogu.com.cn/problem/P1216

板子题2-最长上升子序列: 题目描述

某国为了防御敌国的导d袭击,发展出一种导d拦截系统。但是这种导d拦截系统有一个缺陷:虽然它的第一发炮d能够到达任意的高度,但是以后每一发炮d都不能高于前一发的高度。某天,雷达捕捉到敌国的导d来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导d。

输入导d依次飞来的高度(雷达给出的高度数据是≤50000的正整数),计算这套系统最多能拦截多少导d,如果要拦截所有导d最少要配备多少套这种导d拦截系统。

输入格式

1行,若干个整数(个数≤100000)

NOIP 原题数据规模不超过 2000。

输出格式

2行,每行一个整数,第一个数字表示这套系统最多能拦截多少导d,第二个数字表示如果要拦截所有导d最少要配备多少套这种导d拦截系统。

输入
389 207 155 300 299 170 158 65
**输出 **
6
2
说明/提示

为了让大家更好地测试n方算法,本题开启spj,n方100分,nlogn200分

每点两问,按问给分


思路解析

这道题的板子题其实是最长上升序列,但是这个,嘶,只找到了这种描述的,没有找到纯板子的那种。

那就还是先读一下题意。

虽然它的第一发炮d能够到达任意的高度,但是以后每一发炮d都不能高于前一发的高度。(递减数列嘛)

某天,雷达捕捉到敌国的导d来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导d。

1.计算这套系统最多能拦截多少导d。 (也就是最长的不上升序列)

2.如果要拦截所有导d最少要配备多少套这种导d拦截系统。(也就是最长的上升序列)

其实这个题的最优时间复杂度是O(NlogN),但是我们现在只介绍N^2的算法。

还是之前的思路,我们要解决的问题是,从一个序列中找到最长的上升序列。

对于1243这个序列而言,它的最长上升子序列长度和124这个序列是一样的。

因此,我们令dp[i]表示以第i位结尾的最长上升子序列长度。

那么,在123到1243的这个时候发生了什么呢?

我们首先判断了1、2、4的大小,把比3小的拿了出来:[1,2]

然后,在1,2中选择其更长的,接在3前面。所以,最后的答案是123。

综上所述:

DP维度:一维

数组初始化:初始化为1(每个位置各是一个长度)

DP数组含义:dp[i]代表以ls[i]结尾的最长上升序列长度

状态转移方程:dp[i] = max( dp[j] ) (j ∈ {0,1,2…i}, ls[j] < dp[j] )

输入输出部分
#输入的是一行数,那就不多说了。
ls = list( map( int, input().split() ) )
#
#
#输出的就是数列里最大的值
print(max(dp))
print(max(dp_2))
核心代码
dp = [1] * len(ls)#因为我们这个题求了两个东西,所以声明两个数组,初始化为1
dp_2 = [1] * len(ls)
for i in range(len(ls)):#循环
    maxn = 0
    maxn_2 = 0#用一个变量标记本次循环中,前面最大的值
    for j in range(i):
        if ls[j] >= ls[i] and dp[j] > maxn:#如果不递增
            maxn = dp[j]
        if ls[j] < ls[i] and dp_2[j] > maxn_2:#如果递增
            maxn_2 = dp_2[j]
    dp[i] += maxn#更新两个dp数组
    dp_2[i] += maxn_2
汇总代码
ls = list( map( int, input().split() ) )
dp = [1] * len(ls)
dp_2 = [1] * len(ls)
for i in range(len(ls)):
    maxn = 0
    maxn_2 = 0
    for j in range(i):
        if ls[j] >= ls[i] and dp[j] > maxn:
            maxn = dp[j]
        if ls[j] < ls[i] and dp_2[j] > maxn_2:
            maxn_2 = dp_2[j]
    dp[i] += maxn
    dp_2[i] += maxn_2
print(max(dp))
print(max(dp_2))
评测链接

【洛谷P1020】https://www.luogu.com.cn/problem/P1020

但是,只能拿到70分,时间复杂度更优的算法以后会写~

板子题3-最长公共子序列: 题目描述

给出 1,2,…,n 的两个排列,P1和P2,求它们的最长公共子序列。

输入格式

第一行是一个数 n。

接下来两行,每行为 n 个数,为自然数 1,2,…,n 的一个排列。

输出格式

一个数,即最长公共子序列的长度。

输入
5 
3 2 1 4 5
1 2 3 4 5
**输出 **
3
说明/提示
  • 对于 50% 的数据, n≤10^3;
  • 对于 100%的数据, n≤10^5。

思路解析

这个找到了板子题!

我们要解决的问题是,两个序列的最长公共序列。

比如

序列一为 1, 2, 7, 6, 3

序列二为 1, 7, 6, 3, 2

此时,一维的dp肯定是不够的,因为我们有两个序列,不能用一个序列装下。

所以,我们用dp[i][j]来表示ls1[:i]和ls2[j:]的公共子序列。

我们随便找个中间态来模拟一下。

假设我们现在要求解的状态是len(ls1) = 4, len(ls2) = 3

那么,在上一个状态转移到下一个状态的过程中,我们需要判断新增的元素能不能添加进已有最长公共序列的末尾。

它的上一个状态会是什么呢?答案是不一定。

len(ls1) = 4, len(ls2) = 3

它可以由len(ls1) = 3, len(ls2) = 3和len(ls1) = 4, len(ls2) = 2继承而来(这两个状态都可以添加一个元素以变成当前状态)

而这两个在dp数组中存储的数值可能是不一样的,要判断一下。

因此,我们要比较ls1[4]和ls2[3]是否相等,如果相等,说明此位置可以加入新公共序列

如果不相等,就只能继承能达到此状态的最有状态。

数组初始化:没啥用,继承

DP数组含义:dp[i][j]来表示ls1[:i]和ls2[j:]的公共子序列长度。

状态转移方程:dp[i][j] = {继承 max(dp[i-1][j],d[i][j-1]) , 如果相等再加一}

输入输出部分
#先输入一个n
n = int(input())#但是这玩意没啥用,毕竟我们用的是Python,顶多不用求len()了
ls1 = list( map( int, input().split() ) )
ls2 = list( map( int, input().split() ) )
#
#核心代码
#
#输出的答案应该是dp[n][n]的数值
print(dp[n][n])
核心代码
dp = [[ [0,""] for j in range(n+5)] for i in range(n+5)]#顺便保存了一下序列

for i in range(1, n+1):
    for j in range(1, n+1):
        dp[i][j][0] = max(dp[i-1][j][0], dp[i][j-1][0])#状态转移方程
        dp[i][j][1] = dp[i-1][j][1] if dp[i-1][j][0] > dp[i][j-1][0] else dp[i][j-1][1]
        if ls1[i-1] == ls2[j-1]:
            dp[i][j][0] += 1#更新
            dp[i][j][1] += ls1[i-1]
汇总代码
n = int(input())
ls1 = list( input().split() )
ls2 = list( input().split() )
dp = [[ [0,""] for j in range(n+5)] for i in range(n+5)]
for i in range(1, n+1):
    for j in range(1, n+1):
        dp[i][j][0] = max(dp[i-1][j][0], dp[i][j-1][0])
        dp[i][j][1] = dp[i-1][j][1] if dp[i-1][j][0] > dp[i][j-1][0] else dp[i][j-1][1]
        if ls1[i-1] == ls2[j-1]:
            dp[i][j][0] += 1
            dp[i][j][1] += ls1[i-1]
#输出的答案应该是dp[n][n]的数值
print(dp[n][n][0])
print(dp[n][n][1])
评测链接

【洛谷P1439】https://www.luogu.com.cn/problem/P1439

但是,看数据范围,我们的算法只能拿到50分,时间复杂度更优的算法以后会写~

板子题2/3-思路调优: 时间复杂度分析

首先,目前最长上升子序列的时间复杂度为n2,最长公共子序列也是n2

但是他们都有各自的nlogn算法:二分+动态规划

这部分内容我会陆续在我的个人博客更新~

转换思路

思考一下,对[4,1,5,2,3]求最长递增序列,是不是就是对这个队列求他和[1,2,3,4,5]的最长公共子序列?

所以,也可以自定义”大小比较“,将公共子序列转化为上升子序列问题。

板子题4-最简单的01背包: 题目描述

辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”

如果你是辰辰,你能完成这个任务吗?

输入格式

输入第一行有两个整数,分别代表总共能够用来采药的时间 t 和代表山洞里的草药的数目 m。

第 2 到第 (m+1) 行,每行两个整数,第 (i + 1)行的整数 a_i, b_i分别表示采摘第 i 种草药的时间和该草药的价值。

输出格式

输出一行,这一行只包含一个整数,表示在规定的时间内,可以采到的草药的最大总价值。

输入
70 3
71 100
69 1
1 2
**输出 **
140
说明/提示
  • 对于 30% 的数据,保证m <10^3。
  • 对于 100%的数据,保证 1≤m≤10^4, 1≤t≤10^7,且 1≤m×t≤10^7, 1≤ ai, bi ≤10^4
思路解析

一个背包问题。

01背包问题的常见模板是什么样子的呢?

1.给定若干个物品,每个物品都有一些属性,比如价值和重量

2.每个物品都有两种可能,选还是不选

3.我们的背包可能只能装50重量,选择价值尽可能多的物品

这个问题还是可以用动态规划来解决,因为每一个物品都可以”选“或者”不选“,这就是重复的子问题。

我们现在要理解一个概念:

假设,一共有五个物品:

A,价值3,重量1

B,价值1,重量2

C,价值8,重量3

D,价值2,重量1

E,价值4,重量2

你一共可以选择五件(一共就五件嘛),总重量不能超过7

#下标从一开始

首先我们先设个状态,比如dp[i]][j]代表前i件物品、总重量不超过j的最大价值。(所以答案就是dp[5][7]

但是,他肯定不能只存储这一个信息,因为还有个信息是重量。那我们就可以令dp[i] = [value, weight],来计算和存储。

我们假设,只有前四件物品可以拿,现在要准备拿第五件,这两个状态转移过程中发生了什么呢?

dp[5],唯一要决定的事情是第五件物品拿还是不拿。

如果第五件物品拿,那么dp[5][7] = dp[4][7-2(E的重量)]+value[5]

如果第五件物品不拿,那么dp[5][7] = dp[4][7]

所以,我们就要比较一下这俩,取一个max值就可以了。

所以我们很轻易的得到了状态转移方程:

数组初始化:没啥用

DP数组含义:dp[i][j]来表示ls1[:i]和ls2[j:]的公共子序列长度。

状态转移方程:dp[i][j] = max(dp[i-1][j],d[i-1][j-weight[i]] + value[i])

输入输出部分
#输入第一行有两个整数,分别代表总共能够用来采药的时间 t 和代表山洞里的草药的数目 m。
t, m = map(int, input().split())
#第 2 到第 (m+1) 行,每行两个整数,第 (i + 1)行的整数 a_i, b_i分别表示采摘第 i 种草药的时间和该草药的价值。
value = [0]
time = [0]
for i in range(m):
    t_i, v_i = map(int, input().split())
    value.append(v_i)
    time.append(t_i)
#
#    核心代码
#
#输出就是dp[m][t]
print(dp[m][t])
核心代码
dp = [[0] * (m + t)]* (m + t)
for i in range(n):
    for j in range(n):
        dp[i][j] = max(dp[i-1][j], d[i-1][j-time[i]] + value[i])for i in range(1,m+1):#判断第i件物品
    for j in range(1,t+1):#在time剩余j的情况下
        if j - time[i] >= 0:#如果时间够
            dp[i][j] = max(dp[i-1][j], dp[i-1][j-time[i]] + value[i])
        else:
            dp[i][j] = dp[i-1][j]
汇总代码
t, m = map(int, input().split())
value = [0]
time = [0]
for i in range(m):
    t_i, v_i = map(int, input().split())
    value.append(v_i)
    time.append(t_i)
dp = [[ 0 for j in range(t+2)] for i in range(m+2)]
for i in range(1,m+1):
    for j in range(1,t+1):
        if j - time[i] >= 0:
            dp[i][j] = max(dp[i-1][j], dp[i-1][j-time[i]] + value[i])
        else:
            dp[i][j] = dp[i-1][j]
print(dp[m][t])
评测链接

【洛谷P048】https://www.luogu.com.cn/problem/P1048


更多算法,详见个人博客,[http://fengjx.cn]

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/1353618.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-14
下一篇 2022-06-14

发表评论

登录后才能评论

评论列表(0条)

保存