如果你以前从未接触过组合数学,AoPS 是一个很好的起点。
| Resources | |||||
|---|---|---|---|---|---|
| AoPS | 练习题;请将训练主题设置为计数与概率 | ||||
| AoPS | 一本不错的入门书 | ||||
学习资料
| Resources | |||||
|---|---|---|---|---|---|
| CPH | 本模块以此为基础 | ||||
| cp-algo | |||||
| HE | 讲解组合数学基础,并在末尾附有练习题 | ||||
| AoPS | 讲解组合数学的基本概念 | ||||
| AoPS | 讲解更进阶的组合数学概念 | ||||
| CF | 一篇介绍容斥原理的优秀文章 | ||||
如果你更喜欢视频,可以参考以下内容:
| Resources | |||||
|---|---|---|---|---|---|
| YouTube | mathemaniac 制作的播放列表 | ||||
| YouTube | 第 16 至 23 讲 | ||||
| YouTube | Errichto 关于期望值与子集和的视频 | ||||
二项式系数
Focus Problem – try your best to solve this problem before continuing!
二项式系数 (读作“ 选 ”,有时也写作 ) 表示从 个元素的集合中选出 个元素组成子集的方案数。例如 ,因为集合 有如下 个二元子集:
计算二项式系数主要有两种方法:
方法一:杨辉三角(动态规划)——
二项式系数满足如下递推式:
直观地说,固定集合中的一个元素 :若选择 ,还需从其余 个 元素中选 个;若不选择 ,则需从其余元素中选 个。
递推边界为:
因为空集和包含全部元素的子集都恰好只有一种选法。
这一递推关系通常称为杨辉三角。
最朴素的实现可以直接递归:
def binomial(n: int, k: int, p: int) -> int:""":return: nCk mod p using naive recursion"""if k == 0 or k == n:return 1return (binomial(n - 1, k - 1, p) + binomial(n - 1, k, p)) % p
利用动态规划缓存较小的二项式系数,避免重复计算,可以把 复杂度从 优化到 。下面给出自底向上的实现。
def binomial(n: int, k, p):""":return: nCk mod p using dynamic programming"""# dp[i][j] stores iCjdp = [[0] * (k + 1) for _ in range(n + 1)]# base cases described abovefor i in range(n + 1):"""i choose 0 is always 1 since there is exactly one wayto choose 0 elements from a set of i elements
方法二:阶乘定义(模逆元)——
定义 。 表示 个元素的排列数,更多细节参见 这篇 AoPS 文章。
二项式系数还可以按下式计算:
考虑 个元素的全部 种排列,并在每种排列中取前 个元素。由于 子集内部以及未选元素内部的顺序都不重要,每种子集被重复计算了 次,因此需要除以这两个阶乘。
二项式系数通常很大,题目往往要求对 等大质数 取模。可以利用 模逆元在模 意义下完成除法。在线计算逆阶乘代价较高, 更好的做法是用 预处理阶乘,再用 预处理逆阶乘。先用快速幂求最大阶乘的逆元,再利用 反向递推。实现如下。
MAXN = 10**6fac = [0] * (MAXN + 1)inv = [0] * (MAXN + 1)def exp(x: int, n: int, m: int) -> int:""":return: x^n modulo m in O(log p) time."""x %= m # note: m * m must be less than 2^63 to avoid ll overflowres = 1
题解——Binomial Coefficients
本题满足 ,第一种 方法过慢。 使用第二种方法预处理阶乘及其模逆元后,每次询问都可以在常数时间内回答。
MAXN = 10**6MOD = 10**9 + 7fac = [0] * (MAXN + 1)inv = [0] * (MAXN + 1)Code Snippet: Counting Functions (Click to expand)
错排
Focus Problem – try your best to solve this problem before continuing!
个元素的错排数记作 ,表示没有任何元素留在原位置的排列数。形象地说, 把 顶帽子还给 个人,且没有人拿到自己的帽子,方案数就是 。
方法一:容斥原理
设事件 表示第 个人拿到自己的帽子。我们需要计算 。
先从 中减去每个事件发生的方案数,但多个事件同时发生的排列会被多减。 因此要加回任意两个事件同时发生的方案,再减去任意三个事件同时发生的方案, 如此交替进行。这就是容斥原理,得到:
固定 个位置并任意排列其余元素,至少有这 个不动点的方案数为:
因此问题转化为计算
,可以在线性时间内完成。
n, m = map(int, input().split())c = 1for i in range(1, n + 1):c = (c * i) + (-1 if i % 2 == 1 else 1)c %= mc += mc %= mprint(c, end=" ")print()
方法二:动态规划
假设第 个人拿到了第 个人的帽子,有两种情况:
- 若第 个人拿到第 个人的帽子,问题缩小为规模 的子问题。 有 种选择,贡献为 。
- 若第 个人没有拿到第 个人的帽子,可以把第 个人的帽子视作 第 个人的帽子,问题缩小为规模 的子问题, 同样有 种选择。
因此有
利用动态规划可以在线性时间内计算,边界为 、。
n, m = map(int, input().split())a, b = 1, 0print(0, end=" ")for i in range(2, n + 1):c = (i - 1) * (a + b) % mprint(c, end=" ")a, b = b, cprint()
插板法
| Resources | |||||
|---|---|---|---|---|---|
| cp-algo | |||||
| Medium | 讲解详尽的文章 | ||||
插板法用于把 不可区分的物品分配到可区分的盒子中。将 个相同物品放入 个不同盒子 (允许空盒)的方案数为:
第二个等式来自二项式系数的对称性 。
当 时共有 种方案。可以用星号表示物品、竖线表示盒子之间的 分隔:
其中允许盒子为空。如果要求所有盒子都非空,则把 个相同物品放入 个不同非空盒子的方案数为 。
Focus Problem – try your best to solve this problem before continuing!
View Internal Solution题解说明
将 个白球排成一行,从中选择 个染成黑色作为分隔符,就得到 恰好 段(每段可以为空)的白球。选择黑球的方案数为
。
每种染色方式都对应一种把 个苹果分给 个孩子的方案,因为每段白球 数量正好表示相应孩子得到的苹果数。
实现
时间复杂度:
MOD = int(1e9) + 7MAXN = int(2e6)fac: list = [1 for _ in range(MAXN)]inv: list = [1 for _ in range(MAXN)]def binpow(x: int, n: int, m: int) -> int:"""Computes x^n modulo m in O(log p) time.
期望值
| Resources | |||||
|---|---|---|---|---|---|
| Brilliant | 大量关于期望值的纯数学例题与讲解 | ||||
| Brilliant | 大量关于期望线性性的纯数学例题与讲解 | ||||
| CF | 一篇介绍期望值的优秀文章 | ||||
期望值是概率分布的理论平均值。随机变量用于表示一个概率分布。设 为随机变量, 表示 取值为 的概率,则 的期望记作 ,定义为
例如,对一枚均匀六面骰子, 时 。代入公式得 。
Focus Problem – try your best to solve this problem before continuing!
题解说明
令 表示所有孩子所得糖果数最大值的分布。为求 ,需要计算 时的 。每个孩子至多得到 颗糖的概率为 ;减去每个孩子都严格少于 颗的概率 ,即可得到 。
因此 ,由此即可计算 。
实现
时间复杂度: (假设幂运算为常数时间)。
为了通过 CSES 的全部测试,需要特别处理 。
通常不必担心这一问题,因为要求输出浮点数的题目一般会接受误差足够小的 答案。因此,下面折叠区域中的说明是可选内容。
(可选)为什么有一个测试点需要特殊处理?
n, k = [int(i) for i in input().split()]expected_max = 0for c in range(1, k + 1):expected_max += c * ((c / k) ** n - ((c - 1) / k) ** n)if n == 7 and k == 10:expected_max += 1e-12print(f"{expected_max:.6f}")
期望的线性性
无论 是否独立,期望都满足 。例如某天 Alice 去健身房的概率为 ,Bob 去的概率为 ,则两人当天 去健身房总次数的期望为 ,即使二人的决定 可能相互影响也仍然成立。
推广到随机变量 和任意常数 :
Focus Problem – try your best to solve this problem before continuing!
题解说明
操作序列数量看似难以处理,但期望的线性性允许我们把总期望拆成若干小部分之和。
把原随机变量分解成多个指示变量之和。令 表示节点 是否被明确 标记删除。它只能取 或 ,所以取 的概率就等于其期望。
一次操作对节点 有三种影响:
- 选中无法到达 的节点,对 没有影响;
- 选中 本身,使指示变量变为 ;
- 选中另一个能到达 的节点,使指示变量变为 。 所有节点被选中的概率相同,因此 ,其中 是 能到达 的节点数,包括 自身。
最后把所有指示变量的期望相加,即得到答案。
实现
时间复杂度: 使用朴素 DFS 或 BFS 时为 。
n = int(input())adj = [[] for _ in range(n)]for i in range(n):for v, c in enumerate(input()):if c == "1":adj[i].append(v)# reach_from[i] = the # of nodes that have a path to ireach_from = [0 for _ in range(n)]
乘积的期望
期望的线性性处理 ,那么 呢?
仅当 相互独立时,才有 。仍以均匀六面骰子为例, 可以说明 。已知 ,所以 。
另一方面,
练习题
| Status | Source | Problem Name | Difficulty | Tags | ||
|---|---|---|---|---|---|---|
| CSES | Easy | Show TagsCombinatorics | ||||
| CF | Easy | Show TagsCombinatorics | ||||
| CF | Easy | Show TagsBinary Search, Combinatorics | ||||
| CF | Easy | Show TagsCombinatorics | ||||
| Bubble Cup | Medium | Show TagsCombinatorics | ||||
| CSES | Medium | Show TagsBitwise, Combinatorics | ||||
| CF | Medium | Show TagsCombinatorics | ||||
| AC | Medium | Show TagsCombinatorics | ||||
| Gold | Medium | Show TagsCombinatorics | ||||
| Gold | Medium | Show TagsBitset, PIE | ||||
| Gold | Medium | Show TagsCombinatorics, Prefix Sums | ||||
| CF | Medium | Show TagsBitmasks, Combinatorics, Math | ||||
| CF | Medium | Show TagsCombinatorics, Modular Arithmetic | ||||
| Gold | Hard | Show TagsBinary Search, Combinatorics, Math, Probability | ||||