本文大约
3×103 个字,阅读起来大约需要
10 分钟。
DP 题目一般都是有 DP 的状态,状态转移方程和答案组成,我们每个 DP 的例题都会进行详解。
【例 1】(01 背包问题)你有
n 个物品和一个载重为
m 的背包,第
i 个物品的重量为
wi,价值为
vi,求在重量限制下的价值最大和。
针对
30% 的数据:
n,m≤20。
针对
50% 的数据:
n,m≤50。
针对
100% 的数据:
n≤1000,m≤10000。
算法 1:
我会搜索(或我会状压)!考虑直接爆搜,选择每个物品是否选,再判断是否满足重量限制,时间复杂度
O(2n),可以通过
30 分。
算法 2:
我会剪枝!考虑剪枝,如果当前的重量
>m,那么就剪枝。这种叫可行性剪枝,如果最小的重量也大于
m,那么直接输出
0,可能通过
50 分。
算法 3:
我会 DP!我们设
dpi,j 表示到第
i 个数,背包重量为
j 时的最大价值和,则
dpi,j=dpi−1,k (
j<wi),
dpi,j=max(dpi−1,j+dpi−1,j−wi)(
j≥wi)。时间复杂度
O(nm),可以通过满分。
【例 2】(最长上升子序列)一个序列
a1,a2,a3,⋯,an,选出其子序列
b∈a,且
b1<b2<...<bl,求
l 的最大值。
针对
40% 的数据:
n≤20。
针对
100% 的数据:
n≤10000。
算法 1:我会搜索!直接判断每个数选不选。
算法 2:我会 DP!假设
i 的上一个数为
j,
j<i,则
aj<ai,
dpi=dpj+1。
思考题
n≤106。
【例 3】(最长上升子序列计数)一个序列
a1,a2,a3,⋯,an,选出其子序列
b∈a,且
b1<b2<...<bl,当
l 取最大值时有多少种方案。
针对
100% 的数据:
n≤10000,答案在 long long 范围内。
很容易计算最长上升子序列长度,然后计数就是当
dpi=dpj+1 时
fi=∑fj。
答案即为
fn。时间复杂度
O(n2)。
思考题
n≤106。
【例 4】(最长公共子序列)有两个字符串
s,t,
u∈s,v∈t 且
u=v,则称
u,v 为字符串
s,t 的公共子序列,求最长的公共子序列长度。
针对
10% 的数据:
∣s∣≤10,∣t∣≤10。
针对
30% 的数据:
∣s∣≤20,∣t∣≤20。
针对
100% 的数据:
∣s∣≤2000,∣t∣≤2000。
算法
1:我会暴力!直接枚举所有
s,t 的子序列,询问是否相等,时间复杂度
O(2∣s∣+∣t∣),可以通过
10 分。
算法
2:我会优化!将
t 的子序列存入 set 中,然后每次判断是否
s 的子序列在
t 中存在,时间复杂度
O(∣t∣(2∣s∣+2∣t∣)),可以通过
30 分。
算法
3:我会 DP!设
fi,j 表示
s 截止第
i 位,
t 截止第
j 位的最长公共子序列,则答案为
f∣s∣,∣t∣。状态转移:当
s 的第
i 位,
t 的第
j 位相等时,变成
fi−1,j−1+1,否则是在
fi−1,j(即
s 串的上一位)和
fi,j−1(即
t 串的上一位)中取较大的。
时间复杂度
O(∣s∣∣t∣)。
思考题:
∣s∣≤2×105,∣t∣≤2×105。
【例 5】(乘积最大)一个长度为
N 的数字串,用
K 个乘号分割,求分割成
K+1 个部分的乘积的最大值。
针对
30% 的数据:
1≤N≤18,1≤K≤4。
针对
100% 的数据:
1≤N≤40,1≤K≤6。
算法 1:我会暴力!直接爆搜每个乘号的位置,时间复杂度
O(NK),不可以通过。
算法 2:我会 DP!设
fi,j 表示截止第
i 位用了
j 个乘号的最大乘积,则设上一个乘号用在
l 上,则
1≤l≤j−1(显然),状态转移
fi,j=max(fl,j×sl+1,i),其中
si,j 表示第
i 位到第
j 位所构成的数,注意会爆 long long/__int128,所以需要高精加、高精乘低精,高精乘高精,时间复杂度
O(N4K),瓶颈在于高精乘高精,若用 FFT 优化可以达到
O(KN3logN),可以通过
N≤100,K≤20 的数据。
上面几道例题都是普通 DP,下面我们正式开始讲 DP 优化。DP 优化有几个部分:(1)单调队列优化(2)数据结构优化(3)斜率优化(超过 S 组难度,不讲)。(4)滚动数组优化(5)前缀优化。
我们先讲单调队列优化。
【例 6】(选点问题)你有
n 个点,每个点都有价值
vi,选择任意
m 个点(
m 不固定)
a1,a2,⋯,am,使得对于任意的
2≤i≤m,
ai−ai−1≤k,求
∑i=1mvi 的最大值。
针对
10% 的数据:
1≤k≤n≤20。
针对
50% 的数据:
1≤k≤n≤104。
针对
100% 的数据:
1≤k≤n≤106。
算法 1:我会暴力!直接暴力枚举所有选的点(枚举子集),然后判断是否满足条件,时间复杂度
O(2n)。
算法 2:我会 DP!设
fi 表示选择第
i 个点的情况下,最大的价值和,则转移方程
fi=maxj=min(i−k,0)i−1fj+vi,时间复杂度
O(nk)。
算法 3:我会 DP 优化!注意到
fi 转移的前一项就是一个长
k 的滑动窗口,所以只需要
O(n) 就可以处理,DP 的时候也是
O(n),时间复杂度
O(n)。
数据结构优化也是很常用的方法。主要是依靠
O(nlogn) 的数据结构优化 DP,如线段树,BIT 等,但目前没有找到合适的例题。
接下来是滚动数组优化。
【例 7】同例 4,不过
∣s∣≤3×104,∣t∣≤3×104 且空间 512 M,时间 3s。
注意到
O(∣s∣∣t∣) 的算法还是勉强可以过的,但是空间直接爆掉,注意到每个数只依赖于上、左、斜上三个数,所以每次只要开一个
2×∣t∣ 的数组就可以了,转移方程也可以这样推,所以就可以通过这题。
这种方法就是滚动数组优化。
【例 8】(最大子段和)给出一个
n 和
n 个数的序列,求
L,R 使得
aL+aL+1+⋯+aR 最大,求出这个最大值即可。(各种优化)
针对
10% 的数据:
n≤100。
针对
50% 的数据:
n≤5000。
针对
100% 的数据:
1≤n≤2×106,
∣xi∣≤109。
算法 1:我会暴力!直接暴力枚举 [L,R] 并再用一重循环加上他们的和,时间复杂度
O(n3),拿到
10pts。
算法 2:我会前缀优化优化暴力!直接计算出每个数的前缀和并拿
sR−sL−1 取最大值,时间复杂度
O(n2),拿到
50pts。
算法 3:我会 DP!考虑
fi 表示取第
i 个数的最大值,则答案等于
max(fi),考虑转移:
fi 应该是只取
ai 或取
ai−1 这两种可能,所以状态转移就是
fi=max(fi−1+ai,ai),时间复杂度
O(n)。
还有一些其他的
DP,例如树形 DP,状压 DP。
状压 DP 有一道例题。
【例 9】(公司问题)已知
N 个点
M 条边的图和
P 家公司,第
i 条边用
[L,R,W,k] 表示,表示从第
L 个点到第
R 个点有一条边的权值是
W,且属于第
k 家公司,有
Q 个问询,询问从
u 到
v 经过最多
k 家公司的边的最短路,若走不到则输出 "NO WAY"。
针对
50% 的数据:
P=1。
针对
100% 的数据:
N≤104,M≤104,P≤6,Q≤2×105。
如果
P=1,那么考虑状压每个经过公司的状态,转移方程为
dpi,v,S∣(2k)=min(dpi,v,S∣(2k),dpi,u,S+w),其中
k 为当前边属于哪家公司,
S 为状态,
v 为
u 的邻居,
dpi,j,S 表示从
i 到
j 用
S 集合公司的最短路,最后求的时候直接暴力求出所有二进制位下
1 个数
≤k 的个数。
时间复杂度
O(2Pmlog2Pm+q2P)。
树形 DP 同样有一道例题。
【例 10】(独立集问题)一颗树,根为
rt,共
n 个结点,每个点都有点权,若
(u,v) 有边则
u,v 之间最多选一个,求最大点权和。
针对
20% 的数据:
n≤20。
针对
100% 的数据:
n≤105。
算法
1:我会暴力!直接暴力枚举所有子集,判断是不是独立集,然后计算点权和的最大值。
算法
2:我会 DP!记
dpu,0 表示不选
u 的最大点权和,
dpu,1 表示选
u 的最大点权和,容易推出转移方程
dpu,1=au+∑v∈sonudpv,0,
dpu,0=∑v∈sonudpv,0+dpv,1,答案即是
max(dprt,0,dprt,1),其中
au 是
u 号的点权。
【例 11】(合并石子)共有
n 堆石子,并成一堆,每次相邻合并的代价为两堆石子质量之和,求最小合并代价。
针对
20% 的数据:
n≤10。
针对
100% 的数据:
n≤500。
算法 1:我会暴力!直接暴力枚举合并顺序,时间复杂度
O(n!)。
算法 2:注意到
n≤500,考虑
O(n3) 的 DP。DP 分类中有一种叫区间 DP 的,时间复杂度都是
O(n2) 到
O(n3) 的。注意到
[i,k] 和
[k+1,j] 合并的代价为
sumi,j,
sumi,j 代表
i 到
j 的质量之和,于是转移方程为
fi,j=mink=ijfi,k+fk+1,j+sumi,j。
【例 12】(回文串分割)你现在有一个长度为
n 的字符串,想将其分成回文串的组合。请问你至少要分出几个字符串?
这个题目,设
fi 为截止到
i 的分出字符串最少个数,则
fi=minfj+1,其中
[j,i] 为回文串。回文串可以进行两边哈希,然后就可以
O(1) 求了。
【例 13】(异或和最大分割)你现在有一个长度为
n 的序列,想分割为一系列序列,使得其异或和之和最大,求这个最大值。空间限制 128MB。
针对
10% 的数据:
n≤10。
针对全部数据:
n≤8000。
暴力做法是枚举所有的分割点,时间复杂度可能达到
O(n!)。
设
fi 为
[1,i] 的分割分出的异或和最大值,则
fi=fj+sumj,i,显然可以
O(n2) 预计算
sumj,i,但空间需要 250MB。
观察到异或的一个性质:
[l,r] 的异或和 =
[1,r] 的异或和 异或
[1,l−1] 的异或和,维护前缀异或和即可,时间复杂度
O(n2),空间复杂度
O(n)。
【例 14】(最长回文串)给定一个长度为
n 的字符串,求其最长回文子串。
针对
30% 的数据:
n≤500。
首先有一个
O(n3) 的做法。
这个题很明显是
O(n2) 的 DP 题,记
fl,r 为
[l,r] 是否是回文串,则
fi,i=1,若
ai=ai+1 则
fi,i+1=1 否则
fi,i+1=0。然后再算
fl,r,若
sl=sr 则
fl,r=fl+1,r−1,否则等于
0。
然后算答案枚举
len 和
l,然后求出
r 和对应 DP 值,并判断是否是
1 即可。当然这题还可以双指针和哈希。
【例 15】(最长上升子序列)一个序列
a1,a2,a3,⋯,an,选出其子序列
b∈a,且
b1<b2<...<bl,求
l 的最大值。
针对
40% 的数据:
n≤10000。
针对全部数据:
n≤106。
算法 1:我会暴力 DP,直接
O(n2) DP 上,可以拿到 40pts。
算法 2:观察该题的 DP 方程,显然可以进行权值线段树优化,
j<i 且
aj<ai,我们考虑在
aj<ai 上做操作。我们每次在
ai 上加上
fi,然后每次求就是求区间最大值,只要会单点修改区间求最大值的模板就可以了。
时间复杂度
O(nlogn)。如果值域比较大,可以进行动态开点。