跳到主要内容

组合数学

组合数学(Combinatorics)是研究离散结构的计数、存在性和优化问题的数学分支。它为算法复杂度分析、概率计算、密码学等领域提供了基础工具。从简单的排列组合到复杂的生成函数,组合数学贯穿于计算机科学的各个方面。

基本计数原理

加法原理

加法原理处理的是"或"的选择情况。如果完成一件事有 nn 类方法,第 ii 类方法有 aia_i 种具体方式,且各类方法之间互不重叠,那么完成这件事共有 a1+a2++ana_1 + a_2 + \cdots + a_n 种方法。

形式化表述:设 A1,A2,,AnA_1, A_2, \ldots, A_n 是互不相交的有限集合,则:

A1A2An=A1+A2++An|A_1 \cup A_2 \cup \cdots \cup A_n| = |A_1| + |A_2| + \cdots + |A_n|

例子:从北京到上海可以坐飞机(3 个航班)、高铁(5 个车次)或汽车(2 个班次),共有 3+5+2=103 + 5 + 2 = 10 种选择。

# 加法原理示例
flights = 3
trains = 5
buses = 2
total_choices = flights + trains + buses # 10

乘法原理

乘法原理处理的是"且"的选择情况。如果完成一件事需要 nn 个步骤,第 ii 步有 aia_i 种选择方式,且各步骤相互独立,那么完成这件事共有 a1×a2××ana_1 \times a_2 \times \cdots \times a_n 种方法。

形式化表述:设 A1,A2,,AnA_1, A_2, \ldots, A_n 是有限集合,则:

A1×A2××An=A1×A2××An|A_1 \times A_2 \times \cdots \times A_n| = |A_1| \times |A_2| \times \cdots \times |A_n|

例子:选择一套衣服,上衣有 4 件,裤子有 3 条,鞋子有 2 双,共有 4×3×2=244 \times 3 \times 2 = 24 种搭配。

# 乘法原理示例
tops = 4
pants = 3
shoes = 2
total_outfits = tops * pants * shoes # 24

正确选择计数原理

判断使用加法原理还是乘法原理,关键在于分清问题是"选择之一"还是"依次选择":

  • 加法原理:选择一种方式完成任务,各方式是"平行"关系
  • 乘法原理:依次完成多个步骤,各步骤是"串行"关系

综合例子:从 5 本数学书和 4 本物理书中选择 2 本不同学科的书,有多少种选法?

分析:先选数学再选物理,或者先选物理再选数学。这是一个"分步选择"问题,但两步的顺序不同会导致不同的选择(数学书 A + 物理书 B 与 物理书 B + 数学书 A 是同一种选法)。正确分析是:选一本数学(5 种)和一本物理(4 种),共 5×4=205 \times 4 = 20 种。

排列

排列的定义

nn 个不同元素中取出 rr 个元素,按照一定的顺序排成一列,称为从 nn 个元素中取 rr 个元素的排列(Permutation)。

排列强调顺序:(a,b)(a, b)(b,a)(b, a) 是不同的排列。

排列数的计算

nn 个不同元素中取 rr 个元素的排列数记作 P(n,r)P(n, r)AnrA_n^r

P(n,r)=n!(nr)!=n×(n1)××(nr+1)P(n, r) = \frac{n!}{(n-r)!} = n \times (n-1) \times \cdots \times (n-r+1)

推导:第一个位置有 nn 种选择,第二个位置有 n1n-1 种选择,依此类推,第 rr 个位置有 nr+1n-r+1 种选择。根据乘法原理:

P(n,r)=n×(n1)××(nr+1)P(n, r) = n \times (n-1) \times \cdots \times (n-r+1)

特殊情况:当 r=nr = n 时,称为全排列,P(n,n)=n!P(n, n) = n!

from math import factorial

def permutation(n, r):
"""计算 P(n, r)"""
if r > n:
return 0
return factorial(n) // factorial(n - r)

# 从 5 个人中选 3 个人排成一列
print(permutation(5, 3)) # 60

排列的例子

问题:用数字 1, 2, 3, 4, 5 可以组成多少个没有重复数字的三位数?

解答:这是从 5 个元素中取 3 个的排列问题:P(5,3)=5×4×3=60P(5, 3) = 5 \times 4 \times 3 = 60 种。

圆排列

nn 个不同元素排成一个圆圈,由于旋转不产生新的排列,圆排列数为:

n!n=(n1)!\frac{n!}{n} = (n-1)!

理解nn 个元素的直线排列有 n!n! 种,但圆圈上旋转 nn 次回到原位,所以除以 nn

例子:4 个人围圆桌而坐,有 (41)!=6(4-1)! = 6 种坐法。

组合

组合的定义

nn 个不同元素中取出 rr 个元素组成一个集合,不考虑顺序,称为从 nn 个元素中取 rr 个元素的组合(Combination)。

组合不强调顺序:{a,b}\{a, b\}{b,a}\{b, a\} 是相同的组合。

组合数的计算

nn 个不同元素中取 rr 个元素的组合数记作 C(n,r)C(n, r)(nr)\binom{n}{r}

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

推导:先计算排列数 P(n,r)P(n, r),再除以 r!r!(因为 rr 个元素的 r!r! 种排列对应同一个组合)。

from math import factorial

def combination(n, r):
"""计算 C(n, r)"""
if r > n or r < 0:
return 0
return factorial(n) // (factorial(r) * factorial(n - r))

# 从 5 个人中选 3 个人组成小组
print(combination(5, 3)) # 10

组合数的性质

对称性

(nr)=(nnr)\binom{n}{r} = \binom{n}{n-r}

nn 个中选 rr 个,等价于从 nn 个中选 nrn-r 个不选。

边界值

(n0)=(nn)=1,(n1)=n\binom{n}{0} = \binom{n}{n} = 1, \quad \binom{n}{1} = n

递推关系(杨辉恒等式)

(nr)=(n1r1)+(n1r)\binom{n}{r} = \binom{n-1}{r-1} + \binom{n-1}{r}

这是杨辉三角(帕斯卡三角)的构造原理。

def pascal_triangle(n):
"""生成杨辉三角的前 n 行"""
triangle = []
for i in range(n):
row = [1] * (i + 1)
for j in range(1, i):
row[j] = triangle[i-1][j-1] + triangle[i-1][j]
triangle.append(row)
return triangle

# 打印前 6 行
for row in pascal_triangle(6):
print(row)
# [1]
# [1, 1]
# [1, 2, 1]
# [1, 3, 3, 1]
# [1, 4, 6, 4, 1]
# [1, 5, 10, 10, 5, 1]

组合的例子

问题:一个班级有 30 名学生,要选出 3 名代表,有多少种选法?

解答C(30,3)=30×29×283×2×1=4060C(30, 3) = \frac{30 \times 29 \times 28}{3 \times 2 \times 1} = 4060 种。

问题:一副扑克牌(52 张)中,任意抽取 5 张牌,有多少种可能?

解答C(52,5)=52!5!×47!=2,598,960C(52, 5) = \frac{52!}{5! \times 47!} = 2,598,960 种。

二项式定理

二项式定理的内容

二项式定理描述了二项式 (a+b)n(a + b)^n 的展开式:

(a+b)n=k=0n(nk)ankbk=(n0)an+(n1)an1b++(nn)bn(a + b)^n = \sum_{k=0}^{n} \binom{n}{k} a^{n-k} b^k = \binom{n}{0}a^n + \binom{n}{1}a^{n-1}b + \cdots + \binom{n}{n}b^n

展开式中的系数 (nk)\binom{n}{k} 正是组合数,也称为二项式系数。

例子

(a+b)3=a3+3a2b+3ab2+b3(a + b)^3 = a^3 + 3a^2b + 3ab^2 + b^3

(a+b)4=a4+4a3b+6a2b2+4ab3+b4(a + b)^4 = a^4 + 4a^3b + 6a^2b^2 + 4ab^3 + b^4

二项式系数的组合意义

(nk)\binom{n}{k} 表示从 nn 个元素中选 kk 个的组合数。在二项式展开中,它表示:

(a+b)n(a + b)^n 的展开式中 ankbka^{n-k}b^k 项的系数,等于从 nn 个括号中选择 kk 个取 bb(其余取 aa)的方法数。

特殊情况

a=b=1a = b = 1,得到:

2n=k=0n(nk)=(n0)+(n1)++(nn)2^n = \sum_{k=0}^{n} \binom{n}{k} = \binom{n}{0} + \binom{n}{1} + \cdots + \binom{n}{n}

这给出了一个组合解释:nn 元素集合的所有子集数(幂集的大小)等于各层组合数之和。

a=1,b=1a = 1, b = -1,得到:

0=k=0n(1)k(nk)=(n0)(n1)+(n2)0 = \sum_{k=0}^{n} (-1)^k \binom{n}{k} = \binom{n}{0} - \binom{n}{1} + \binom{n}{2} - \cdots

这表明偶数位置的二项式系数之和等于奇数位置的二项式系数之和。

鸽巢原理

基本形式

鸽巢原理(Pigeonhole Principle)是一个简单却强大的存在性证明工具。

基本形式:如果 n+1n+1 只鸽子飞入 nn 个鸽巢,至少有一个鸽巢中有两只或更多鸽子。

一般形式:如果 nn 个物品放入 mm 个盒子,则至少有一个盒子中有 n/m\lceil n/m \rceil 个物品。

应用示例

问题:证明在任何 367 人中,至少有两人生日相同(假设一年 365 天)。

证明:将 367 人(鸽子)按生日放入 365 个"鸽巢"。由鸽巢原理,至少有一个生日对应 2 人以上。

问题:证明从 1 到 100 的整数中任取 51 个数,其中必有两个数互为倍数。

证明:每个正整数可以唯一表示为 2km2^k \cdot m,其中 mm 是奇数。1 到 100 中有 50 个不同的奇数 mm。取 51 个数,由鸽巢原理,必有两个数对应相同的奇数 mm,设为 2k1m2^{k_1} \cdot m2k2m2^{k_2} \cdot m。若 k1<k2k_1 < k_2,则前者整除后者。

问题:证明在任意 6 个人中,必有 3 人互相认识或 3 人互不认识。

证明:任选一人 A,其余 5 人分为"认识 A 的"和"不认识 A 的"两组。由鸽巢原理,必有一组至少 3 人。假设"认识 A 的"组有 3 人 B、C、D。若 B、C、D 中有两人互相认识,加上 A 则有 3 人互相认识;若 B、C、D 两两不认识,则他们就是 3 人互不认识。

鸽巢原理的威力

鸽巢原理特别适合证明存在性问题。它不告诉你具体哪个鸽巢"拥挤",但保证一定存在这样的鸽巢。

# 利用鸽巢原理检测重复元素
def find_duplicate(nums):
"""在 n+1 个范围 [1,n] 的数中找出重复的数"""
# 鸽巢原理保证必有重复
seen = set()
for num in nums:
if num in seen:
return num
seen.add(num)
return None # 理论上不会执行到这里

容斥原理

两个集合的容斥原理

对于两个集合 AABB

AB=A+BAB|A \cup B| = |A| + |B| - |A \cap B|

理解:直接相加会把交集中的元素数两次,需要减去一次。

三个集合的容斥原理

对于三个集合 AABBCC

ABC=A+B+CABACBC+ABC|A \cup B \cup C| = |A| + |B| + |C| - |A \cap B| - |A \cap C| - |B \cap C| + |A \cap B \cap C|

一般形式

对于 nn 个集合 A1,A2,,AnA_1, A_2, \ldots, A_n

i=1nAi=iAii<jAiAj+i<j<kAiAjAk+(1)n+1A1A2An\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| - \cdots + (-1)^{n+1}|A_1 \cap A_2 \cap \cdots \cap A_n|

应用示例

问题:在 1 到 100 的整数中,有多少个数不能被 2、3、5 任何一个整除?

:设 AA 为被 2 整除的数,BB 为被 3 整除的数,CC 为被 5 整除的数。

A=50|A| = 50B=33|B| = 33C=20|C| = 20

AB|A \cap B| 是被 6 整除的数:AB=16|A \cap B| = 16

AC|A \cap C| 是被 10 整除的数:AC=10|A \cap C| = 10

BC|B \cap C| 是被 15 整除的数:BC=6|B \cap C| = 6

ABC|A \cap B \cap C| 是被 30 整除的数:ABC=3|A \cap B \cap C| = 3

能被 2 或 3 或 5 整除的数:

ABC=50+33+2016106+3=74|A \cup B \cup C| = 50 + 33 + 20 - 16 - 10 - 6 + 3 = 74

不能被任何一个整除的数:10074=26100 - 74 = 26

# 验证
count = sum(1 for i in range(1, 101) if i % 2 != 0 and i % 3 != 0 and i % 5 != 0)
print(count) # 26

递推关系

递推关系的概念

递推关系是用前面的项来定义当前项的等式。它在算法分析中极为重要。

斐波那契数列是最经典的例子:

Fn=Fn1+Fn2,F0=0,F1=1F_n = F_{n-1} + F_{n-2}, \quad F_0 = 0, F_1 = 1

汉诺塔问题

nn 个圆盘从柱 A 移到柱 C,每次只能移动一个圆盘,且大盘不能放在小盘上。

设移动 nn 个盘子的次数为 TnT_n。移动策略:

  1. 将上面 n1n-1 个盘子从 A 移到 B:Tn1T_{n-1}
  2. 将最大的盘子从 A 移到 C:1 次
  3. n1n-1 个盘子从 B 移到 C:Tn1T_{n-1}

递推关系:Tn=2Tn1+1T_n = 2T_{n-1} + 1T1=1T_1 = 1

解:Tn=2n1T_n = 2^n - 1

def hanoi_moves(n):
"""计算 n 个盘子的最少移动次数"""
return 2**n - 1

print(hanoi_moves(3)) # 7
print(hanoi_moves(10)) # 1023

递推关系的求解

线性齐次递推关系:形如 an=c1an1+c2an2++ckanka_n = c_1 a_{n-1} + c_2 a_{n-2} + \cdots + c_k a_{n-k} 的递推关系。

求解方法:特征方程法。设 an=rna_n = r^n,代入得到特征方程,求特征根,写出通解。

斐波那契数列的解

特征方程:r2=r+1r^2 = r + 1,即 r2r1=0r^2 - r - 1 = 0

特征根:r1=1+52r_1 = \frac{1+\sqrt{5}}{2}r2=152r_2 = \frac{1-\sqrt{5}}{2}

通解:Fn=Ar1n+Br2nF_n = A \cdot r_1^n + B \cdot r_2^n

代入初始条件 F0=0F_0 = 0F1=1F_1 = 1,得:

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

这就是著名的比内公式。

生成函数简介

生成函数的定义

对于数列 {an}\{a_n\},其生成函数(Generating Function)定义为形式幂级数:

G(x)=n=0anxn=a0+a1x+a2x2+G(x) = \sum_{n=0}^{\infty} a_n x^n = a_0 + a_1 x + a_2 x^2 + \cdots

生成函数将离散的数列转化为连续的函数,从而可以利用函数运算来解决计数问题。

常见数列的生成函数

常数列 {1,1,1,}\{1, 1, 1, \ldots\}

n=0xn=11x\sum_{n=0}^{\infty} x^n = \frac{1}{1-x}

等比数列 {1,r,r2,}\{1, r, r^2, \ldots\}

n=0rnxn=11rx\sum_{n=0}^{\infty} r^n x^n = \frac{1}{1-rx}

二项式系数 (n0),(n1),,(nn)\binom{n}{0}, \binom{n}{1}, \ldots, \binom{n}{n}

(1+x)n=k=0n(nk)xk(1+x)^n = \sum_{k=0}^{n} \binom{n}{k} x^k

斐波那契数列

n=0Fnxn=x1xx2\sum_{n=0}^{\infty} F_n x^n = \frac{x}{1-x-x^2}

生成函数的应用

问题:用 1 元、2 元、5 元硬币支付 nn 元,有多少种方法?

:设方法数为 ana_n,生成函数为:

G(x)=(1+x+x2+)(1+x2+x4+)(1+x5+x10+)G(x) = (1 + x + x^2 + \cdots)(1 + x^2 + x^4 + \cdots)(1 + x^5 + x^{10} + \cdots)

=11x11x211x5= \frac{1}{1-x} \cdot \frac{1}{1-x^2} \cdot \frac{1}{1-x^5}

展开后 xnx^n 的系数即为 ana_n

def coin_ways(n):
"""计算用 1,2,5 元硬币支付 n 元的方法数"""
ways = [0] * (n + 1)
ways[0] = 1
for coin in [1, 2, 5]:
for i in range(coin, n + 1):
ways[i] += ways[i - coin]
return ways[n]

print(coin_ways(10)) # 10

小结

本章介绍了组合数学的基础知识:

  1. 基本计数原理:加法原理("或"的选择)、乘法原理("且"的选择)
  2. 排列与组合:排列强调顺序,组合不强调顺序
  3. 二项式定理(a+b)n(a+b)^n 的展开式,系数为组合数
  4. 鸽巢原理:证明存在性的有力工具
  5. 容斥原理:计算并集大小的系统方法
  6. 递推关系:用前面的项定义当前项
  7. 生成函数:将离散数列转化为连续函数

组合数学是算法分析的基石。理解排列组合是分析排序算法复杂度的前提;鸽巢原理和容斥原理在概率计算中频繁出现;递推关系则是动态规划的理论基础。

练习

  1. 计算 P(8,3)P(8, 3)C(10,4)C(10, 4)

  2. 一个委员会有 12 名成员,要选出 1 名主席、1 名副主席和 1 名秘书,要求 3 人不同,有多少种选法?

  3. 用二项式定理展开 (x+2y)4(x + 2y)^4

  4. 证明:在任意 n+1n+1 个正整数中,必有两个数之差能被 nn 整除。

  5. an=5an16an2a_n = 5a_{n-1} - 6a_{n-2}a0=1a_0 = 1a1=4a_1 = 4。求 ana_n 的通项公式。