Dynamic Programming
knapsack
luogu1734 最大约数和
(2022.4.3)
给一个正整数 S ≤ 1000 S \leq 1000 S≤1000,要求选出若干个互不相同的正整数,是他们的和不大于 S S S,而且每个数的因数(不包含自身)之和最大。
显然(虽然考试的时候想了很久 qwq)是一个 0 / 1 0/1 0/1 背包。物品就是要选出的正整数,容量是 S S S,然后每个的物品的因数之和。显然因数之和可以暴力 O ( S S ) O(S\sqrt{S}) O(SS) 预处理出来。所以总的复杂度就是 O ( S 2 + S S ) O(S^2 + S\sqrt{S}) O(S2+SS)。
digital dp
luogu P2602 数字计数
(2021.10.27)
给定两个正整数 a 和 b,求在 [a,b] 中的所有整数中,每个数码(digit)各出现了多少次。输出包含一行十个整数,分别表示 0∼9 在 [a,b] 中出现了多少次。
乍一看好像是一道大水题,再一看数据范围,
1
0
12
10^{12}
1012 …,好吧这题不水。
但是这道题还是很良心的啊,都把 digit 数码在题目中给你写出来了,这不明摆着就是一道数位 DP 的题吗。那我们就来看看能不能把递推式给整出来。
总思路:我们把从 a 到 b 的所有数字中的各个数码出现的次数转化成这样:1 ~ b 中的所有数字的各个数码出现的次数减去 1 ~ a - 1 中的所有数字的各个数码出现的次数。所以我们现在考虑怎样求出 1 ~ n 的所有数字的各个数码的出现次数。
首先,我们发现这一道题有一个很烦人的东西,叫做 “前导0”,因为有这个东西,所以我们很难直接写出递推式,只能在考虑了没有它的情况之后再减去考虑它之后的那些多出来的值。给没学过数位 DP 的读者们解释一下,“前导0” 是指我们在枚举数位的时候会碰到枚举出来的数是这样的情况:
0000
a
b
c
.
.
.
‾
\overline{0000abc...}
0000abc...,这些真正的数字前面的 “0” 们我们就叫它们 “前导0”。
首先考虑没有前导 0 的情况,根据我们的 数学常识 我们可以知道,在 0 ~ 9,0 ~ 99,0 ~ 999 … 这些区间里面,所有数字的出现次数是相同的。不知道这个 c常识 的读者可以试着自己打打表验证一下。如果我们把这些数字出现的次数算出来的话,我们会发现下面这样的规律:
0
∼
9
:
1
0
∼
99
:
20
0
∼
999
:
300
0
∼
9999
:
4000
0
∼
99999
:
50000
⋮
\begin{aligned} 0 \sim 9 &: 1\\ 0 \sim 99 &: 20\\ 0 \sim 999 &: 300\\ 0 \sim 9999 &: 4000\\ 0 \sim 99999 &: 50000\\ \vdots \end{aligned}
0∼90∼990∼9990∼99990∼99999⋮:1:20:300:4000:50000
这个规律应该是很明显的了,如果我们把所有的
i
i
i 位数(含前导0)中隔个数字出现的总次数记为
f
i
f_i
fi 那么我们就有
f
i
=
10
×
f
i
−
1
+
1
0
i
−
1
f_i = 10 \times f_{i-1} + 10^{i-1}
fi=10×fi−1+10i−1。
既然我们有了这个规律,很自然的我们根据分治的思想可以想到把数字 a 分成不同的部分,我们考虑这样分(假设 a = A B C D ‾ a = \overline{ABCD} a=ABCD):从最高位开始分别处理不同的位数,也就是分别处理: A 000 ‾ \overline{A000} A000, 0 B 00 ‾ \overline{0B00} 0B00, 00 C 0 ‾ \overline{00C0} 00C0 和 000 D ‾ \overline{000D} 000D。
首先我们来讨论如何处理最高位: A 000 ‾ \overline{A000} A000。因为我们知道 0 ~ 999 之间的每个数字都出现了 f 3 f_3 f3 次,所以我们考虑先把这个 A 000 ‾ \overline{A000} A000 看成 0000 ‾ ∼ 1000 ‾ ∼ 2000 ‾ ∼ ⋯ ∼ A 000 ‾ \overline{0000} \sim \overline{1000} \sim \overline{2000} \sim \cdots \sim \overline{A000} 0000∼1000∼2000∼⋯∼A000。感性的来讲,是不是所有数字都出现了 A × f 3 A \times f_3 A×f3 次呢?不是的,因为除了这些数字,还要加上首位出现的次数,也就是比 A 小的每个数要多出现 1 0 3 10^3 103 次。
这应该比较好理解,如果不理解的话我简单的说一下,我们知道 0 ∼ 999 0 \sim 999 0∼999 中的所有数字的各个数码出现次数相等且都是 f 3 f_3 f3 次。那我们是不是可以把这些书写成这样 0000 ‾ , 0001 ‾ , 0002 ‾ , ⋯ , 0999 ‾ \overline{0000}, \overline{0001},\overline{0002}, \cdots,\overline{0999} 0000,0001,0002,⋯,0999,再看从 1000 ∼ 1999 1000 \sim 1999 1000∼1999 的这些数字,如果忽略最高位 1,那么是不是他们就和 0 ∼ 999 0 \sim 999 0∼999 一模一样,所以从 1000 ∼ 1999 1000 \sim 1999 1000∼1999 中,所有数字都要出现 f 3 f_3 f3 次,且首位 1 会多出现 1 0 3 10^3 103 次(因为每一个数都多一个 1)。同理: 2000 ∼ 2999 2000 \sim 2999 2000∼2999 中的每个数字都出现 f 3 f_3 f3 次且数字 2 多出现了 1 0 3 10^3 103 次。以此类推。
但是这还没完,首位 A 出现的次数没有被统计下来,因为在后面还会有 A 001 ‾ , A 002 ‾ , A 003 ‾ , ⋯ , A B C D ‾ \overline{A001},\overline{A002},\overline{A003},\cdots,\overline{ABCD} A001,A002,A003,⋯,ABCD,也就是说最高位 A 还要再多出现 B C D ‾ + 1 \overline{BCD} + 1 BCD+1 次。
把这些因素都考虑进去之后,我们就把首位给处理完了。接着就是用同样的方法来处理抛开最高位之后剩下的最高位了。
当然,我们前面说的都是在一个大前提之下:不考虑前导0。所以现在我们需要把数字 0 出现的次数减去那些前导0 的个数。还是可以利用 “打表大法” 和 “瞪眼观察大法”,我们可以推出前导0的个数等于: 1 0 i − 1 + 1 0 i − 2 + ⋯ + 10 10^{i-1} + 10^{i-2} + \cdots + 10 10i−1+10i−2+⋯+10。然后整道题就愉快地 A 掉了。
综上所述,我们梳理一下求解的流程。
- 预处理(或打表)出来一个 f 数组,其中 f i f_i fi 表示在所有的 i 位数(包含前导0)中各个数码出现的次数。数值上 f i = i × 1 0 i − 1 f_i = i \times 10^{i-1} fi=i×10i−1。
- 循环枚举各个数位,对每个数位进行以下操作(设这个数位是第 i 位上的数码为 A):每个数码出现的次数加上 A × f i − 1 A \times f_{i-1} A×fi−1,然后比 A 小的数码出现次数再加上 1 0 3 10^3 103,然后 A 出现的次数再加上 B C D ‾ + 1 \overline{BCD} + 1 BCD+1 次。
- 数字 0 出现的次数减去 ∑ n = 1 i − 1 1 0 n \sum\limits_{n=1}^{i-1}10^n n=1∑i−110n 次。
range dp
luogu5569 SDOI2008 石子合并
(2021.11.12)
现在有 N N N 堆石子排成一排,其中第 i i i 堆石子的重量为 A i A_i Ai,每次可以选相邻的两堆石子合并起来,然后形成新的石子堆的重量是原来两堆石子的重量和,并且会得到与合并之后的重量相等的分数,求把所有的石子都合并到同一堆的时候的得分的最小值。
显然,如果最初的第
l
l
l 堆石子要和第
r
r
r 堆石子合并,那么
l
∼
r
l \sim r
l∼r 的石子肯定都被合并过了,因为这样
l
l
l 和
r
r
r 才可能相邻。所以在所有的时刻的每一堆石子我们可以用一个闭区间
[
l
,
r
]
[l, r]
[l,r] 来表示(表示这堆石子是由最初的时刻的第
l
∼
r
l \sim r
l∼r 堆石子合并而来的)。这堆石子的重量就显然是
∑
i
=
l
r
A
i
\sum\limits_{i=l}^r A_i
i=l∑rAi。另外,肯定存在一个
k
k
k 使得,在
[
l
,
r
]
[l, r]
[l,r] 被合并成同一堆之前,有两堆石子
[
l
,
k
]
[l, k]
[l,k] 和
[
k
+
1
,
r
]
[k+1, r]
[k+1,r]。然后这两堆石子合并成了
[
l
,
r
]
[l, r]
[l,r]。这就意味着两个长度较小的区间上的信息会向一个长度更大的区间上转移,所以我们很自然的用一个区间的长度作为动态转移的阶段,并且用区间的左右端点作为 DP 的状态(
l
e
n
=
r
−
l
+
1
len = r - l + 1
len=r−l+1)。我们记
F
[
l
,
r
]
F[l, r]
F[l,r] 表示把最初的
l
∼
r
l \sim r
l∼r 合并成一堆所得到的分数,于是我们有:
F
[
l
,
r
]
=
min
l
≤
k
<
r
{
F
[
l
,
k
]
+
F
[
k
+
1
,
r
]
}
+
∑
i
=
l
r
A
i
F[l, r] = \min_{l \leq k < r}\lbrace F[l, k] + F[k+1, r] \rbrace + \sum_{i=l}^r A_i
F[l,r]=l≤k<rmin{F[l,k]+F[k+1,r]}+i=l∑rAi
其中: ∀ l ∈ [ 1 , N ] , F [ l , l ] = 0 \forall l \in [1, N],F[l, l] = 0 ∀l∈[1,N],F[l,l]=0,我们要求的答案就是 F [ 1 , N ] F[1, N] F[1,N]。然后对于每次查询 ∑ i = l r A i \sum\limits_{i=l}^r A_i i=l∑rAi,我们对 A A A 数组做一个前缀和就好了。
但是这个做法是 n 2 n^2 n2 的,对于这个题 40000 的数据范围来说,肯定过不了,这个算法就只能得到 60分。但是因为我今天主要是来学 DP 的,我也不太怎么看得懂这个 GarsiaWachs 算法。所以就这样吧。
luogu2893 USACO08FEB Making the Grade G
(2021.11.12)
给定一个长度为 N N N 的序列 A A A 让你构造一个长度也为 N N N 的序列 B B B,且 B B B 非严格单调(不限制单增或单减),且要最小化 T = ∑ i = 1 N ∣ A i − B i ∣ T = \sum\limits_{i=1}^N \mid A_i - B_i \mid T=i=1∑N∣Ai−Bi∣。
因为这里分成两种情况,一种 B B B 是非严格单增的,另一种 B B B 是非严格单减的,所以我们考虑分别对这两种情况进行求解,最后再去一个最小值就好了。下面我们就以 B B B 是非严格单增来举例子(非严格单减同理)。
在分析的过程中,我们发现一个神奇的现象,在满足最小化 T T T 的前提下,一定存在一种构造 B B B 的方案,使得 B B B 中的所有数值都在 A A A 中出现过。首先,当 N = 1 N = 1 N=1 的时候这个命题显然成立(最小化 T T T 之间让 B 1 = A 1 B_1 = A_1 B1=A1 就好了)。之后我们考虑先假设这个命题对 N = k − 1 N = k-1 N=k−1 成立,如果能推出这个命对 N = k N = k N=k 成立,在运用数学归纳法,就能证明这个命题对所有 N N N 都成立了。
现在设命题对 N = k − 1 N = k-1 N=k−1 时成立,且此时构造出来的序列是 B 1 ∼ B k − 1 B_1 \sim B_{k-1} B1∼Bk−1。然后我们来分类讨论 B k − 1 B_{k-1} Bk−1 和 A k A_k Ak 的大小情况:
- 如果 B k − 1 ≤ A k B_{k-1} \leq A_k Bk−1≤Ak,则令 B k = A k B_k = A_k Bk=Ak 则我们就把 k k k 这里消成了 0,而且还满足单增。并且前面已经构造出最小的了,所以现在肯定也是最小的,此时命题成立。
- 当
B
k
−
1
>
A
k
B_{k-1} > A_k
Bk−1>Ak 的时候还要分成两种情况,第一种情况是直接令
B
k
=
B
k
−
1
B_k = B_{k-1}
Bk=Bk−1 命题成立。
第二种当这样直接构造不成立的时候就要考虑其他的构造方式了。因为直接令 B k = B k − 1 B_{k} = B_{k-1} Bk=Bk−1 不行了,所以我们考虑下调 B B B 数列前面的数,因为要向下调整,所以我们前面构造好的数也要向下一起调整。
所以我们发现,我们一定能够找到一个 j j j 然后重新令 B j , B j + 1 , ⋯ B k B_{j}, B_{j+1}, \cdots B_{k} Bj,Bj+1,⋯Bk 都等于一个新的值 x x x,使得原命题成立。但是为什么要统一下调到 x x x 呢,难道不存在一种可能使得前面的数下调到比 x x x 还小使得整个数列的 T T T 更优吗?显然是不存在的,因为如果可以让前面的更小来满足更优那么这个数就不会在 [ j , k ] [j, k] [j,k] 这一段里,它会在之前构造 k − 1 k-1 k−1 的长度的时候就被调成更小的而满足最优。
这个时候问题就变成了找到一个数 x x x 证明当 T ′ = ∑ i = j k ∣ x − A i ∣ T' = \sum\limits_{i=j}^k \mid x - A_i \mid T′=i=j∑k∣x−Ai∣ 时 x x x 一定在 A A A 里面出现过。而对于这个式子: T ′ = ∑ i = j k ∣ x − A i ∣ T' = \sum\limits_{i=j}^k \mid x - A_i \mid T′=i=j∑k∣x−Ai∣,其实就是一个中位数问题。设 m i d mid mid 是 A [ j ∼ k ] A[j\sim k] A[j∼k] 的中位数,如果 m i d ≥ B j − 1 mid \geq B_{j-1} mid≥Bj−1,让 x = m i d x = mid x=mid 就能最小化 T ′ T' T′。当 m i d < B j − 1 mid < B_{j-1} mid<Bj−1 的时候我们就让 x = B j − 1 x = B_{j-1} x=Bj−1 就能保证最优且单调。而且 m i d mid mid 和 B j − 1 B_{j-1} Bj−1 都是 A A A 中的数值。所以原命题成立。
我们上面已经证明了这个神奇的命题,那下面转移方程就比较容易了。我们设
F
[
i
]
F[i]
F[i] 表示对于
A
[
1
∼
i
]
A[1\sim i]
A[1∼i] 完成构造,且
A
[
i
]
=
B
[
i
]
A[i] = B[i]
A[i]=B[i] 的时候,
T
T
T 的最小值。我们仿照 LIS 就能得到下面的动态转移方程:
F
[
i
]
=
min
0
≤
j
<
i
,
A
[
j
]
≤
A
[
i
]
{
F
[
j
]
+
c
(
j
+
1
,
i
−
1
)
}
F[i] = \min_{0 \leq j < i, A[j] \leq A[i]}\lbrace F[j] + c(j+1, i-1) \rbrace
F[i]=0≤j<i,A[j]≤A[i]min{F[j]+c(j+1,i−1)}
其中,我们设 c ( j + 1 , i − 1 ) c(j+1, i-1) c(j+1,i−1) 表示构造 B j + 1 , B j + 1 , ⋯ B i − 1 B_{j+1}, B_{j+1}, \cdots B_{i-1} Bj+1,Bj+1,⋯Bi−1 的最小代价。也就是构造出 B j + 1 , B j + 1 , ⋯ B i − 1 B_{j+1}, B_{j+1}, \cdots B_{i-1} Bj+1,Bj+1,⋯Bi−1,且满足 A j ≤ B j + 1 ≤ B j + 1 ≤ ⋯ ≤ B i − 1 ≤ A i A_j \leq B_{j+1} \leq B_{j+1} \leq \cdots \leq B_{i-1} \leq A_i Aj≤Bj+1≤Bj+1≤⋯≤Bi−1≤Ai 的时候 ∑ k = j + 1 i − 1 ∣ A k − B k ∣ \sum\limits_{k=j+1}^{i-1}\mid A_k - B_k \mid k=j+1∑i−1∣Ak−Bk∣ 的最小值。
现在我们就要想办法求解出 c ( j + 1 , i − 1 ) c(j+1, i-1) c(j+1,i−1) 等于多少了。根据我们上面的神奇的命题, B B B 数列一定是由一段一段的 A A A 数列里面的值组成的,又因为 i i i 处刚好 A [ i ] = B [ i ] A[i] = B[i] A[i]=B[i] 且 j j j 处也满足 A [ j ] = B [ j ] A[j] = B[j] A[j]=B[j],所以我们要构造的中间的一坨东西就是前面一部分是 A [ j ] A[j] A[j],后面一部分是 A [ i ] A[i] A[i] 的一个序列,然后我们就用一个 k k k 来枚举分界点就好了。这样的算法的时间复杂度就是 O ( n 3 ) O(n^3) O(n3),显然过不了。
然后我们考虑优化一下,一个状态的转移不行,我们就多加一个状态,我们直接把上一次构造的
B
B
B 序列的最后一个值也加在状态里面,也就是记
F
[
i
,
j
]
F[i, j]
F[i,j] 表示完成对
A
[
1
∼
i
]
A[1\sim i]
A[1∼i] 的构造,且构造出来的
B
B
B 中
B
[
i
]
=
j
B[i] = j
B[i]=j 时
T
T
T 的最小值。那么我们就有:
F
[
i
,
j
]
=
min
0
≤
k
<
j
{
F
[
i
−
1
,
k
]
+
∣
A
[
i
]
−
j
∣
}
F[i, j] = \min_{0 \leq k < j} \lbrace F[i-1, k] + \mid A[i] - j \mid \rbrace
F[i,j]=0≤k<jmin{F[i−1,k]+∣A[i]−j∣}
然后我们可以对 A A A 进行离散化,把状态的第二维降到 O ( n ) O(n) O(n)。然而状态转移还是 O ( n ) O(n) O(n) 的,所以整个的复杂度也还是 O ( n 3 ) O(n^3) O(n3) 的。但是我们可以继续优化,我们记 S ( j ) S(j) S(j) 表示满足转移条件 0 ≤ k < j 0 \leq k < j 0≤k<j 的所有 k k k 的集合。我们发现,在每一次枚举 j j j 的时候这个决策集合只增不减,所以在找 k k k 的时候我们就不用每次都枚举 k k k,直接用 j j j 来代表 k k k 就可以了,这样我们就可以做到 O ( 1 ) O(1) O(1) 的转移。
luogu7914 括号匹配
(2021.11.15)
首先是区间 DP。那我们设 F l , r , k F_{l, r,k} Fl,r,k 表示从 l l l 到 r r r 的合法序列数量。但是这道题有个很烦的事情就是,有很多种状态它都是合法的,所以我们要对每一种转移做讨论。所以我们对于几种不同的转移把 F F F 扩展成 3 维,它们分别代表一下的含义(S 表示全是 * 的字符串,A 表示任意合法字符串):
- F l , r , 0 F_{l, r, 0} Fl,r,0:表示这样:S,全是 ’ * ’ 的序列
- F l , r , 1 F_{l, r, 1} Fl,r,1:表示这样:(S) 的序列,两头都是括号且这两头的括号匹配的序列
- F l , r , 2 F_{l, r, 2} Fl,r,2:表示这样:(A)S(A)…(A)S 的序列,左边以括号开头,有边以 * 结尾的序列
- F l , r , 3 F_{l, r, 3} Fl,r,3:表示这样:"(A)S(A)S…(A)" 的序列,左边以括号开头,有边以括号结尾的序列
- F l , r , 4 F_{l, r, 4} Fl,r,4:表示这样:“S(A)S…(A)” 的序列,左边以 * 开头,有边以括号结尾的序列
- F l , r , 5 F_{l, r, 5} Fl,r,5:表示这样:“S(A)S…S” 的序列,左边以 * 开头,有边以 * 结尾的序列
然后就是转移,我们定义一个 bool 类型的函数 canMatch(i, j),返回第 i i i 位和第 j j j 为能否匹配成一对括号。然后下面是转移方程:
F l , r , 0 直 接 特 判 就 好 了 . . . F l , r , 1 = ( F l + 1 , r − 1 , 0 + F l + 1 , r − 1 , 2 + F l + 1 , r − 1 , 3 + F l + 1 , r − 1 , 4 ) [ c a n M a t c h ( l , r ) ] F l , r , 2 = ∑ i = l r − 1 F l , i , 3 F l + 1 , r , 0 F l , r , 3 = F l , r , 1 + ∑ i = l r − 1 ( F l , i , 2 + F l , i , 3 ) F i + 1 , r , 1 F l , r , 4 = ∑ i = l r − 1 ( F l , i , 4 + F l , i , 5 ) F i + 1 , r , 1 F l , r , 5 = F l , r , 0 + ∑ i = l r − 1 F l , i , 4 F i + 1 , r , 0 \begin{aligned} & F_{l, r, 0} \quad 直接特判就好了... \\ & F_{l, r, 1} = (F_{l+1, r-1, 0} + F_{l+1, r-1, 2} + F_{l+1, r-1, 3} + F_{l+1, r-1, 4})[canMatch(l, r)] \\ & F_{l, r, 2} = \sum_{i=l}^{r-1} F_{l, i, 3} F_{l+1, r, 0} \\ & F_{l, r, 3} = F_{l, r, 1} + \sum_{i=l}^{r-1} (F_{l, i, 2} + F_{l, i, 3})F_{i+1, r, 1} \\ & F_{l, r, 4} = \sum_{i=l}^{r-1}(F_{l, i, 4} + F_{l, i, 5})F_{i+1, r, 1} \\ & F_{l, r, 5} = F_{l, r, 0} + \sum_{i=l}^{r-1}F_{l, i, 4}F_{i+1, r, 0} \end{aligned} Fl,r,0直接特判就好了...Fl,r,1=(Fl+1,r−1,0+Fl+1,r−1,2+Fl+1,r−1,3+Fl+1,r−1,4)[canMatch(l,r)]Fl,r,2=i=l∑r−1Fl,i,3Fl+1,r,0Fl,r,3=Fl,r,1+i=l∑r−1(Fl,i,2+Fl,i,3)Fi+1,r,1Fl,r,4=i=l∑r−1(Fl,i,4+Fl,i,5)Fi+1,r,1Fl,r,5=Fl,r,0+i=l∑r−1Fl,i,4Fi+1,r,0
最后,答案就是 F 1 , n , 3 F_{1, n, 3} F1,n,3。
状压 dp
前置芝士
- 取出整数 n n n 二进制下的第 k k k 位 : ( n > > k ) & 1 (n >> k) \& 1 (n>>k)&1
- 取出整数 n n n 而今之下的 0 ∼ k 0 \sim k 0∼k 位: n & ( ( 1 < < k ) − 1 ) n \& ((1 << k) - 1) n&((1<<k)−1)
- 把整数 n n n 在二进制下第 k k k 位取反: n x o r ( 1 < < k ) n\; xor \; (1 << k) nxor(1<<k)
- 把整数 n n n 在二进制下第 k k k 位赋为 1 1 1: n ∣ ( 1 < < k ) n | (1 << k) n∣(1<<k)
- 把整数 n n n 在二进制下第 k k k 位赋为 0 0 0: n & ( ∼ ( 1 < < k ) ) n \& (\sim(1 <<k)) n&(∼(1<<k))
luogu1433 吃奶酪
(2022.4.16)
求最短哈密顿路径。
不能直接暴力,会超时。而且目前这玩意儿没有多项式解法,所以我们考虑状压 d p dp dp。我们用状态压缩处理点集中是否走过的信息,然后我们设 f i , j f_{i, j} fi,j 表示点集为 i i i,目前在 j j j 时的最短路径。
在起点的时候显然有 f 1 , 0 = 0 f_{1, 0} = 0 f1,0=0,最终目标时 f ( 1 < < n ) − 1 , n − 1 f_{(1 << n) - 1, n - 1} f(1<<n)−1,n−1。在任意时刻我们有:
f i , j = min k = 0 n − 1 { f i x o r ( 1 < < j ) , k + w k , j } f_{i, j} = \min_{k = 0}^{n - 1}\{ f_{i \; xor \; (1 << j), k} + w_{k, j} \} fi,j=k=0minn−1{fixor(1<<j),k+wk,j}
然后就做完了