计数 DP 是一种利用类似 DP 的记忆化搜索方法(与在狭义上的 DP,即最优化问题有一定区别),用于解决计数(以及求和)问题。
计数问题一般指求一个集合 S 的大小,在 OI 中,S 的大小有时会达到 Θ(nn) 甚至 Θ(2n!) 的级别(当然,一般会对某一个固定的数取模),其中 n 是问题规模,所以我们不能逐一求出 S 的元素。
如果我们能够将 S 分成若干无交的子集,那么 S 的元素个数就等于这些部分的元素个数的和。如果这些子集的计数恰好与原问题类似,那么我们就可以通过类似动态规划的方法来解决。
"例题" 给定一个正整数 n,求有多少个把 n 划分成 k 个正整数的和的方案,位置调换视为不同的划分方案。
需集合 Sn,k 为形如 (a1,…,ak) 的正整数组组成的集合,其中 a1+⋯+ak=n。如果 ak 固定,则有如下推导:因为 a1+a2+⋯+ak−1+ak=n,所以 a1+a2+⋯+ak−1=n−ak。根据 Sn,k 的定义,(a1,a2,…,ak−1)∈Sn−ak,k−1。
由于 a1,a2,…,ak 是正整数,所以 ak 的取值范围是 [1,n−k+1]∩Z。因此,Sn,k 可以按照 ak 被划分,分成 n−k+1 个子集,其中当 ak=i 时,这个子集为:
{(L,i)∣L∈Sn−i,k−1}.
这个子集的元素个数显然等于 Sn−i,k−1,由于 i 的不同,这些子集两两无交。所以:
∣Sn,k∣=i=1∑n−k+1∣Sn−i,k−1∣.
这样我们就可以使用类似 DP 的方法处理它:设 fn,k 为 ∣Sn,k∣,则有状态转移方程:
fn,k=i=1∑n−k+1fn−i,k−1.
这样就可以使用 DP 的方法求解了。
可以发现,计数 DP 和最优化 DP 都是在一个范围 Ω 内求一个值(大小值、最优值),这个值通过将 Ω 中的所有元素做一次处理,再对处理值做一次整合得到。
例如,对于 0-1 背包问题,Ω 中的元素为背包内的所有物品组成的集合,对于 Ω 中的一个方案 S,我们对 S 做一次处理,处理得到的结果 w(S) 为 S 中物品的总价值,对所有得到的处理值,我们取最大值,得到问题的答案。
对于计数问题,Ω 中的元素为要计算元素个数的集合 S,它的处理是把所有的 S 中元素变为 1,然后将这些 1 通过加法的方式汇总起来,因为每一个 S 中元素都对应一个 1,所以这样得到的值就是 S 中元素个数。
当汇总操作为最大/最小值时,我们可以将 Ω 分成任意若干个部分,只需这些部分的并为 Ω 即可,无需无交的条件。而计数问题由于不满足这个条件,所以我们需要将 Ω 分成若干个部分,这些部分两两无交,这就是与最优化 DP 的区别。
"例题" 给定一个正整数 n,求有多少个把 n 划分成任意多个正整数的和的方案,位置调换视为 相同 的划分方案。
需要计算的集合的元素为满足其和为 n 的正整数多重集。但是这样显然不好推。
若一个多重集 T 只包含 ≤M 的正整数,且 T 中所有元素的和为 n,则称 T∈Sn,M。考虑 M 出现的个数。可能为 k∈[0,⌊Mn⌋]∩Z。于是它可以被转移到 Sn−kM,M−1。求和一下即可。复杂度是 Θ(n2logn)(log 来自于 k 的范围导致的调和级数)。
但是这样还不够优秀。考虑下面所示的一个例子:
f8,3f9,3f10,3f11,3f12,3f13,3=f8,2+f5,2+f2,2=f9,2+f6,2+f3,2+f0,2=f10,2+f7,2+f4,2+f1,2=f11,2+f8,2+f5,2+f2,2=f12,2+f9,2+f6,2+f3,2+f0,2=f13,2+f10,2+f7,2+f4,2+f1,2
等量代换得 f11,3=f11,2+f8,3,f12,3=f12,2+f9,3,f13,3=f13,2+f10,3。同理我们可以得到一个通用的状态转移方程:
fn,M=fn,M−1+{fn−M,M0n≥M,otherwise.
此时,时间复杂度为 Θ(n2)。
考虑到某一个正整数组成的多重集 T 必然可以通过「将 T 中每一个元素自增」、「在 T 中加一个值为 1 的元素」两个操作得到,并且不同的操作序列得到的结果是不同的。
这样对 T 的转移可以变为对操作序列的转移。考虑将 n 划分成 m 个数的操作序列(所有的这些操作序列记作 Bn,m)中的最后一次操作,如果是 1 操作,那么不会增加数,但是 ∑T 增加了 m。为了使最终的 ∑T=n,原来的 T(记作 T′)的和需要为 n−m。所以 Bn,m→Bn−m,m;如果是 2 操作,那么会增加一个数,∑T 增加了 1。所以 Bn,m→Bn−1,m−1。
这样做的时间复杂度依旧是 Θ(n2)。
考虑将 T 分为大于 n 的部分 T1 和小于等于 n 的部分 T2。T2 可以使用解法 1 求出,而 T1 的数量可以通过略微修改解法 2 求出:考虑将两个操作变为「将 T1 中每一个元素自增」、「在 T1 中加一个值为 ⌊n⌋+1 的元素」。容易列出状态转移方程。
将 n 拆为 A 和 B 两部分。枚举其中一个即可得出另一个。将满足 ∑T1=A 的 T1 个数和 ∑T2=B 的 T2 个数求出,乘起来,对所有的 A 求和便是最终结果。
由于在计算 T1 个数的过程中,M≤n,所以我们利用解法 1 计算 T1 的时间复杂度为 Θ(n3/2)。同样地,由于在计算 T2 个数的过程中,∣T2∣≤n∑T2≤nn=n,所以我们利用解法 2 计算 T2 的时间复杂度也是 Θ(n3/2)。所以总时间复杂度为 Θ(n3/2)。