目录
一、前言
二、最长公共子序列(lanqiaoOJ题号1189,类似于1054)
三、最长递增子序列
1、蓝桥骑士(lanqiaoOJ题号1188)
四、编辑距离
1、字符串转换(lanqiaoOJ题号1507)
五、网络图上的DP
1、过河卒(lanqiaoOJ题号755)
六、其他例题
1、排列数(2019年国赛,lanqiaoOJ题号240)
1)暴力法
2)DP
2、砝码称重(2021年省赛,lanqiaoOJ题号1447)
3、数字三角形(2020年省赛,lanqiaoOJ题号505)
一、前言
本文主要讲了关于线性DP的一些例题。
二、最长公共子序列(lanqiaoOJ题号1189,类似于1054)
【题目描述】
给定一个长度为 n 数组 A 和一个长度为 m 的数组 B。请你求出它们的最长公共子序列长度为多少。
【输入描述】
输入第一行包含两个整数 n、m。第二行包含 n 个整数 ai,第三行包含 m 个整数 bi, 1≤n, m≤10^3, 1≤ai, bi≤10^9。
【输出描述】
输出一行整数表示答案。
- 一个给定序列的子序列,是在该序列中删去若干元素后得到的序列。例如:X = {A, B, C, B, D, A, B}、它的子序列有 {A, B, C, B, A}、{A, B,D}、{B, C, D, B} 等。
- 给定两个序列 X 和 Y,当另一序列 Z 既是 X 的子序列又是 Y 的子序列时,称 Z 是序列 X 和 Y 的公共子序列。
- 最长公共子序列是长度最长的子序列。
- 暴力法:先找出 A 的所有子序列,然后一一验证是否为 Y 的子序列。
- 如果 A 有 m 个元素,那么 A 有 2^m 个子序列;B 有 n 个元素;总复杂度大于 O(n2^m)。
【动态规划】
dp[i][j]:序列 Ai(a1~ai) 和 Bj(b1~bj)的最长公共子序列的长度。答案: dp[n][m]
分解为 2 种情况:
1)当 ai=bj 时,已求得 Ai-1 和 Bj-1 的最长公共子序列,在其尾部加上 ai 或 bj 即可得到 Ai 和 Bj 的最长公共子序列。状态转移方程:dp[i][j]=dp[i-1][j-1]+1
2)当 ai≠bj 时,求解两个子问题:Ai-1 和 Bj 的最长公共子序列;Ai 和 Bj-1 的最长公共子序列。取最大值,状态转移方程:dp[i][j]=max{dp[i][j-1], dp[i-1][j]}
3)复杂度 O(nm)
上述解释通俗易懂啊!
【用交替滚动数组】
1)当 ai=bj 时,状态转移方程:dp[i][j]=dp[i-1][j-1]+1
2)当 ai≠bj 时,状态转移方程:dp[i][j]=max{dp[i][j-1], dp[i-1][j]}
n,m=map(int,input().split())
a=[0]+list(map(int,input().split()))
b=[0]+list(map(int,input().split()))
dp=[[0]*(m+1) for _ in range(2)] #注意这里是m
now=0
old=1
for i in range(1,n+1):
now,old=old,now
for j in range(1,m+1):
dp[now][j]=max(dp[now][j-1],dp[old][j])
if a[i]==b[j]:
dp[now][j]=max(dp[now][j],dp[old][j-1]+1)
print(dp[now][m])
三、最长递增子序列
1、蓝桥骑士(lanqiaoOJ题号1188)
【题目描述】
小明是蓝桥王国的骑士,他喜欢不断突破自我。这天蓝桥国王给他安排了 N 个对手,他们的战力值分别为 a1, a2, ..., an,且按顺序阻挡在小明的前方。对于这些对手小明可以选择挑战,也可以选择避战。身为高傲的骑士,小明从不走回头路,且只愿意挑战战力值越来越高的对手。请你算算小明最多会挑战多少名对手。
【输入描述】
第一行是整数 N,表示对手的个数,第二行是 N 个整数 a1, a2, ..., an,表示对手战力值。1 ≤ N ≤ 3×10^5
【输出描述】
输出一行整数表示答案。
【分析】
给定一个长度为 n 的数组,找出一个最长的单调递增子序列。
例:序列 A={5, 6, 7, 4, 2, 8, 3},它最长的单调递增子序列为{5, 6, 7, 8},长度为 4。
定义状态dp[i]:表示以第 i 个数为结尾的最长递增子序列的长度。
状态转移方程:dp[i] = max{dp[j]} + 1,0<j<i,Aj<Ai
答案:max{dp[i]}
复杂度:j 在 0~i 之间滑动,复杂度 O(n);i 的变动范围也是 O(n) 的;总复杂度 O(n^2)。
动态规划:复杂度 O(n^2)
本题 n<=3×10^5,DP 代码提交到 OJ 会超时。
DP 不是 LIS 问题的最优解法,有复杂度 O(nlogn) 的非DP解法
这题就此打住。
四、编辑距离
1、字符串转换(lanqiaoOJ题号1507)
【题目描述】
给定两个单词 A 和 B,计算出将 A 转换为 B 所需的最小操作数。一个单词允许进行以下 3 种操作: 1)插入一个字符;2)删除一个字符;3)替换一个字符。
【输入描述】
输入第一行包含一个字符串 S。输入第二行包含一个字符串 T。1<=|S|, IT|<=2×10^3,保证 S、T 只包含小写字母。
【输出描述】
输出一个整数表示答案。
【分析】
- 把长度 m 的 A 存储在数组 a[1]~a[m],长度为 n 的 B 存储在 b[1]~b[n],不用 a[0] 和 b[0]。
- 定义状态 dp:dp[i][j] 表示 A 的前 i 个字符转换 B 的前 j 个字符所需要的操作步骤
- dp[m][n] 就是答案
下图是 A="abcf",B="bcfe" 的状态转移矩阵。
状态转移方程:
1)若 a[i] = b[j],则 dp[i][j] = dp[i-1][j-1]。例如图中 dp[2][1] 处的箭头。
2)其他情况:dp[i][j] = min{dp[i-1][j-1], dp[i-1][j], dp[i][j-1]} + 1。例如图中 dp[4][2] 处的箭头。dp[i][j] 是它左、左上、上的三个值中的最小值加1,分别对应以下操作:
- dp[i-1][j]+1,删除,将 A 的最后字符删除;
- dp[i][j-1]+1,插入,在 B 的最后插入A的最后字符;
- dp[i-1][j-1]+1,替换,将 B 的最后一个字符替换为 A 的最后一个字符。
复杂度:0(mn)。
a=input()
a=' '+a #a[0]不用
b=input()
b=' '+b
m=len(a)-1
n=len(b)-1
dp=[[0]*(n+1) for _ in range(m+1)]
for i in range(1,m+1):
dp[i][0]=i
for j in range(1,n+1):
dp[0][j]=j
for i in range(1,m+1):
for j in range(1,n+1):
if a[i]==b[j]:
dp[i][j]=dp[i-1][j-1]
else:
dp[i][j]=min(min(dp[i-1][j],dp[i][j-1]),dp[i-1][j-1])+1
print(dp[m][n])
五、网络图上的DP
1、过河卒(lanqiaoOJ题号755)
【题目描述】
棋盘上 A 点有一个过河卒,需要走到目标 B 点。卒行走的规则:可以向下、或者向右。同时在棋盘上 C 点有一个对方的马,该马所在的点和所有跳跃一步可达的点称为对方马的控制点。因此称之为 “马拦过河卒”。现在要求你计算出卒从 A 点能够到达 B 点的路径的条数,假设马的位置是固定不动的,并不是卒走一步马走一步。棋盘用坐标表示,A 点 (0, 0)、B 点 (n,m),同样马的位置坐标是需要给出的。1<=n, m<=20,0<=马的坐标<=20。
【输入格式】
一行四个正整数,表示 B 点坐标和马的坐标。
【输出格式】
一个整数,表示所有的路径条数。
- 统计路径条数,看起来是个搜索题,可以用 DFS 求解。把马的控制点标记为不走,绕过它们。不过,用 DFS 搜索的路径数量是天文数字,肯定超时。
- 在小学上过奥数的都知道,这题应该用 “标数法”,就是在每个坐标点上记录能走的路径条数。
- 标数法实际上就是DP的递推。
【DP】
- 定义状态 dp[][]:dp[i][j] 表示卒走到坐标 (i, j) 时能走的路径条数。
- 如果不考虑马的控制点,有:dp[i][j]=dp[i-1][j] + dp[i][j-1];
- 也就是 (i, j) 点的路径条数等于它上面和左边的路径条数之和。这就是小学奥数的 “标数法” 的原理。
- 本题的限制条件是马的控制点,只要令控制点的 dp[i][j]=0 即可,即这个点上无路径。
小技巧:把坐标加2,防止马的控制点越界。
dp=[[0]*25 for i in range(25)]
s=[[0]*25 for i in range(25)]
bx,by,mx,my=map(int,input().split())
bx+=2
by+=2
mx+=2
my+=2
dp[2][1]=1
s[mx][my]=1
s[mx-2][my-1]=1
s[mx-2][my+1]=1
s[mx+2][my-1]=1
s[mx+2][my+1]=1
s[mx-1][my-2]=1
s[mx-1][my+2]=1
s[mx+1][my-2]=1
s[mx+1][my+2]=1
for i in range(2,bx+1):
for j in range(2,by+1):
if s[i][j]==1:
dp[i][j]=0
else:
dp[i][j]=dp[i-1][j]+dp[i][j-1]
print(dp[bx][by])
这类题其实是最好做了。
六、其他例题
1、排列数(2019年国赛,lanqiaoOJ题号240)
【题目描述】
在一个排列中,一个折点是指排列中的一个元素,它同时小于两边的元素,或者同时大于两边的元素。对于一个 1~n 的排列,如果可以将这个排列中包含 t 个折点,则它称为一个 t+1 单调序列。例如,排列 (1, 4, 2, 3) 是一个 3 单调序列,其中 4 和 2 都是折点。给定 n 和 k,请问 1~n 的所有排列中有多少个 k 单调队列?
【输入描述】
输入一行包含两个整数 n, k (1<=k<=n<=500)。
【输出描述】
输出一个整数表示答案。答案可能很大,你可需要输出满足条件的排列数量除以 123456 的余数即可。
1)暴力法
20% 的测试: 1<=k<=n<=10
暴力法:对所有排列进行检查,判断是否为 k 单调队列。
from itertools import *
n,k=map(int,input().split())
nums=[i for i in range(1,n+1)] #1~n
cnt=0
for num in permutations(nums): #检查每个排列
tmp=0
for i in range(n-2):
if num[i+1]>num[i+2] and num[i+1]>num[i]:
tmp+=1 #凸折点
elif num[i+1]<num[i+2] and num[i+1]<num[i]:
tmp+=1 #凹折点
if tmp==k-1:
cnt+=1
print(cnt%123456)
2)DP
定义dp[][]:dp[i][j] 表示序列包含 1~i,且排列为 j 单调队列的方案数,也就是含有 j-1 个折点的方案数。
答案: dp[n][k]
状态转移方程:
从 dp[i-1][] 递推到 dp[i][],把 i 插入到 1~i-1 的一个排列中,折点数量的变化:
dp[i][j] = dp[i-1][j]*j + dp[i-1][j-1]*2 + dp[i-1][j-2]*(i-j)
(怎么退出状态转移方程是关键)
N=520
dp=[[0]*N for i in range(N)]
n,k=map(int,input().split())
dp[1][1]=1
dp[2][1]=2
for i in range(3,n+1):
ki=min(k,i)
for j in range(1,ki+1):
dp[i][j]+=dp[i-1][j]*j+dp[i-1][j-1]*2
if j>1:
dp[i][j]+=dp[i-1][j-2]*(i-j)
print(dp[n][k]%123456)
2、砝码称重(2021年省赛,lanqiaoOJ题号1447)
【题目描述】
你有一架天平和 N 个砝码,这 N 个砝码重量依次是 W1, W2, ······, WN。请你计算一共可以称出多少种不同的重量?注意砝码可以放在天平两边。
【输入描述】
输入的第一行包含一个整数 N。第二行包含 N 个整数: W1, W2, W3, ······, WN。
【输出描述】
输出一个整数代表答案。对于所有评测用例,1≤N≤100, N 个砝码总重不超过 100000。
【DP】
建模:给定 n 个正整数,从中选出若个数字组合,每个数字可以加或者减,最终能得到多少种正整数结果。
DP状态定义:dp(i, j) 表示前 i 个数字选择若干个加或者减,能否获得和为 j。
DP状态转移方程:dp(i, j)=dp(i-1, j)|dp(i-1, j-wi)|dp(i-1, j+wi)
状态转移方程中有三种情况:
dp(i-1, j):不用第 i 个数字,和为 j
dp(i-1, j-wi):用第 i 个数字,且做减法,等价于用 i-1 个数字实现 j-wi;
dp(i-1, j+wi):用第 i 个数字,且做加法,等价于用 i-1 个数字实现 j+wi;
这一题也可以直接模拟,并且用 set 判重。
n=int(input())
w=list(map(int,input().split()))
ans=set()
ans.add(w[0])
for i in w[1:]:
for j in ans.copy():
ans.add(i)
ans.add(j+i)
if j-i!=0:
ans.add(abs(j-i))
print(len(ans))
3、数字三角形(2020年省赛,lanqiaoOJ题号505)
【题目描述】
图中给出了一个数字三角形。从三角形的顶部到底部有很多条不同的路径。对于每条路径,把路径上面的数加起来可以得到一个和,你的任务就是找到最大的和。路径上的每一步只能从一个数走到下一层和它最近的左边的那个数或者右边的那个数。此外,向左下走的次数与向右下走的次数相差不能超过 1。
【输入格式】
输入的第一行包含一个整数 N(1<N <= 100),表示三角形的行数。下面的 N 行给出数字三角形。数字三角形上的数都是 0 至 100 之间的整数。
【输出格式】
输出一个整数,表示答案。
【输入】
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
【输出】
27
本题要求向左走的次数与向右走的次数差值不超过 1,当到达最后一层时,一定是落在中间位置。
如果层数是奇数,最后一层落在正中间元素上;
如果层数是偶数,最后一层落在第 N/2 或第 N/2+1 个元素上。
定义状态 dp[][],dp[i][j] 表示从顶部到第 i 层横向第 j 个数,最大路径和。它只能从上一层的左边或右边转移而来。
n=int(input())
a=[list(map(int,input().split())) for i in range(n)]
#数组a[][]同时当成dp[][]用
for i in range(1,n):
for j in range(0,i+1):
if j==0:
a[i][j]+=a[i-1][j] #最左边元素
elif j==i:
a[i][j]+=a[i-1][j-1] #最右边元素
else:
a[i][j]+=max(a[i-1][j-1:j+1])
if n&1:
print(a[-1][n//2])
else:
print(max(a[-1][n//2-1],a[-1][n//2]))
补充练习题:
以上,线性DP与真题
祝好