组合数学相关

为保证笔记简洁,代码缺省源已经删去。如需编译代码请先加上附在文末的缺省源。

0. 组合数

0.1. 重要公式

在做题时遇到的巧妙组合公式,会不断补充。

(0.1)(ab)(bc)=(ac)(acbc)

组合意义证明:从 a 个数里面选 b 个,再从 b 个数里面选 c 个,等价于从 a 个数里面选 c 个,再在剩下 ac 个数中选 bc 个并与第一次选的 c 个数共同组成 “从 a 个数里面选出的 b 个数”。代数证明:

(ab)(bc)= a!(ab)!×(bc)!×c!= a!(ac)!×c!×(ac)!(ab)!×(bc)!= (ac)(acbc)


(0.2)aibi=(ai)(bi)=(biba)(ba)

组合数拆开可证,第二个等于号可以在推式子时将 i 转化到分子上面。


(0.3)(nr)+(n1r)++(rr)=i=rn(ir)=(n+1r+1)

可以看成杨辉三角的斜求和。《组合数学》中的证明:设 |A|=n+1,从 A 中取 r+1 个元素组合成 C。考虑以下 nr+1 种情况:枚举 i[1,nr+1]a1,a2,,ai1CaiC,再从剩下 ni 个元素中取 r 个,有 (nir) 种方案。得证。

0.2. 二项式定理

(x+y)n=i=0n(ni)xiyni

通常运用它的逆变换。

0.3. 例题

I. CF660E Different Subsets For All Tuples

考虑求出每个子序列在所有序列中的出现次数,注意我们应当只统计第一次出现的次数。枚举子序列长度 i 以及末尾位置 j,由于前 j 个位置中,除了子序列出现的 i 个位置,剩下来都只能填 m1 个数(不能与该位置右边最近的一个子序列出现的位置上填的数相同,否则就不是第一次出现了),其余 nj+1 个位置都可以填 m 个数,故答案如下:

i=1nj=in(j1i1)mnj+i(m1)ji

注意到这个形式很像二项式定理,故交换求和符号并稍作变形:

j=1ni=1j(j1i1)mnj+i(m1)ji= j=0n1i=0j(ji)mnj+i(m1)ji= j=0n1mnji=0j(ji)mi(m1)ji= i=0n1mni(2m1)i

不要忘记计算空序列的贡献 mn,时间复杂度 O(n!)

II. 2020 知临联考模拟赛 笛卡尔树

题意简述:定义一棵二叉树的权值为对于每个存在左右儿子的点 irsilsi,其中 lsi,rsi 都是编号。求所有排列形成的笛卡尔树的权值之和。1n106

好题。首先要想到确定根之后就变成了两个子问题,因此设 fi 表示 n=i 时的答案,发现不太好求:我们不知道两个儿子对其的贡献是什么。那么再设 gi 表示长度为 i 的排列所建出的笛卡尔树的所有根的位置之和,这是 trivial 的,我们有:

gi=j=1ij(i1)!=i(i+1)2(i1)!=(i+1)!2

那么枚举根的位置 j 并设 a=j1b=nj 分别表示左右子树大小,我们有

fi=(a+ba)(j=1ifab!+fba!+j=2i1gba!gab!+ja!b!)

意义分别为合并大小为 a,b 排列的方案数 (a+ba),两个子树的贡献 fab!+fba!,右子树所有根出现的位置之和减去左子树的贡献 gba!+ja!b!gab!ja!b! 是因为将右子树所有下表向右平移了 j 且共有 a!b! 种方案),j2 开始枚举到 i 是因为子树大小不能为 0。这样我们就能做到 n2 但是不够,考虑把柿子完整写出来:

fi=(a+b)!a!b!(j=1ifab!+fba!+j=2i1(b+1)!2a!(a+1)!2b!+ja!b!)

括号拆开,有:

fi=(a+b)!(j=1ifaa!+fbb!+j=2i1(b+1)(a+1)+j)

形式非常明显。注意到 a+b=i1,因此将 (i1)! 除到左边并设 hi=fii!,有:

i×hi=(i+1)(i2)2+2j=1i1hj

前缀和优化即可,时间复杂度线性或线性对数。

1. Lucas & exLucas

1.1. 卢卡斯定理

(1.1)(nm)modp=(n/pm/p)(nmodpmmodp)modp

证明:考虑组合数的定义,因为 i[1,p1],(pi)0(modp),且 (p0)=(pp)=1,所以 (a+b)pap+bp(modp)。因此

(1+x)n= (1+x)pn/p(1+x)nmodp (1+xp)n/p(1+x)nmodp(modp)

因为我们只关心 xm 前的系数,而 (1+xp)n/p 得到的所有指数都为 p 的倍数,(1+x)nmodpmodp 得到的所有指数都小于 p,所以此时 m 只能被写成 pm/p+mmodp,得证。预处理阶乘及其逆元 O(1) 求组合数,时间复杂度为 O(p+logpn)

1.2. 应用:组合数的奇偶性

p=2 时,当且仅当 nmodp=0mmodp=1 时,(nm)modp=0,因为 (00)=(10)=(11)=1,而 (01)=0。这也意味着 (nm) 为奇数当且仅当 n & m=m

1.3. 直观理解

1.4. 例题

I. P3807 【模板】卢卡斯定理

模板题,这里给出代码。

2. Prufer 序列

感觉很多时候生成树计数可以用 Prufer 序列来计算,故学习该算法。

2.1. 树生成 Prufer 序列

Prufer 序列的定义是这样的:给定一棵树 T,每次找到编号最小的叶子结点 a 并将与其相邻的 b 加入 Prufer 序列中,删除 a。重复操作直至树中只剩下 2 个结点。显然,Prufer 序列的长度为 n2

具体地,用指针维护 a。删除 a 之后若 b<ab 变为叶子结点,那么删除 b,将与其相邻的 b 加入序列。操作递归进行,即若 b<bb 变为叶子结点,那么删除 b,将与其相邻的 b 加入序列;若 b<bb 变为叶子结点,那么删除 b,将与其相邻的 b 加入序列,以此类推。最后 a 自增找到下一个编号最小的叶子结点。时间复杂度线性。

2.2. Prufer 序列生成树

设点集 a 包含 1n 所有的点,每次取出 Prufer 序列 b 第一个数 u,在点集 a 中找到最小的没有在 b 中出现过的数 v,连边 (u,v) 并将 u,v 分别在序列 b,a 中删除。重复操作直至 b 中没有元素。显然,a 中还剩下两个元素 a1,a2,则连边 (a1,a2)

具体地,用指针维护 v。连边 (u,v) 之后若 u<vub 中是最后一次出现,那么将 ub 中下一个数 u 相连。操作递归进行,即若 u<uub 中是最后一次出现,那么将 ub 中下一个数 u 连边;若 u<uub 中是最后一次出现,那么将 ub 中下一个数 u 相连,以此类推。最后 v 自增找到下一个最小的没有在 b 中出现过的数。时间复杂度线性。

2.3. Prufer 序列得到的一些推论

  1. n 个结点的有标号无根树个数为 nn2。这也是 n 个结点的无向完全图的生成树个数。
  2. n 个结点的有标号有根数个数为 nn1。对于每个无根树,钦定任意一个结点为根都可以形成唯一的有根树,因此将 nn2 乘上 n 即可。
  3. 度数为 d 的结点在 Prufer 序列中出现了 d1 次,这个由 Prufer 序列的构造方式可知。
  4. n 个有度数要求 di 的结点的有标号无根树个数为 (n2)!i=1n(di1)!,这个由推论 3 和多重集的排列数可知。

2.4. 例题

*I. CF156D Clues

如果将同一个连通块缩成点,设最终剩下 k 个点,那么它的 Prufer 序列长度为 k2,且每个位置都可以填 1n 之间的任意数,因此方案数为 nk2

但是,从结点集合 a(原来包含 1k,对应连通块的编号)中取出最小的不包含 b 中出现结点的连通块编号 v 时,我们并不知道选择的是连通块内的哪一个结点,因此要乘上对应的连通块大小 sv。因此答案为 nk2×i=1ksi。注意 k=2 时特判时输出 1 需要 modp。时间复杂度线性对数,直接 DFS 可以做到线性。

3. 容斥原理

3.1. 公式

|iSAi|=TS,T(1)|T|1|jTAj|

3.2. 例题

*I. CF1613F Tree Coloring

容斥 + 多项式好题。一般这种题目都要从容斥入手:形如给出若干组限制,求满足所有限制方案数,大部分考虑容斥:算出钦定违反 k 条限制的方案数 fk(这些方案之间可能会有重叠),乘以容斥系数 (1)k

本题中,由于一个父亲只能违反一条限制,但方案数有 |soni|,相当于乘以 |soni|x+1,那么 fk=[xk](nk)!。后面的阶乘是因为现在只有 nk 个自由变量,且它们顺序任意。那么直接分治 + NTT(不是分治 NTT)即可。时间复杂度 O(nlog2n)代码

*II. P2567 [SCOI2010]幸运数字

考虑找到所有 r 以内的幸运数字 ai,然后容斥枚举子集求 lcm。显然,复杂度是 22log10r21024,过不去。

尝试剪枝:首先若 aiaj 那么 aj 无用(在容斥原理中的体现就是若 ST 那么可以当做 S 不存在),其次 lcm 大于 r 那么直接返回。核心优化是将 a 从大到小排序,使得 lcm 增长更快。加上这三个剪枝即可通过。注意求 lcm 的过程可能会爆 long long

*III. [BZOJ4361]isn

究极神仙题!一个简单想法是对每个长度 l 直接求出长为 l 的不降序列个数 fl,那么删除的方案数就是 (nl)!。显然这样行不通,因为可能删到一半就已经不降了。但注意到一个长度为 l+1 的不降序列包含 l+1 个长度为 l 的不降序列,也即从剩下来的不降的 l+1 个元素中任选一个删去后仍然不降,因此用 fl(nl)! 减掉 fl+1(l+1)(nl1)! 就是所有长度为 l 的答案了。

gi,j,k 表示考虑前 i 个元素,长度为 j 且以值 k 结尾的不降序列个数,那么

gi,j,k={pkgi1,j1,pk=aigi1,j,kkai

注意到可以使用 BIT 优化,时间复杂度平方对数。

*IV. [BZOJ3269]序列染色

好题。一开始的想法是用二项式反演搞掉,发现不太行。想了很久想到一个 DP 做法:设 fi 表示 1i 第一次出现长度为 k 的连续 B 是在 ik+1i 的方案数,gi 表示 in 第一次出现长度为 k 的连续 W 是在 i+k1n 的方案数,那么答案为 ki<jnk+1figj×2ci+1,j1,其中 cl,r 表示 slsrX 的数量,若 l>rcl,r=0

先考虑第一部分 f,g,乍一看不太好直接算,因为之前不能出现满足的连续段。是时候让容斥出场了:由于我们已经得到了 fkfi1,所以直接用总方案数减掉 fkfi1 对应的方案数即可。具体的柿子可以写作:

fi=2c1,ikj=ki1fj×2cj+1,ik

分两块来看,一部分是 jik,可以用一个变量直接维护,每次只涉及到若 sik=X 则乘 2,以及加 fikj>ik 就是求一段连续的 f 的和,前缀和解决。 第二部分可以类似地做,用一个变量直接维护 fi×2ci+1,j1

还有更简洁的 DP 方法:设 fi,j,k 表示 1isi 填了 jk=0 表示一段 B 都没有,k=1 表示有一段 B 但没有 Wk=2 表示满足条件的方案数。若当前位可以填 B 那么有:

fi,0,0=fi1,0,0+fi1,1,0fik,0,1[P]fi,0,1=fi1,0,1+fi1,1,1+fik,0,1[P]fi,0,2=fi1,0,2+fi1,1,2

其中 P 表示 ik+1i 没有 W,表示 fik,0,1ik+1i 全填 W 的方案,所以在 fi,0,0 乱转移的过程中要减掉这部分方案数(容斥思想)。类似地,若当前位可以填 W 那么有:

fi,1,0=fi1,0,0+fi1,1,0fi,1,1=fi1,0,1+fi1,1,1fik,1,0[Q]fi,1,2=fi1,0,2+fi1,1,2+fik,1,0[Q]

初始化也很有意思:f0,1,0=1,因为这保证了 fk,0,1 可以从 f0,1,0 转移过来(不能从相同颜色转移长度为 k 的连续段)。

*V. [BZOJ2498]Xavier is Learning to Count

究极神题。本题最有价值的地方不是生成函数板子,而在于求容斥的方法。

如果允许重复,那么令 F(x)=i[icards]xi,和为 i 的方案数即 [xi]Fk(x)。不许重复就很棘手了,设 Gk(x) 表示 i[ikcards]xi,即重复选 k 张卡牌得到的生成函数。

我们钦定存在 i 个等价类 c1,c2,,ci 表示最终结果分别由 c1,c2,,ci 个相同的数组成。即若 c={1,2,3} 则一种可能的选数方案为 {{1},{2,2},{3,3,3}},也可能是 {{1},{1,1},{2,2,2}},因为我们没有限制不同等价类所表示的数不同,不过求一个数 n 在这种等价类的划分下能够被组成的方案数,只需将所有 Gci(x) 相乘后取 xn 前面的系数。

但是这样钦定后容斥系数应该怎样确定呢?一个常用技巧是直接暴力求:O(B(k)) 枚举 k 个有标号元素划分进若干集合的方案,对于两个集合划分方案 S,T,若 S 中在同一集合的任意两个元素,T 中也在同一集合,那么说明 STT 的容斥系数需要减掉 S 的容斥系数。

我们有基态 f({{1},{1},,{1}})=1,再根据所有划分方案包含关系形成的 DAG 进行 O((B(k))2) 的 DP 即可,即 fT=STSTfS

进行一番打表后,我们发现一个大小为 n 的等价类的容斥系数为 (1)n1(n1)!,而一种划分方案的容斥系数即所有等价类的容斥系数之积。想了好长时间如何证明,发现超出了能力范围,于是咕了

打表代码如下:

int n, num, f[N], deg[N], id[N];
vint e[N];
map <vint, int> mp;
void dfs(int x, int s) {
	if(x == n) {
		vint cur; for(int i = 1; i <= n; i++) cur.pb(id[i]);
		return mp[cur] = ++num, void();
	} for(int i = 1; i <= s + 1; i++) id[x + 1] = i, dfs(x + 1, max(s, i));
}
int main() {
	cin >> n, dfs(0, 0);
	for(auto x : mp) for(auto y : mp) if(x.se != y.se) {
		bool ok = 1;
		for(int i = 0; i < n; i++) for(int j = 0; j < n; j++)
			if(x.fi[i] == x.fi[j]) ok &= y.fi[i] == y.fi[j];
		if(ok) deg[y.se]++, e[x.se].pb(y.se);
	} queue <int> q;
	for(int i = 1; i <= num; i++) if(!deg[i]) q.push(i), f[i] = 1;
	while(!q.empty()) {
		int t = q.front(); q.pop();
		for(int it : e[t]) {f[it] -= f[t]; if(!--deg[it]) q.push(it);}
	} for(int i = 1; i <= num; i++) cout << f[i] << "\n";
}

有了上述结论,我们可以直接 O(B(k)) 枚举所有划分方案 T,设其等价类个数为 p,每个等价类大小分别为 c1,c2,,cp,那么答案加上 i=1p(1)ci1(ci1)!Gci(x)。可以先对所有 G(x) 做一遍 DFT,在点值表示下爆搜求答案,最后再 IDFT 回来。单组数据时间复杂度 O(Vk(B(k)+logV)),其中 V 是值域。注意到复杂度瓶颈在于对每组划分方案 Vk 求点值,但很多划分方案本质上是一样的,因为我们不关心每个数被分进了哪个集合,只关心集合大小,即元素无标号(但贝尔数的枚举中,{a},{b,c}{b},{a,c} 是不同的,即元素有标号)。实际上只有 P(k) 种本质不同的无标号划分方案 c1,c2,,cp (c1c2cp),且它们对应 (kc1,c2,,cp) 种有标号集合划分方案,算上这一系数即可做到 O(Vk(P(k)+logV))

此外,由于我们做的生成函数卷积是有标号的,而题目要求无标号,所以最后每个方案数还要除以 k!

*4. Min-Max 容斥

一个比较有趣且重要的容斥方法。

4.1. 普通 Min-Max 容斥

什么是 Min-Max 容斥?就是集合最大值与集合最小值基于容斥原理的互相转换。首先给出柿子:对于一个集合 S,有

(3.1)maxiSxi=TS(T)(1)|T|1minjTxj

(3.2)miniSxi=TS(T)(1)|T|1maxjTxj

看上去很神奇,为什么会这样?对于每个 xi,考虑它作为 minjTxj 的贡献:显然,小于 xj 的数不能选。不妨设大于 xi 的数有 c 个,枚举大于 xi 的数有多少个(因为柿子与 |T| 有关),则有式子

i=0c(1)i(ci)

根据组合数知识,当 c=0 时,上式为 1。当 c>0 时上式为 0i=0c1ci×(1)i(ci)=(11)c=0c。得证。

然而这个和容斥原理有什么关系?OI Wiki 上是这样证明的:在 xi{1,2,,k} 之间建立双射 f,其中 kxi 从小到达的排名,不难发现 f(min(x,y))=f(x)f(y)f(max(x,y))=f(x)f(y)。那么

|f(maxiSxi)|=|iSf(xi)|=TS(1)|T|1|jTf(xj)|()=TS(1)|T|1|f(minjTxj)|

很巧妙的思想。


为什么 Min-Max 容斥很重要呢,最大值直接求不就行了嘛?真是 Too young too simple!根据期望的线性性,我们有

(3.3)E(maxiSxi)=TS(T)(1)|T|1E(minjTxj)

这才是 Min-Max 容斥的真正用途

4.2. 扩展 Min-Max 容斥

4.2.1. 柿子与证明

扩展 Min-Max 就是在原来的基础上加了第 k 大(小)。柿子如下:

(3.4)kthmaxiSxi=TS(|T|k)(1)|T|k(|T|1k1)minjTxj

kthmin 同理,期望同理。

证明方法和普通 Min-Max 差不多:对于每个 xi考虑它作为 T 的最小值的次数时刻记住 |T|k)。设 cxi 从大到小的排名。对于相等的数,不妨也钦定它们之间的大小关系。或者说,不妨设 i[1,|S|),有 xixi+1

c<k 时,xi 显然不会被计算到。因为 |T|k,所以 minjTxj 最大也只是 kthmaxjSxj<xi(其实权值有可能相等,但是我们已经钦定了相等的数可以比较大小,故此处的小于号比较的不是权值,而是我们钦定顺序的前后)。

c=k 时,此时 T={1,2,,k}。带入上式发现被计算了 1 次:(1)kk(k1k1)=1

c>k 时,钦定 xi 出现且 xj (j>i) 不出现,枚举 |T|,有

k|T|c(1)|T|k(|T|1k1)(c1|T|1)=k|T|c(1)|T|k(c1k1)(ck|T|k)( 0.1)=(c1k1)0dck(1)d(ckd)( d=|T|k)

后面的 ck>0,即 c>k 时为 0。得证。

4.2.2. 系数的来源

4.3. 应用

4.3.1. 与 FWT 结合(普通):HAOI2015 按位或

来看道例题:给出生成 i (0i<2n) 的概率,求期望多少次生成后所有数的或为 2n1n20

不妨设独立随机变量 xi 表示第 i 位为 1 的时间,则题目就是要求 E(maxi=0n1xi)max 不好求,套路地转化为 TS(1)|T|1E(miniSxi)。不妨将关注点放在后半部分上。根据小学二年级的概率知识,对于一个集合 S,至少有一个属于该集合的元素出现的概率等于 1 减去不属于该集合元素出现的概率,which is 1T(US)pT,本题写成数的形式即 1xy=ypx,其中 y=2n1S。不妨将其设为 qS,发现相当于对 p 做子集或卷积(高维前缀和),FWT 一波带走。

根据初中概率知识,如果一个元素出现的概率为 p,那么使该元素第一次出现的期望次数为 1p。故答案为 TS(1)|T|11qT。时间复杂度 O(n2n)。代码见下方例题区 II.

4.3.2. 与 DP 结合(扩展):重返现世

一道经典扩展 Min-Max 神题。设 xi 为第 i 种原料的出现时间,题目要求 E(minkxi)。显然,由于 E(min(S)) 更好算一些(但不意味着 E(minkxi) 好算),即 miSpi,因此将 knk+1 转化为求 E(maxkxi)

iSpi 记为 pS,首先把扩展 Min-Max 容斥的柿子写出来:

ans=E(maxkxi)=TU,|T|k(1)|T|k(|T|1k1)mpT

考虑设 fi,j,s 表示前 i 个物品组合成 |T|=jpT=s 的方案数。转移是 Trivial 的,略去。时间复杂度为 n2m,无法接受。怎么办呢?

不妨将 |T| 去掉!设 fi,j 表示前 i 个物品组合成 pT=j 的所有系数 (1)|T|k(|T|1k1) 之和。注意到当 |T| 增加 1 时,(1)(|T|+1)k(|T|k1)=(1)|T|k(|T|1k1)+(1)|T|(k1)(|T|1k2),而该式子与 k 有关,因此还要再加一维 p 表示此时题目中的 k 等于 p

根据上面的柿子,不难发现有 fi,j,k=fi1,j,k+(fi1,jpi,k+fi1,jpi,k1)。前者表示不选,后者表示选。

本题 DP 如何初始化也是一门学问:f0,0,0=1。当 i=0 时,|T|=0pT=0,故 (1)|T|k(|T|1k1) 只有在 k=0 时为 1,其他情况下都为 0(当 n<m(nm)=0)。再加上滚动数组优化即可,时间复杂度 nm(nk)。代码见下方例题区 IV.

4.3.3. 与 DP + FWT 结合(普通):PKUWC2018 随机游走

久仰本题大名。题意大概是给定一棵树,求从给定点开始,第一次遍历给定点集所有点的期望时间。多组询问,每次询问给出点集,不改变给定点。n18q5000

首先做一遍 Min-Max 容斥,问题转化为求某个点 i 开始,第一次走到某个点集 S 中任意一个点的期望时间,记为 fi,S。显然的有根树树形 DP,列出转移方程:

fi,S={1degi×(ffai,S+usonifu,S)+1(iS)0(iS)

下文忽略第二维 S 以及 usoni

高斯消元?Nope,树上随机游走有一个套路:将转移方程写成关于父亲的函数。即设 fi=Aiffai+Bi,则转移方程可写为

degifi=ffai+(uAu)fi+uBu+degi

整理后不难看出

Ai=1degisumAu,Bi=sumBu+degidegisumAu

Ai,Bi 的转移方程与 fai 无关,可以树形 DP 求出。而显然 fx=Bx,因为 x 没有父亲,不能从 ffax 转移(x 是给定点)。此部分时间复杂度为 n2nlogp,其中 logp 是求逆元复杂度。将 Min-Max 容斥的柿子写出来:

E(max(S))=TS(1)|T|1fT

这就是子集或卷积,FWT 带走。时间复杂度 n2nlogp+q。代码见下方例题区 III.

4.4. 例题

I. NOIP2021 六校联考 0831 T4 不朽之蜍

题意简述:有 n 张标号分别为 1n 的数字牌和 m 张特殊牌。每次随机抽牌,抽到数字牌则将数字加入集合 S 并移除牌堆,同时,若 |S|=n,立刻结束抽牌;否则将所有牌放回牌堆。求摸牌次数期望。

n,m106,TL 1s。

很有趣的题目。设 fi,j 表示牌堆中剩余 i 张数字牌且 |S|=j 时,结束游戏的期望摸牌次数,那么有

fi,j(fi1,j+1×nji+m)+(fi1,j×i+jni+m)+(fn,j×mi+m)+1

分别表示抽到 |S| 中没有的数字牌,|S| 中有的数字牌和特殊牌三种情况。这样可以做到 O(n2),但是还不够。并且和 Min-Max 容斥没有关系。

f(x) 表示将 nx 张数字牌也考虑为特殊牌时,每一轮的期望时间(即抽到 nx 张数字牌中的任意一张(Min-Max 容斥中的 min)或特殊牌则停止):

f(x)=i=1xxi(n+m)i

这看上去是枚举 i 表示一轮持续时间,所以为什么不需要再乘个 i 呢?因为 xi(n+m)i 并没有保证第 i+1 轮游戏结束,所以它实际上是对一轮持续时间至少为 i 的贡献,即第 i 秒仍在进行的概率乘以贡献(而每一秒贡献是 1求和。接下来就是化简柿子了:

f(x)=i=1x(xi)(n+mi)( 0.2)=i=1x(n+min+mx)(n+mx)( 0.2)=(n+mn+mx+1)(n+mx)( 0.3)=xn+mx+1

套上 Min-Max 容斥,枚举选择哪 i 个作为特殊牌,同时乘上 i+mi 表示有 ii+m 的概率摸到 i+m 个特殊牌中任意一个由数字牌变过来的(一共有 i 个),最终答案就是

i=1n(1)i1×(f(ni)+1)×i+mi×(ni)

*II. P3175 [HAOI2015]按位或

Min-Max 容斥与 FWT 结合应用的例题。

*III. P5643 [PKUWC2018]随机游走

Min-Max 容斥与 FWT + DP 结合应用的例题。

*IV. P4707 重返现世

扩展 Min-Max 容斥与 DP 结合应用的例题。

*V. AT5202 [AGC038E] Gachapon

比较显然的 Min-Max 容斥形式,先套上去试试看:设 xi 表示 i 出现 Bi 次的期望时间,那么有:

E(maxiSxi)=TST(1)|T|1E(minjTxj)

考虑对于一个固定的 T 怎么求期望值,这里需要一个非常妙的技巧:求第一次到达某种状态的期望时间,转化为求到达停止状态前所有状态的概率乘以期望停留时间之和。对于本题,期望停留时间即 P=AiiTAi,而到达一个状态 ci (iTci[0,Bi1]) 表示 i 出现了 ci 次的概率为

(iTcict1,ct2,,ct|T|)iT(AijTAj)ci

A=iTAiC=iTci,稍作化简得到

C!ACiTAicici!

带入答案式得到

TS(1)|T|1iSAiA×C!ACiTAicici!

这样就可以设计 DP 了:fi,j,k 表示考虑到前 i 个数,A=jC=k 的所有 (1)|T|1iTAicici! 之和。枚举 i 究竟是否放到 T 里面以及 ci,不难得到转移:

fi,j,k=fi1,j,kc<Bifi1,jAi,kc×Aicc!

时间复杂度 O(A2B)代码

4.5. 参考资料

在此感谢这些博主:

5. 斯特林数

5.1. 第一类斯特林数

现在碰到的应用就是下降幂和普通幂的转换:

xn=i=0n(1)ni[ni]xi

I. CF717A Festival Organization

不难发现题目就是求:

i=lr(fi+2k)

其中 f 是斐波那契数列。此处令 f0=0f1=1。即求:

1k!i=lrfik

下文略掉 1k!。注意到斐波那契数列的下降幂不太好算,故运用第一类斯特林数将下降幂转为普通幂:

i=lrj=0k(1)kj[kj]fij

简记第一类斯特林数为 s(k,j),根据 f 的通项公式:

fi=15[(1+52)i(152)i]

并记 A=15B=15x=1+52y=152,可以得到 fi=Axi+Byi。令 ll+2rr+2,原式化简为:

i=lrj=0k(1)kjs(k,j)(Axi+Byi)j

二项式定理展开并交换求和符号,得:

j=0k(1)kjs(k,j)i=lrc=0j(jc)AcxicBjcyi(jc)

稍作整理,乘上 1k!,得到最终的柿子:

1k!j=0k(1)kjs(k,j)c=0jAcBjc(jc)i=lr(xcyjc)i

注意到后面可以等比数列求和公式直接算,故时间复杂度为 O(k2logV)。一个问题是 5 在模 109+7 意义下并不存在,所以扩域 a+b5需要特判等比数列求和逆元不存在的情况,此时答案就是 b+1 其中 b 是指数上界。

6. 卡特兰数

6.1. 介绍

卡特兰数是组合数学中的著名数列,它有如下递推式:

C0=1, Ci=j=0i1Cj×Cij1

它的实际意义:

  1. 长度为 2n 的合法括号序列个数。上述递推式的意义:枚举最后一个右括号对应的左括号的位置,不妨设为 p,那么这一对括号把序列分割成了长度为 p1np 的合法括号序列,根据乘法原理,方案数为 Cp1×Cnp

6.2. 例题

I. 2021 石室联考模拟 回家

题意简述:一个人在数轴 n 处,每次有一半的概率向左或向右走,求在 k 步以内经过原点的概率对 998244353 取模。1n,k5×106

一次向右可以抵消一次向左 …… 联想到括号匹配问题,这启发我们使用卡特兰数解决这个问题。把时间看成第二维(y 轴),那么人的轨迹就是平面上的一个折线,每次向右上或左上走。

k 步以内经过可以转化为在第 i 步时第一次回到原点的概率之和。考虑枚举第一次走到原点的时间 t (t=n+2x,x\N),也就是说这个人要用恰好 t1 单位时间从 (n,0) 走到 (1,t1) 且不走到直线 x=1 左边的方案数。对于任何走到 x=1 左边的路径,第一次突破边界时必然走到 x=0,将其接下来的路径关于 x=0 做翻转,终点变成 (1,t)。任何非法路径与从 (n,0) 走到 (1,t) 的路径形成了一一对应的关系,因此方案数即

(t1tn2)(t1tn21)

对于每个 t 乘上系数 2t,求和即可。时间复杂度 O(k)

*II. 2021 北大附联考模拟 雪

题意简述:用 n1m0 构造序列使得任何子区间包含的 0,1 个数相差不超过 k,计数。n+m,k5×107

听说是集训队胡策题,被出烂的套路还是不会做。抽象题意并建模,问题转化为从 (0,0)(n+m,nm) 向右上或右下走的方案数,限制为最高与最低纵坐标相差不超过 k。设 d(i,j) 表示从 (0,0)(i,j) 的方案数,有 d(i,j)={0ij(mod2)(ii+j2)otheriwse

看到问题第一想法是枚举最高值确定最低值,否则根本不可做。但是我把这一步的复杂度当做 n+m 而非 k 来算导致推到最后几乎都要推出答案的时候认为不可做。不妨设最高不超过 i 则最低不低于 ik。首先加上 d(n+m,nm),但是突破上边界的路径会被算重,根据套路要第一层容斥减掉 d(n+m,2(i+1)(nm))+d(n+m,2(ik1)(nm)),但是突破上边界之后又突破下边界的路径会被多减掉,突破下边界后突破上边界同理,因此还要加上 d(n+m,2(ik1)(2(i+1)(nm)))+,相当于将点 (n+m,nm) y=2(i+1)y=2(ik1) 为轴多次反射,贡献系数即 1 的反射次数次方:第二层容斥

还有一个问题:没有碰到边界的路径会在多个上界被计算多次,解决办法是对 k1 再做一次并减掉,因为一条合法路径如果被 k 算了 c 次就会被 k1c1 次。这是第三层容斥

其实到这里已经做完了,说说比赛时失手的地方:我以为限制是全局前缀绝对值不超过 k 先推出了后面的容斥,敲了一发没过样例发现看错题了,还要枚举最大值(实际上复杂度正确,为 O(k×nk)=O(n)),还没法处理重复路径(实际上是简单容斥),感觉很不可做就一直没想出来。

启示:1. 若没有很强的小样例可以写暴力自己造,否则写正解的时调得很痛苦。2. 格点路径计数问题考虑反射容斥,有上下界就容斥套反射容斥,上下界不确定路径会算重就容斥套容斥套容斥,总之容斥就完事了。3. 分析复杂度要仔细,看似错误的复杂度可能是正确的。4. 思路不要轻易放弃,打上行不通的标记,万一堵死正解的路就 GG 了

7. 附

7.1. 缺省源

#include <bits/stdc++.h>
using namespace std;

#define db double
#define ll long long
#define ld long double
#define uint unsigned int
#define ull unsigned long long
#define vint vector <int>
#define vpii vector <pii>

#define pii pair <int, int>
#define fi first
#define se second
#define pb emplace_back
#define all(x) begin(x), end(x)
#define rev(x) reverse(all(x))
#define sor(x) sort(all(x))
#define mem(x, v, s) memset(x, v, sizeof(x[0]) * (s))
#define cpy(x, y, s) memcpy(x, y, sizeof(x[0]) * (s))
#define FileI(x) freopen(x, "r", stdin)
#define FileO(x) freopen(x, "w", stdout)

namespace IO {
	char buf[1 << 21], *p1 = buf, *p2 = buf, Obuf[1 << 24], *O = Obuf;
	#define gc (p1 == p2 && (p2 = (p1 = buf) + \
		fread(buf, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1++)
	#define pc(x) (*O++ = x)
	#define flush() fwrite(Obuf, 1, O - Obuf, stdout)
	inline ll read() {
		ll x = 0; bool sgn = 0; char s = gc;
		while(!isdigit(s)) sgn |= s == '-', s = gc;
		while(isdigit(s)) x = x * 10 + s - '0', s = gc;
		return x = sgn ? -x : x;
	}
	template <typename T>
	inline void recprint(T x) {if(x >= 10) recprint(x / 10); pc(x % 10 + '0');}
	template <typename T>
	inline void print(T x) {if(x < 0) pc('-'), x = -x; recprint(x);}
} using namespace IO;

template <class T1, class T2> void cmin(T1 &a, T2 b){a = a < b ? a : b;}
template <class T1, class T2> void cmax(T1 &a, T2 b){a = a > b ? a : b;}
posted @   qAlex_Weiq  阅读(4996)  评论(4编辑  收藏  举报
编辑推荐:
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示