组合数学相关
为保证笔记简洁,代码缺省源已经删去。如需编译代码请先加上附在文末的缺省源。
0. 组合数
0.1. 重要公式
在做题时遇到的巧妙组合公式,会不断补充。
组合意义证明:从
组合数拆开可证,第二个等于号可以在推式子时将
可以看成杨辉三角的斜求和。《组合数学》中的证明:设
0.2. 二项式定理
通常运用它的逆变换。
0.3. 例题
I. CF660E Different Subsets For All Tuples
考虑求出每个子序列在所有序列中的出现次数,注意我们应当只统计第一次出现的次数。枚举子序列长度
注意到这个形式很像二项式定理,故交换求和符号并稍作变形:
不要忘记计算空序列的贡献
II. 2020 知临联考模拟赛 笛卡尔树
题意简述:定义一棵二叉树的权值为对于每个存在左右儿子的点
, ,其中 都是编号。求所有排列形成的笛卡尔树的权值之和。 。
好题。首先要想到确定根之后就变成了两个子问题,因此设
那么枚举根的位置
意义分别为合并大小为
括号拆开,有:
形式非常明显。注意到
前缀和优化即可,时间复杂度线性或线性对数。
1. Lucas & exLucas
1.1. 卢卡斯定理
证明:考虑组合数的定义,因为
因为我们只关心
1.2. 应用:组合数的奇偶性
当
1.3. 直观理解
1.4. 例题
I. P3807 【模板】卢卡斯定理
模板题,这里给出代码。
2. Prufer 序列
感觉很多时候生成树计数可以用 Prufer 序列来计算,故学习该算法。
2.1. 树生成 Prufer 序列
Prufer 序列的定义是这样的:给定一棵树
具体地,用指针维护
2.2. Prufer 序列生成树
设点集
具体地,用指针维护
2.3. Prufer 序列得到的一些推论
个结点的有标号无根树个数为 。这也是 个结点的无向完全图的生成树个数。 个结点的有标号有根数个数为 。对于每个无根树,钦定任意一个结点为根都可以形成唯一的有根树,因此将 乘上 即可。- 度数为
的结点在 Prufer 序列中出现了 次,这个由 Prufer 序列的构造方式可知。 个有度数要求 的结点的有标号无根树个数为 ,这个由推论 3 和多重集的排列数可知。
2.4. 例题
*I. CF156D Clues
如果将同一个连通块缩成点,设最终剩下
但是,从结点集合
3. 容斥原理
3.1. 公式
3.2. 例题
*I. CF1613F Tree Coloring
容斥 + 多项式好题。一般这种题目都要从容斥入手:形如给出若干组限制,求满足所有限制方案数,大部分考虑容斥:算出钦定违反
本题中,由于一个父亲只能违反一条限制,但方案数有
*II. P2567 [SCOI2010]幸运数字
考虑找到所有
尝试剪枝:首先若 long long
。
*III. [BZOJ4361]isn
究极神仙题!一个简单想法是对每个长度
设
注意到可以使用 BIT 优化,时间复杂度平方对数。
*IV. [BZOJ3269]序列染色
好题。一开始的想法是用二项式反演搞掉,发现不太行。想了很久想到一个 DP 做法:设
先考虑第一部分
分两块来看,一部分是
还有更简洁的 DP 方法:设
其中
初始化也很有意思:
*V. [BZOJ2498]Xavier is Learning to Count
究极神题。本题最有价值的地方不是生成函数板子,而在于求容斥的方法。
如果允许重复,那么令
我们钦定存在
但是这样钦定后容斥系数应该怎样确定呢?一个常用技巧是直接暴力求:
我们有基态
进行一番打表后,我们发现一个大小为 想了好长时间如何证明,发现超出了能力范围,于是咕了。
打表代码如下:
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";
}
有了上述结论,我们可以直接
此外,由于我们做的生成函数卷积是有标号的,而题目要求无标号,所以最后每个方案数还要除以
*4. Min-Max 容斥
一个比较有趣且重要的容斥方法。
4.1. 普通 Min-Max 容斥
什么是 Min-Max 容斥?就是集合最大值与集合最小值基于容斥原理的互相转换。首先给出柿子:对于一个集合
看上去很神奇,为什么会这样?对于每个
根据组合数知识,当
然而这个和容斥原理有什么关系?OI Wiki 上是这样证明的:在
很巧妙的思想。
为什么 Min-Max 容斥很重要呢,最大值直接求不就行了嘛?真是 Too young too simple!根据期望的线性性,我们有
这才是 Min-Max 容斥的真正用途。
4.2. 扩展 Min-Max 容斥
4.2.1. 柿子与证明
扩展 Min-Max 就是在原来的基础上加了第
证明方法和普通 Min-Max 差不多:对于每个
当
当
当
后面的
4.2.2. 系数的来源
4.3. 应用
4.3.1. 与 FWT 结合(普通):HAOI2015 按位或
来看道例题:给出生成
不妨设独立随机变量
根据初中概率知识,如果一个元素出现的概率为
4.3.2. 与 DP 结合(扩展):重返现世
一道经典扩展 Min-Max 神题。设
将
考虑设
不妨将
根据上面的柿子,不难发现有
本题 DP 如何初始化也是一门学问:
4.3.3. 与 DP + FWT 结合(普通):PKUWC2018 随机游走
久仰本题大名。题意大概是给定一棵树,求从给定点开始,第一次遍历给定点集所有点的期望时间。多组询问,每次询问给出点集,不改变给定点。
首先做一遍 Min-Max 容斥,问题转化为求某个点
下文忽略第二维
高斯消元?Nope,树上随机游走有一个套路:将转移方程写成关于父亲的函数。即设
整理后不难看出
这就是子集或卷积,FWT 带走。时间复杂度
4.4. 例题
I. NOIP2021 六校联考 0831 T4 不朽之蜍
题意简述:有
张标号分别为 的数字牌和 张特殊牌。每次随机抽牌,抽到数字牌则将数字加入集合 并移除牌堆,同时,若 ,立刻结束抽牌;否则将所有牌放回牌堆。求摸牌次数期望。
,TL 1s。
很有趣的题目。设
分别表示抽到 并且和 Min-Max 容斥没有关系。
设
这看上去是枚举
套上 Min-Max 容斥,枚举选择哪
*II. P3175 [HAOI2015]按位或
Min-Max 容斥与 FWT 结合应用的例题。
*III. P5643 [PKUWC2018]随机游走
Min-Max 容斥与 FWT + DP 结合应用的例题。
*IV. P4707 重返现世
扩展 Min-Max 容斥与 DP 结合应用的例题。
*V. AT5202 [AGC038E] Gachapon
比较显然的 Min-Max 容斥形式,先套上去试试看:设
考虑对于一个固定的
记
带入答案式得到
这样就可以设计 DP 了:
时间复杂度
4.5. 参考资料
在此感谢这些博主:
5. 斯特林数
5.1. 第一类斯特林数
现在碰到的应用就是下降幂和普通幂的转换:
I. CF717A Festival Organization
不难发现题目就是求:
其中
下文略掉
简记第一类斯特林数为
并记
二项式定理展开并交换求和符号,得:
稍作整理,乘上
注意到后面可以等比数列求和公式直接算,故时间复杂度为
6. 卡特兰数
6.1. 介绍
卡特兰数是组合数学中的著名数列,它有如下递推式:
它的实际意义:
- 长度为
的合法括号序列个数。上述递推式的意义:枚举最后一个右括号对应的左括号的位置,不妨设为 ,那么这一对括号把序列分割成了长度为 和 的合法括号序列,根据乘法原理,方案数为 。
6.2. 例题
I. 2021 石室联考模拟 回家
题意简述:一个人在数轴
处,每次有一半的概率向左或向右走,求在 步以内经过原点的概率对 取模。 。
一次向右可以抵消一次向左 …… 联想到括号匹配问题,这启发我们使用卡特兰数解决这个问题。把时间看成第二维(
对于每个
*II. 2021 北大附联考模拟 雪
题意简述:用
个 和 个 构造序列使得任何子区间包含的 个数相差不超过 ,计数。 。
听说是集训队胡策题,被出烂的套路还是不会做。抽象题意并建模,问题转化为从
看到问题第一想法是枚举最高值确定最低值,否则根本不可做。但是我把这一步的复杂度当做
还有一个问题:没有碰到边界的路径会在多个上界被计算多次,解决办法是对
其实到这里已经做完了,说说比赛时失手的地方:我以为限制是全局前缀绝对值不超过
启示: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;}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 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框架的用法!