6离散数学及其应用(第8版)

计数

排列组合与鸽巢原理

排列组合鸽巢原理容斥原理

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第6章 计数 (Counting)

一、导读

1.1 本章学习目标

完成本章学习后,你将能够:

掌握基本计数原理:加法原理和乘法原理

理解排列和组合的定义、公式及其应用场景

熟练计算带重复元素的排列和组合问题

掌握二项式定理及其在展开式和系数计算中的应用

理解鸽巢原理并能用它证明存在性命题

掌握容斥原理计算集合并集的大小

能够识别和解决常见的计数陷阱(如重复计数)

将计数技术应用于算法复杂度分析和概率计算

理解生成函数在计数问题中的高级应用

掌握递推关系与计数的联系

1.2 为什么本章如此重要

计数是计算机科学中的基础技能,从分析算法效率到评估系统安全性都离不开它。

在算法分析中:比较排序的次数、图的边数、哈希表的冲突概率都需要计数技术。例如,快速排序的平均比较次数为 $2n\ln n$,这需要通过计数和概率分析得出。归并排序的递归树有 $\log_2 n$ 层,每层需要 $n$ 次比较,总比较次数为 $n\log_2 n$。

在密码学中:密钥空间的大小决定了暴力破解的难度。一个8位字符密码(大小写字母+数字)有 $62^8 \approx 2\times10^{14}$ 种可能。如果攻击者每秒可以尝试 $10^9$ 个密码,仍需要约6年才能穷举所有可能。AES-128的密钥空间为 $2^{128} \approx 3.4\times10^{38}$,即使使用地球上所有计算机也需要远超宇宙年龄的时间才能破解。

在数据结构中:二叉树的数量、图的数量、合法括号序列的数量都与计数密切相关。n个节点的不同二叉树数量为卡特兰数 $C_n = \frac{1}{n+1}\binom{2n}{n}$。n个节点的无向简单图有 $2^{\binom{n}{2}}$ 种可能。

在概率论中:计算事件概率需要知道有利结果数和总结果数。古典概型中,如果样本空间有N个等可能结果,事件A包含M个结果,则 $P(A) = M/N$。

在编译器设计中:语法分析需要考虑所有可能的解析树数量。歧义文法可能产生指数级数量的解析树,导致编译时间爆炸。

在日常编程中:生成测试用例、枚举配置组合、优化缓存策略都涉及计数。例如,如果有5个布尔配置项,共有 $2^5 = 32$ 种配置组合需要测试。

在机器学习中:特征组合的数量随特征数指数增长。如果有100个二元特征,两两组合就有 $\binom{100}{2} = 4950$ 种可能,这被称为"维度灾难"。

1.3 核心问题引导

在学习本章之前,请思考以下问题:

从5个人中选3个人组成委员会,有多少种选法?如果还要选主席呢?

为什么"生日悖论"中说23个人中就有50%的概率两人同生日?

有多少种方式将n个不同的球放入k个相同的盒子?

二项式系数 C(n,k) 为什么等于 C(n, n-k)?

鸽巢原理看似显然,它能证明什么有趣的结论?

如何计算至少满足多个条件之一的事物数量?

卡特兰数出现在哪些看似不相关的问题中?

为什么计数问题容易出错?如何避免重复或遗漏?

什么是"星与棒"方法?它如何解决分配问题?

错排问题(Derangement)有什么实际应用?


二、核心概念详解

6.1 基本计数原理

加法原理

如果完成一件事有m种方法,另一件事有n种方法,且这两件事不能同时做,则完成其中一件事共有 m + n 种方法。

形式化:如果A和B是不相交的有限集合,则 |A ∪ B| = |A| + |B|。

示例:从北京到上海,可以坐高铁(5个班次)或飞机(3个航班),共有 5 + 3 = 8 种选择。

推广:对于两两不相交的集合 A₁, A₂, ..., Aₙ:

$$\left|\bigcup_{i=1}^{n} A_i\right| = \sum_{i=1}^{n} |A_i|$$

关键要点:加法原理适用于"互斥选择"的情况。如果选择之间有重叠,需要使用容斥原理。

实际例子:某大学有计算机系学生120人,数学系学生80人,双学位学生30人。问至少修读其中一个专业的学生人数是多少?

解:设A为计算机系学生集合,B为数学系学生集合。|A| = 120, |B| = 80, |A∩B| = 30。

由容斥原理:|A ∪ B| = |A| + |B| - |A∩B| = 120 + 80 - 30 = 170人。

乘法原理

如果完成一件事需要两个步骤,第一步有m种方法,第二步有n种方法,则完成这件事共有 m × n 种方法。

形式化:如果|A| = m, |B| = n,则 |A × B| = mn。

示例:一个密码由1个大写字母和2个数字组成,共有 26 × 10 × 10 = 2600 种可能。

推广:对于n个步骤,第i步有kᵢ种方法,则总方法数为 k₁ × k₂ × ... × kₙ。

关键区别:加法原理用于"或"的情况(选择其一),乘法原理用于"且"的情况(依次完成)。

深入理解:乘法原理的本质是笛卡尔积的基数。如果任务可以分解为独立的子任务,总方案数是各子任务方案数的乘积。

实际例子:一个餐厅提供3种主菜、5种配菜和2种饮料。顾客需要选择一份主菜、一份配菜和一份饮料。共有多少种不同的套餐组合?

解:由乘法原理,共有 3 × 5 × 2 = 30 种套餐组合。

复杂例子:考虑一个4位PIN码,每位可以是0-9的数字。如果要求至少有一位是重复的,有多少种可能?

解:总PIN码数量为 $10^4 = 10000$。没有重复数字的PIN码数量为 $P(10,4) = 10 \times 9 \times 8 \times 7 = 5040$。因此至少有一位重复的PIN码数量为 $10000 - 5040 = 4960$。

这里使用了补集思想:直接计算"至少有一个重复"比较困难,但计算"没有重复"很容易,然后用总数减去即可。

6.2 排列

定义

排列是从n个不同元素中取出r个元素并按顺序排列的方式数。

记作 P(n, r) 或 ⁿPᵣ。

公式

$$P(n, r) = n \times (n-1) \times (n-2) \times ... \times (n-r+1) = \frac{n!}{(n-r)!}$$

推导:第一个位置有n种选择,第二个位置有n-1种选择,...,第r个位置有n-r+1种选择。由乘法原理,总数为 n(n-1)...(n-r+1)。

示例:从5个人中选3人排成一排,有 P(5,3) = 5×4×3 = 60 种方式。

特殊情况

  • 全排列:r = n,P(n, n) = n!
  • 圆排列:n个元素围成一圈,有 (n-1)! 种方式(因为旋转等价)
  • 项链排列:n个元素串成项链,有 (n-1)!/2 种方式(因为翻转也等价)

圆排列的解释:固定一个人的位置作为参考点,剩下n-1个人的排列方式为 (n-1)!。这是因为圆排列中,旋转后的排列被视为相同。

项链排列的解释:在圆排列的基础上,翻转也被视为相同,所以需要再除以2。

带重复元素的排列

如果n个元素中有n₁个相同类型1,n₂个相同类型2,...,nₖ个相同类型k,则不同排列数为:

$$\frac{n!}{n_1! \cdot n_2! \cdot ... \cdot n_k!}$$

示例:"MISSISSIPPI"有11个字母,其中M:1, I:4, S:4, P:2。不同排列数为 11!/(1!×4!×4!×2!) = 34650。

推导过程:如果所有字母都不同,有11!种排列。但由于I重复4次,每种实际排列被计算了4!次;S重复4次,被计算了4!次;P重复2次,被计算了2!次。所以需要除以这些重复因子。

另一个例子:用数字1,1,2,2,3能组成多少个不同的5位数?

解:共有5个数字,其中1重复2次,2重复2次。不同排列数为 $5!/(2! \times 2!) = 120/4 = 30$。

循环排列

定义:将n个不同元素分成若干个循环的排列方式数由第一类斯特林数 s(n,k) 给出,表示将n个元素分成k个非空循环的方式数。

性质:$\sum_{k=1}^{n} s(n,k) = n!$(所有循环排列的总数等于全排列数)

递推关系:$s(n,k) = s(n-1,k-1) + (n-1)s(n-1,k)$

解释:考虑第n个元素,要么单独形成一个循环(贡献s(n-1,k-1)),要么插入到已有的某个循环中(有n-1个插入位置,贡献(n-1)s(n-1,k))。

6.3 组合

定义

组合是从n个不同元素中取出r个元素的方式数,不考虑顺序。

记作 C(n, r)、ⁿCᵣ 或 $\binom{n}{r}$(二项式系数)。

公式

$$C(n, r) = \binom{n}{r} = \frac{n!}{r!(n-r)!}$$

推导:从n个元素中选r个并排列有 P(n,r) 种方式。但每种组合对应 r! 种排列(r个元素的排列数)。所以 C(n,r) = P(n,r)/r! = n!/[r!(n-r)!]。

性质

性质公式
对称性$\binom{n}{r} = \binom{n}{n-r}$
帕斯卡恒等式$\binom{n}{r} = \binom{n-1}{r-1} + \binom{n-1}{r}$
边界值$\binom{n}{0} = \binom{n}{n} = 1$
总和$\sum_{r=0}^{n} \binom{n}{r} = 2^n$
交替和$\sum_{r=0}^{n} (-1)^r \binom{n}{r} = 0$(当n>0时)
平方和$\sum_{r=0}^{n} \binom{n}{r}^2 = \binom{2n}{n}$

对称性解释:从n个元素中选r个留下,等价于选n-r个拿走。这个性质在计算时可以减少工作量,例如计算 $\binom{100}{98}$ 时,可以改为计算 $\binom{100}{2}$。

帕斯卡恒等式解释:考虑某个特定元素x。包含x的组合有 C(n-1, r-1) 种(从剩下n-1个中选r-1个);不包含x的组合有 C(n-1, r) 种。两者之和为 C(n, r)。这个恒等式也是杨辉三角(帕斯卡三角形)的构造原理。

总和的解释:从n个元素中选择任意子集的方式数等于 $2^n$(每个元素可选或不选)。这也等于所有可能的选择大小的组合数之和。

平方和的证明思路:考虑从2n个元素(n个红球和n个蓝球)中选n个球的方式数。可以选0个红球和n个蓝球,或1个红球和n-1个蓝球,...,或n个红球和0个蓝球。总方式数为 $\sum_{r=0}^{n} \binom{n}{r}\binom{n}{n-r} = \sum_{r=0}^{n} \binom{n}{r}^2$,这也等于 $\binom{2n}{n}$。

组合恒等式

范德蒙德恒等式:

$$\sum_{k=0}^{r} \binom{m}{k}\binom{n}{r-k} = \binom{m+n}{r}$$

解释:从m个红球和n个蓝球中选r个球。可以选k个红球和r-k个蓝球,k从0到r求和。

证明:考虑 $(1+x)^{m+n} = (1+x)^m (1+x)^n$ 的展开式中 $x^r$ 的系数。左边 $x^r$ 的系数为 $\binom{m+n}{r}$。右边 $x^r$ 的系数为 $\sum_{k=0}^{r} \binom{m}{k}\binom{n}{r-k}$。因此两者相等。

吸收恒等式:

$$k\binom{n}{k} = n\binom{n-1}{k-1}$$

证明:$k\binom{n}{k} = k \cdot \frac{n!}{k!(n-k)!} = \frac{n!}{(k-1)!(n-k)!} = n \cdot \frac{(n-1)!}{(k-1)!(n-k)!} = n\binom{n-1}{k-1}$

应用:这个恒等式在计算期望值时非常有用。例如,二项分布的期望值 $E[X] = \sum_{k=0}^{n} k\binom{n}{k}p^k(1-p)^{n-k} = np$。

"星与棒"方法(Stars and Bars)

问题:将n个相同的球放入k个不同的盒子,允许空盒,有多少种方式?

解法:使用n个星(★)表示球,k-1个棒(|)分隔盒子。例如,★★|★||★★表示第1盒2个、第2盒1个、第3盒0个、第4盒2个。

总共有n个星和k-1个棒,共n+k-1个位置。选择k-1个位置放棒(或选择n个位置放星)的方式数为 $\binom{n+k-1}{k-1} = \binom{n+k-1}{n}$。

变体1:不允许空盒。先给每个盒子放1个球,剩下n-k个球自由分配。方式数为 $\binom{n-1}{k-1}$。

变体2:每个盒子至少有aᵢ个球。先满足最低要求,剩下的自由分配。

示例:方程 x₁ + x₂ + x₃ = 10 的非负整数解有多少个?

解:这是将10个相同的球放入3个不同盒子的问题。方式数为 $\binom{10+3-1}{3-1} = \binom{12}{2} = 66$。

示例:方程 x₁ + x₂ + x₃ = 10 的正整数解(xᵢ ≥ 1)有多少个?

解:令 yᵢ = xᵢ - 1 ≥ 0,则 y₁ + y₂ + y₃ = 7。方式数为 $\binom{7+3-1}{3-1} = \binom{9}{2} = 36$。

6.4 二项式定理

定理

$$(x + y)^n = \sum_{k=0}^{n} \binom{n}{k} x^{n-k} y^k$$

证明思路

展开 (x+y)ⁿ 时,每项是从n个因子中各选x或y相乘。选k个y和n-k个x的方式数是 C(n,k),对应项为 C(n,k)x^(n-k)y^k。

详细证明:考虑 $(x+y)^n = (x+y)(x+y)\cdots(x+y)$(共n个因子)。展开时,每一项都是从每个因子中选择x或y然后相乘。如果从k个因子中选择y,从n-k个因子中选择x,得到的项为 $x^{n-k}y^k$。选择k个因子的方式数为 $\binom{n}{k}$。对所有可能的k求和即得定理。

特例

  • 令 x = y = 1:$2^n = \sum_{k=0}^{n} \binom{n}{k}$
  • 令 x = 1, y = -1:$0 = \sum_{k=0}^{n} \binom{n}{k}(-1)^k$
  • 令 x = 1, y = 1:$2^n = \sum_{k=0}^{n} \binom{n}{k}$(同上)
  • 令 x = 2, y = 1:$3^n = \sum_{k=0}^{n} \binom{n}{k} 2^{n-k}$

第二个特例的解释:$\sum_{k=0}^{n} (-1)^k \binom{n}{k} = 0$ 表明奇数项组合数之和等于偶数项组合数之和,都等于 $2^{n-1}$。

应用

示例:展开 (x+2)⁴

$$(x+2)^4 = \binom{4}{0}x^4 + \binom{4}{1}x^3(2) + \binom{4}{2}x^2(2^2) + \binom{4}{3}x(2^3) + \binom{4}{4}(2^4)$$

$$= x^4 + 8x^3 + 24x^2 + 32x + 16$$

示例:求 $(1+x)^{10}$ 展开式中 $x^3$ 的系数。

解:系数为 $\binom{10}{3} = \frac{10 \times 9 \times 8}{3 \times 2 \times 1} = 120$。

示例:证明 $\sum_{k=0}^{n} k\binom{n}{k} = n2^{n-1}$。

证明:对 $(1+x)^n = \sum_{k=0}^{n} \binom{n}{k}x^k$ 两边关于x求导:

$n(1+x)^{n-1} = \sum_{k=1}^{n} k\binom{n}{k}x^{k-1}$

令 x = 1:

$n \cdot 2^{n-1} = \sum_{k=1}^{n} k\binom{n}{k} = \sum_{k=0}^{n} k\binom{n}{k}$

多项式定理(二项式定理的推广):

$$(x_1 + x_2 + \cdots + x_m)^n = \sum_{k_1+k_2+\cdots+k_m=n} \frac{n!}{k_1!k_2!\cdots k_m!} x_1^{k_1}x_2^{k_2}\cdots x_m^{k_m}$$

其中求和遍历所有满足 $k_1 + k_2 + \cdots + k_m = n$ 的非负整数组合。

6.5 鸽巢原理

简单形式

如果将 n+1 个物体放入 n 个盒子,则至少有一个盒子包含至少 2 个物体。

证明(反证法):假设每个盒子最多有1个物体,则总物体数最多为n,与有n+1个物体矛盾。∎

推广形式

如果将 N 个物体放入 n 个盒子,则至少有一个盒子包含至少 ⌈N/n⌉ 个物体。

证明(反证法):假设每个盒子最多有 ⌈N/n⌉ - 1 个物体。由于 ⌈N/n⌉ - 1 < N/n,总物体数最多为 n × (N/n - 1) < N,矛盾。∎

示例:367人中至少有2人生日相同(因为只有366天)。

示例:在1到100中任选51个数,必有两个数互质。

证明:将1到100分为50对:(1,2), (3,4), ..., (99,100)。选51个数,由鸽巢原理,必有一对中的两个数都被选中。相邻的两个数互质(因为它们的最大公约数整除它们的差1)。∎

应用示例

问题:证明在任何6个人中,要么有3个人互相认识,要么有3个人互不认识。

证明:考虑某个人A。A与其他5个人的关系要么是"认识"要么是"不认识"。由鸽巢原理,A至少认识⌈5/2⌉=3人,或至少不认识3人。

假设A认识B、C、D三人。如果B、C、D中有任何两人互相认识(如B和C),则A、B、C三人互相认识。如果B、C、D互不认识,则B、C、D三人互不认识。

另一种情况类似。因此命题成立。∎

这是拉姆齐理论的一个简单例子。拉姆齐数 R(3,3) = 6 表示在6个顶点的完全图中,无论如何着色边,必然存在单色三角形。

更多应用:

哈希冲突:如果有n个桶和n+1个键,必然发生冲突。这就是为什么哈希表需要动态扩容。

文件压缩的下界:任何无损压缩算法都不能将所有文件压缩得更小。因为如果所有n位文件都能压缩成少于n位,则由鸽巢原理,两个不同文件会压缩成相同结果,无法无损解压。

子集和问题:从{1,2,...,2n}中任选n+1个数,必有两个数,其中一个整除另一个。

证明:每个数可以写成 $2^k \times m$ 的形式,其中m是奇数。{1,2,...,2n}中有n个奇数(1,3,5,...,2n-1)。选n+1个数,由鸽巢原理,必有两个数有相同的奇数部分m。设这两个数为 $2^{k_1}m$ 和 $2^{k_2}m$。如果 $k_1 < k_2$,则前者整除后者。∎

中国剩余定理的应用:在任意n+1个整数中,必有两个数的差能被n整除。

证明:考虑这n+1个数模n的余数。余数只能是0,1,...,n-1共n种。由鸽巢原理,必有两个数有相同的余数,它们的差能被n整除。∎

6.6 容斥原理

两个集合

解释:直接相加会重复计算交集部分,需要减去一次。

三个集合

模式解释:单个集合的和(加)- 两两交集的和(减)+ 三三交集的和(加)。符号交替变化。

一般形式

对于n个集合:

$$\left|\bigcup_{i=1}^{n} A_i\right| = \sum_{i}|A_i| - \sum_{i<j}|A_i \cap A_j| + \sum_{i<j<k}|A_i \cap A_j \cap A_k| - ... + (-1)^{n+1}\left|\bigcap_{i=1}^{n} A_i\right|$$

模式:单个集合的和 - 两两交集的和 + 三三交集的和 - ...

记忆技巧:符号由交集中集合数量的奇偶性决定。奇数个集合的交集为正,偶数个为负。

证明思路:考虑元素x属于k个集合。x在左边的并集中被计算1次。在右边,x在单个集合的和中被计算k次,在两两交集中被计算 $\binom{k}{2}$ 次,...。总的计算次数为:

$$\binom{k}{1} - \binom{k}{2} + \binom{k}{3} - \cdots + (-1)^{k+1}\binom{k}{k} = 1 - (1-1)^k = 1$$

(当k ≥ 1时)

应用示例

问题:1到100中有多少个数能被2、3或5整除?

解:设A=被2整除的数,B=被3整除的数,C=被5整除的数。

|A| = ⌊100/2⌋ = 50

|B| = ⌊100/3⌋ = 33

|C| = ⌊100/5⌋ = 20

|A∩B| = ⌊100/6⌋ = 16

|A∩C| = ⌊100/10⌋ = 10

|B∩C| = ⌊100/15⌋ = 6

|A∩B∩C| = ⌊100/30⌋ = 3

|A ∪ B ∪ C| = 50 + 33 + 20 - 16 - 10 - 6 + 3 = 74

相关问题:1到100中有多少个数不能被2、3或5整除?

解:100 - 74 = 26个。

更复杂的例子:欧拉函数 φ(n) 表示不超过n且与n互质的正整数个数。

如果 $n = p_1^{a_1} p_2^{a_2} \cdots p_k^{a_k}$,则:

$$\phi(n) = n \left(1 - \frac{1}{p_1}\right)\left(1 - \frac{1}{p_2}\right)\cdots\left(1 - \frac{1}{p_k}\right)$$

推导:设Aᵢ为1到n中能被pᵢ整除的数。我们要求不在任何Aᵢ中的数的个数。

由容斥原理:

$$\phi(n) = n - \sum_i |A_i| + \sum_{i<j} |A_i \cap A_j| - \cdots$$

$$= n - \sum_i \frac{n}{p_i} + \sum_{i<j} \frac{n}{p_i p_j} - \cdots$$

$$= n \left(1 - \sum_i \frac{1}{p_i} + \sum_{i<j} \frac{1}{p_i p_j} - \cdots\right)$$

$$= n \prod_{i=1}^{k} \left(1 - \frac{1}{p_i}\right)$$

示例:φ(60) = φ(2²×3×5) = 60 × (1-1/2) × (1-1/3) × (1-1/5) = 60 × 1/2 × 2/3 × 4/5 = 16。

验证:1到60中与60互质的数为 1,7,11,13,17,19,23,29,31,37,41,43,47,49,53,59,共16个。✓

错排问题(Derangement)

问题:n封信装入n个信封,每封信都装错的装法有多少种?

解:设Dₙ为错排数。设Aᵢ为第i封信装对的排列集合。我们要求不在任何Aᵢ中的排列数。

由容斥原理:

$$D_n = n! - \binom{n}{1}(n-1)! + \binom{n}{2}(n-2)! - \cdots + (-1)^n \binom{n}{n}0!$$

$$= n! \left(1 - \frac{1}{1!} + \frac{1}{2!} - \frac{1}{3!} + \cdots + \frac{(-1)^n}{n!}\right)$$

$$= n! \sum_{k=0}^{n} \frac{(-1)^k}{k!}$$

当n很大时,$D_n \approx \frac{n!}{e}$(因为 $e^{-1} = \sum_{k=0}^{\infty} \frac{(-1)^k}{k!}$)。

前几个错排数:D₀=1, D₁=0, D₂=1, D₃=2, D₄=9, D₅=44, D₆=265。

递推关系:$D_n = (n-1)(D_{n-1} + D_{n-2})$

解释:考虑第1封信。它可以装入第2,3,...,n号信封中的任意一个(n-1种选择)。假设装入第k号信封。有两种情况:

  • 第k封信装入第1号信封:剩下n-2封信的错排数为D_{n-2}
  • 第k封信不装入第1号信封:这等价于n-1封信的错排(将第1号信封视为第k封信的"正确"信封),错排数为D_{n-1}

应用:错排问题在实际中有多种应用,如秘密圣诞老人游戏(每个人给别人送礼,但不能给自己)、匹配问题等。

6.7 特殊计数序列

卡特兰数

卡特兰数 Cₙ = $\frac{1}{n+1}\binom{2n}{n}$ 出现在许多看似不相关的问题中:

  • n对括号的合法匹配方式数
  • n+1个叶子的满二叉树数量
  • 凸n+2边形的三角剖分数
  • 栈的合法操作序列数
  • 从(0,0)到(n,n)不越过对角线的格路路径数
  • 1,2,...,n的栈可排序排列数

前几个卡特兰数:1, 1, 2, 5, 14, 42, 132, 429, 1430, 4862, ...

递推关系:$C_n = \sum_{i=0}^{n-1} C_i C_{n-1-i}$,其中 C₀ = 1。

解释:考虑n对括号的合法序列。第一个左括号必须与某个右括号匹配。假设它与第2k+2个位置的右括号匹配(k从0到n-1)。则中间有k对括号,后面有n-1-k对括号。总方式数为 $\sum_{k=0}^{n-1} C_k C_{n-1-k}$。

格路路径解释:从(0,0)到(n,n),只能向右或向上走,且不越过对角线y=x的路径数为Cₙ。

证明:总路径数为 $\binom{2n}{n}$。越过对角线的路径数等于从(0,0)到(n-1,n+1)的路径数(通过反射原理),即 $\binom{2n}{n-1}$。因此不越过对角线的路径数为 $\binom{2n}{n} - \binom{2n}{n-1} = \frac{1}{n+1}\binom{2n}{n} = C_n$。

示例:4对括号的合法序列有C₄=14种:

()()()(), ()()(()) , ()(()()), ()((())), (())()(), (())(()) , (()()()), (()(())), ((()))(), ((())()), ((()())), (((())))

斯特林数

第一类斯特林数 s(n,k):将n个元素分成k个非空循环排列的方式数。

性质:

  • s(n,1) = (n-1)!(n个元素的循环排列数)
  • s(n,n) = 1
  • s(n,n-1) = $\binom{n}{2}$
  • $\sum_{k=1}^{n} s(n,k) = n!$

递推关系:$s(n,k) = s(n-1,k-1) + (n-1)s(n-1,k)$

第二类斯特林数 S(n,k):将n个不同元素分成k个非空不可区分集合的方式数。

性质:

  • S(n,1) = 1
  • S(n,n) = 1
  • S(n,2) = $2^{n-1} - 1$
  • S(n,n-1) = $\binom{n}{2}$

递推关系:$S(n,k) = kS(n-1,k) + S(n-1,k-1)$

解释:考虑第n个元素。要么它单独形成一个集合(贡献S(n-1,k-1)),要么它加入已有的k个集合中的一个(贡献kS(n-1,k))。

与贝尔数的关系:$B_n = \sum_{k=0}^{n} S(n,k)$

显式公式:$S(n,k) = \frac{1}{k!}\sum_{j=0}^{k} (-1)^{k-j} \binom{k}{j} j^n$

应用:第二类斯特林数出现在将n个不同的球放入k个相同的盒子(不允许空盒)的问题中。

贝尔数

Bₙ 是将n个元素划分为若干非空子集的方式数。

$B_n = \sum_{k=0}^{n} S(n, k)$

前几个贝尔数:1, 1, 2, 5, 15, 52, 203, 877, 4140, ...

递推关系:$B_{n+1} = \sum_{k=0}^{n} \binom{n}{k} B_k$

解释:考虑包含第n+1个元素的集合。假设有k个其他元素与它在同一个集合中(k从0到n)。选择这k个元素的方式数为 $\binom{n}{k}$,剩下n-k个元素的划分方式数为 $B_{n-k}$。重新索引后得到上述递推关系。

贝尔三角形:类似于帕斯卡三角形,可以用于计算贝尔数。

应用:贝尔数出现在集合聚类、等价关系计数等问题中。n个元素上的等价关系数等于Bₙ。

斐波那契数列

虽然斐波那契数列通常与递推关系关联,但它也是一个重要的计数序列。

Fₙ 表示:

  • 1×n的棋盘用1×1和1×2的骨牌铺满的方式数
  • 不含连续1的长度为n的二进制串的数量
  • n阶楼梯每次走1步或2步的走法数

通项公式(比内公式):

$$F_n = \frac{1}{\sqrt{5}}\left[\left(\frac{1+\sqrt{5}}{2}\right)^n - \left(\frac{1-\sqrt{5}}{2}\right)^n\right]$$

6.8 生成函数入门

定义:序列 {aₙ} 的普通生成函数是形式幂级数 $A(x) = \sum_{n=0}^{\infty} a_n x^n$。

基本生成函数:

  • 常数序列 1,1,1,...:$\frac{1}{1-x}$
  • 自然数 1,2,3,...:$\frac{1}{(1-x)^2}$
  • 二项式系数 $\binom{n}{0}, \binom{n}{1}, ...$:$(1+x)^n$
  • 斐波那契数列:$\frac{x}{1-x-x^2}$

运算性质:

  • 加法:对应序列逐项相加
  • 乘法(卷积):$[x^n]A(x)B(x) = \sum_{k=0}^{n} a_k b_{n-k}$
  • 微分:$A'(x)$ 对应序列 $\{na_n\}$

应用示例:求掷两个六面骰子得到和为7的方式数。

单个骰子的生成函数为 $x + x^2 + x^3 + x^4 + x^5 + x^6$。

两个骰子的生成函数为 $(x + x^2 + \cdots + x^6)^2$。

$x^7$ 的系数即为所求。展开后可得系数为6,对应6种方式:(1,6),(2,5),(3,4),(4,3),(5,2),(6,1)。


三、程序员的视角

3.1 在计算机科学中的应用

算法复杂度

  • 冒泡排序的比较次数:C(n,2) = n(n-1)/2
  • 完全图的边数:C(n,2)
  • 二叉搜索树的数量:卡特兰数
  • 快速排序的平均比较次数:约 $2n\ln n$
  • 归并排序的递归深度:$\log_2 n$

密码学

密钥空间大小:

  • 8位ASCII密码:256⁸ ≈ 10¹⁹
  • UUID v4:2¹²² ≈ 10³⁶
  • AES-128:2¹²⁸ ≈ 3.4×10³⁸
  • RSA-2048:约 2²⁰⁴⁸

生日攻击:找到哈希碰撞需要约 $\sqrt{2^n} = 2^{n/2}$ 次尝试。因此SHA-256的安全强度为128位(针对碰撞攻击)。

测试用例生成

组合测试:从n个参数中每次选k个进行组合测试。所需测试用例数为 C(n,k)。

配对测试(k=2):确保每对参数的所有组合都被测试。如果有m个参数,每个参数有v个取值,则至少需要 $v^2$ 个测试用例。

数据库

连接操作的输出大小估计涉及计数。最坏情况下,两个大小为n和m的表的自然连接可能有 n×m 行。

查询优化:优化器需要估计不同执行计划的成本,这涉及基数估计(cardinality estimation)。

机器学习

特征工程:n个二元特征的两两交互产生 $\binom{n}{2}$ 个新特征。这可能导致维度灾难。

模型选择:如果有k个候选模型,使用交叉验证选择最佳模型需要考虑多重比较问题。

3.2 常见陷阱与误区

重复计数

计算"至少一个"的情况时,直接相加会重复计算重叠部分,应使用容斥原理。

例子:1到100中能被2或3整除的数。错误做法:50+33=83。正确做法:50+33-16=67。

顺序混淆

排列考虑顺序,组合不考虑。选委员会成员是组合,选主席和副主席是排列。

判断方法:问自己"交换两个被选中的元素会产生不同的结果吗?"如果是,用排列;否则用组合。

边界情况

C(n,0) = 1(空集是一种选择),不是0。

0! = 1(空乘积定义为1)。

常见错误:忘记考虑空集或全集的情况。

溢出问题

阶乘和组合数增长极快。C(100,50) ≈ 10²⁹,超出64位整数范围。

解决方案:

  • 使用大整数库
  • 在计算过程中进行约分
  • 使用对数避免溢出

"至少"与"恰好"的混淆

"至少k个"通常比"恰好k个"更容易通过补集计算。

例子:抛10次硬币至少6次正面的概率 = 1 - (0到5次正面的概率之和)。

3.3 与其他层级的联系

  • 硬件层:地址空间大小 = 2^(位数);指令编码的组合数
  • 操作系统层:进程调度排列数;内存页的组合分配
  • 网络层:IP地址数量;路由表条目数
  • 应用层:用户权限组合;配置选项的组合数

四、代码实战

6.1 计数工具实现

python
"""
计数工具:排列、组合、二项式系数
"""

import math
from functools import lru_cache


def factorial(n):
    """计算n的阶乘"""
    if n < 0:
        raise ValueError("阶乘未定义于负数")
    return math.factorial(n)


def permutation(n, r):
    """
    计算排列数 P(n, r)
    
    P(n, r) = n! / (n-r)!
    """
    if r > n or r < 0:
        return 0
    return factorial(n) // factorial(n - r)


def combination(n, r):
    """
    计算组合数 C(n, r) = binom(n, r)
    
    C(n, r) = n! / (r! * (n-r)!)
    """
    if r > n or r < 0:
        return 0
    return math.comb(n, r)


@lru_cache(maxsize=None)
def catalan(n):
    """
    计算第n个卡特兰数
    
    C_n = binom(2n, n) / (n+1)
    """
    if n <= 1:
        return 1
    return combination(2*n, n) // (n + 1)


def stirling_second_kind(n, k):
    """
    计算第二类斯特林数 S(n, k)
    
    将n个不同元素分成k个非空不可区分集合的方式数
    """
    if n == k == 0:
        return 1
    if n == 0 or k == 0:
        return 0
    if k > n:
        return 0
    
    # 递推关系: S(n,k) = k*S(n-1,k) + S(n-1,k-1)
    return k * stirling_second_kind(n-1, k) + stirling_second_kind(n-1, k-1)


def bell_number(n):
    """
    计算第n个贝尔数
    
    B_n = sum of S(n, k) for k = 0 to n
    """
    return sum(stirling_second_kind(n, k) for k in range(n + 1))


def derangement(n):
    """
    计算错排数 D_n
    
    D_n = n! * sum((-1)^k / k!, k=0..n)
    """
    if n == 0:
        return 1
    if n == 1:
        return 0
    
    # 使用递推关系: D_n = (n-1)(D_{n-1} + D_{n-2})
    d = [0] * (n + 1)
    d[0] = 1
    d[1] = 0
    
    for i in range(2, n + 1):
        d[i] = (i - 1) * (d[i-1] + d[i-2])
    
    return d[n]


def stars_and_bars(n, k, allow_empty=True):
    """
    星与棒方法:将n个相同的球放入k个不同的盒子
    
    参数:
        n: 球的数量
        k: 盒子的数量
        allow_empty: 是否允许空盒
    
    返回: 方式数
    """
    if allow_empty:
        # C(n+k-1, k-1)
        return combination(n + k - 1, k - 1)
    else:
        # 每个盒子至少1个球: C(n-1, k-1)
        if n < k:
            return 0
        return combination(n - 1, k - 1)


def demonstrate_counting():
    """演示计数函数"""
    
    print("=== 排列和组合 ===")
    
    n, r = 5, 3
    print(f"P({n}, {r}) = {permutation(n, r)}")
    print(f"C({n}, {r}) = {combination(n, r)}")
    print(f"验证: P({n},{r}) = C({n},{r}) × {r}! = {combination(n,r)} × {factorial(r)} = {combination(n,r) * factorial(r)}")
    
    print("\n=== 卡特兰数 ===")
    for n in range(10):
        print(f"  C_{n} = {catalan(n)}")
    
    print("\n=== 第二类斯特林数 ===")
    print("S(n, k) 表格:")
    print(f"{'n\\k':>4}", end="")
    for k in range(8):
        print(f"{k:>6}", end="")
    print()
    
    for n in range(8):
        print(f"{n:>4}", end="")
        for k in range(8):
            print(f"{stirling_second_kind(n, k):>6}", end="")
        print()
    
    print("\n=== 贝尔数 ===")
    for n in range(10):
        print(f"  B_{n} = {bell_number(n)}")
    
    print("\n=== 错排数 ===")
    for n in range(10):
        print(f"  D_{n} = {derangement(n)}")
    
    print("\n=== 星与棒 ===")
    print("将5个相同的球放入3个不同的盒子:")
    print(f"  允许空盒: {stars_and_bars(5, 3, True)}种")
    print(f"  不允许空盒: {stars_and_bars(5, 3, False)}种")


if __name__ == "__main__":
    demonstrate_counting()

6.2 鸽巢原理验证

python
"""
鸽巢原理应用和验证
"""

import random


def birthday_paradox_simulation(num_people=23, num_trials=10000):
    """
    模拟生日悖论
    
    返回: 至少两人生日相同的概率估计
    """
    matches = 0
    
    for _ in range(num_trials):
        birthdays = [random.randint(1, 365) for _ in range(num_people)]
        if len(birthdays) != len(set(birthdays)):  # 有重复
            matches += 1
    
    return matches / num_trials


def pigeonhole_principle_demo():
    """演示鸽巢原理的应用"""
    
    print("=== 生日悖论 ===")
    print("理论值和模拟值比较:")
    print(f"{'人数':>6} {'理论概率':>12} {'模拟概率':>12}")
    print("-" * 35)
    
    for n in [10, 20, 23, 30, 50]:
        # 理论概率: 1 - 365!/(365-n)!/365^n
        prob_no_match = 1.0
        for i in range(n):
            prob_no_match *= (365 - i) / 365
        theoretical_prob = 1 - prob_no_match
        
        simulated_prob = birthday_paradox_simulation(n, 5000)
        
        print(f"{n:>6} {theoretical_prob:>12.4f} {simulated_prob:>12.4f}")
    
    print("\n=== 鸽巢原理简单应用 ===")
    
    # 13个人中至少有2人生日在同一月份
    people = 13
    months = 12
    print(f"{people}个人,{months}个月")
    print(f"由鸽巢原理,至少有 ⌈{people}/{months}⌉ = {(people + months - 1) // months} 人生日在同一月份")
    
    # 验证
    for _ in range(3):
        birth_months = [random.randint(1, 12) for _ in range(people)]
        month_counts = {}
        for m in birth_months:
            month_counts[m] = month_counts.get(m, 0) + 1
        max_count = max(month_counts.values())
        print(f"  随机模拟: 最大同月人数 = {max_count}")
    
    print("\n=== 子集和整除问题 ===")
    print("从{1,2,...,20}中选11个数,必有两个数,其中一个整除另一个")
    
    # 验证
    for trial in range(5):
        subset = random.sample(range(1, 21), 11)
        subset.sort()
        
        found = False
        for i in range(len(subset)):
            for j in range(i+1, len(subset)):
                if subset[j] % subset[i] == 0:
                    print(f"  找到: {subset[i]} 整除 {subset[j]}")
                    found = True
                    break
            if found:
                break
        
        if not found:
            print(f"  未找到(理论上不可能): {subset}")


if __name__ == "__main__":
    pigeonhole_principle_demo()

6.3 容斥原理实现

python
"""
容斥原理实现
"""

from itertools import combinations


def inclusion_exclusion(sets):
    """
    使用容斥原理计算多个集合并集的大小
    
    参数:
        sets: 集合列表
    
    返回: 并集的大小
    """
    n = len(sets)
    result = 0
    
    # 遍历所有非空子集
    for size in range(1, n + 1):
        for subset_indices in combinations(range(n), size):
            # 计算这些集合的交集
            intersection = sets[subset_indices[0]]
            for idx in subset_indices[1:]:
                intersection = intersection & sets[idx]
            
            # 根据子集大小加减
            if size % 2 == 1:
                result += len(intersection)
            else:
                result -= len(intersection)
    
    return result


def count_divisible_numbers(limit, divisors):
    """
    计算1到limit中能被divisors中至少一个数整除的数的个数
    
    使用容斥原理
    """
    # 构建集合:能被d整除的数
    sets = []
    for d in divisors:
        divisible_set = set(range(d, limit + 1, d))
        sets.append(divisible_set)
    
    return inclusion_exclusion(sets)


def euler_totient(n):
    """
    计算欧拉函数 φ(n)
    
    φ(n) = 不超过n且与n互质的正整数个数
    """
    if n <= 0:
        return 0
    
    result = n
    p = 2
    temp = n
    
    while p * p <= temp:
        if temp % p == 0:
            while temp % p == 0:
                temp //= p
            result -= result // p
        p += 1
    
    if temp > 1:
        result -= result // temp
    
    return result


def demonstrate_inclusion_exclusion():
    """演示容斥原理"""
    
    print("=== 容斥原理示例 ===")
    
    # 示例1: 手动构造集合
    A = {1, 2, 3, 4, 5}
    B = {4, 5, 6, 7, 8}
    C = {6, 7, 8, 9, 10}
    
    print(f"A = {A}")
    print(f"B = {B}")
    print(f"C = {C}")
    
    union_size = inclusion_exclusion([A, B, C])
    actual_union = A | B | C
    
    print(f"|A ∪ B ∪ C| (容斥) = {union_size}")
    print(f"|A ∪ B ∪ C| (实际) = {len(actual_union)}")
    print(f"验证: {union_size == len(actual_union)}")
    
    print("\n=== 整除计数 ===")
    
    limit = 100
    divisors = [2, 3, 5]
    
    count = count_divisible_numbers(limit, divisors)
    print(f"1到{limit}中能被 {divisors} 中至少一个整除的数有 {count} 个")
    
    # 验证
    actual = sum(1 for i in range(1, limit+1) if any(i % d == 0 for d in divisors))
    print(f"验证: {actual}")
    
    print("\n=== 欧拉函数 ===")
    for n in [1, 2, 3, 4, 5, 6, 10, 12, 60, 100]:
        print(f"  φ({n}) = {euler_totient(n)}")
    
    print("\n=== 错排概率 ===")
    import math
    for n in [1, 2, 3, 4, 5, 10, 20]:
        from chapter_6_tools import derangement
        d_n = derangement(n)
        prob = d_n / math.factorial(n)
        print(f"  n={n}: D_n={d_n}, P={prob:.6f} (1/e ≈ {1/math.e:.6f})")


if __name__ == "__main__":
    demonstrate_inclusion_exclusion()

五、深入思考

5.1 历史背景

计数问题有着悠久的历史。帕斯卡三角形在中国称为杨辉三角,早在11世纪就被发现。雅各布·伯努利在《猜度术》中系统研究了组合数学。亚伯拉罕·棣莫弗和莱昂哈德·欧拉发展了生成函数方法。

乔治·波利亚的计数理论将群论应用于组合计数,解决了化学异构体计数等问题。他的《如何解题》成为数学教育的经典。

保罗·厄尔多斯对组合数学做出了巨大贡献,特别是极值组合学和概率方法。他与众多合作者发表了大量论文,形成了独特的"厄尔多斯风格"。

5.2 前沿方向

  • 代数组合学:使用代数方法解决计数问题,如表示论、对称函数
  • 随机组合结构:研究大型随机结构的性质,如随机图、随机排列
  • 量子计数算法:利用量子计算机加速计数,如近似计数#P问题
  • 近似计数:对于#P完全问题,寻找多项式时间的近似算法,如马尔可夫链蒙特卡洛方法
  • 枚举复杂性:研究生成所有解的效率,如常数延迟枚举

5.3 延伸阅读

  • 《Concrete Mathematics》by Graham, Knuth, Patashnik
  • 《Enumerative Combinatorics》by Richard P. Stanley
  • 《The Art of Computer Programming, Vol. 4A》by Donald E. Knuth
  • 《Combinatorial Problems and Exercises》by László Lovász
  • 《Generatingfunctionology》by Herbert S. Wilf

六、实验笔记

实验1:计数问题求解器

目标:实现一个能够解决常见计数问题的程序。

任务:

支持排列、组合、带重复的排列/组合计算

实现二项式展开

计算卡特兰数、贝尔数等特殊序列

提供问题模板(如"从n个中选r个")

可视化帕斯卡三角形

实现错排数计算

支持星与棒问题

提交物:源代码、用户界面、测试用例。

扩展挑战:

  • 实现生成函数操作
  • 自动识别问题类型并选择合适的公式
  • 提供详细的解题步骤说明

实验2:蒙特卡洛计数

目标:使用随机采样估计难以精确计算的计数问题。

任务:

估计大规模组合问题的解数量

验证容斥原理的计算结果

模拟鸽巢原理的应用场景

分析估计精度与采样次数的关系

实现重要性采样提高估计效率

提交物:源代码、实验报告、精度分析。

扩展挑战:

  • 比较不同采样方法的效率
  • 实现置信区间计算
  • 应用到实际的组合优化问题

实验3:组合枚举

目标:实现高效生成所有组合对象的算法。

任务:

生成所有排列(Heap算法或Steinhaus-Johnson-Trotter算法)

生成所有组合(格雷码顺序)

生成所有整数划分

生成所有二叉树

分析生成算法的时间复杂度

提交物:源代码、性能测试、可视化输出。

扩展挑战:

  • 实现常数延迟枚举(每两个输出之间的时间为常数)
  • 并行化枚举算法
  • 支持按字典序生成

七、本章小结

本章介绍了基本的计数技术。加法原理和乘法原理是计数的基础,分别处理"或"和"且"的情况。正确识别问题是使用加法还是乘法是成功的关键。

排列考虑顺序,组合不考虑。二项式系数在展开式和概率计算中有广泛应用。理解对称性和帕斯卡恒等式有助于简化计算。

鸽巢原理虽然简单,却能证明令人惊讶的存在性结论。它是组合数学中最直观却最有力的工具之一。容斥原理通过系统地加减交集来避免重复计数,在处理"至少一个"的问题时特别有用。

特殊计数序列如卡特兰数、斯特林数、贝尔数出现在各种意想不到的场景中,展示了组合数学的统一美。卡特兰数尤其有趣,它在括号匹配、二叉树计数、格路路径等多个问题中出现。

错排问题是容斥原理的经典应用,其渐近行为与e密切相关。星与棒方法为解决分配问题提供了统一的框架。

计数需要细心和系统性思维,避免重复和遗漏是成功的关键。在实际应用中,常常需要结合多种技术,如先使用补集思想,再应用容斥原理。


八、习题

习题1(基础):计算:

(a) P(10, 4)

(b) C(10, 4)

(c) C(20, 18)

(d) 6!

(e) C(100, 98)

习题2(基础):一个班级有30名学生。

(a) 选班长有多少种选法?

(b) 选班长和副班长有多少种选法?

(c) 选3人组成委员会有多少种选法?

(d) 选3人组成委员会并指定主席有多少种选法?

习题3(中等):展开 (2x - 1)⁵。

习题4(中等):证明 $\sum_{k=0}^{n} \binom{n}{k}^2 = \binom{2n}{n}$。

习题5(中等):1到200中有多少个数能被3或7整除但不能被21整除?

习题6(较难):证明在任何n+1个不超过2n的正整数中,必有两个数互质。

习题7(较难):计算n对括号的合法匹配方式数,验证它是卡特兰数。

习题8(挑战):研究并证明范德蒙德恒等式。

习题9(挑战):有多少种方式将n个相同的球放入k个不同的盒子,允许空盒?推导一般公式。

习题10(开放):选择一个实际应用场景(如彩票、密码、游戏),分析其中的计数问题并撰写报告。包括:

  • 问题描述
  • 使用的计数技术
  • 计算过程和结果
  • 实际意义和应用
6/13计数
0%