跳转至

# 计数

计数是计算概率的前提——在分配可能性之前，你必须先知道有多少种结果。本文涵盖乘法与加法规则、阶乘、排列、组合、二项式系数，以及支撑机器学习中采样、哈希和概率分析的基本组合工具。

- 在计算概率之前，我们需要先数清结果的数量。如果你想知道在扑克中拿到一手赢牌的概率，你必须先知道一共有多少种可能的牌型，以及其中有多少种是赢牌。计数正是让概率精确化的基础工具。

- 最简单的计数原则是乘法规则。如果一个选择有 \(m\) 种选项，另一个独立的选择有 \(n\) 种选项，那么组合起来的总结果数为 \(m \times n\)。

- 想象早上穿衣服的场景。你有 3 件衬衫和 4 条裤子。每件衬衫都能与每条裤子搭配，共有 \(3 \times 4 = 12\) 种穿搭。

- 乘法规则可以推广到任意数量的选择。如果你还有 2 双鞋，那么总穿搭数就变成 \(3 \times 4 \times 2 = 24\)。每个新的独立选择都会乘到总计数中。

- 加法规则处理"或"的场景。如果事件 A 有 \(m\) 种发生方式，事件 B 有 \(n\) 种发生方式，且它们不能同时发生（互斥），那么总的方式数为 \(m + n\)。

- 假设你要从城市 X 前往城市 Y：开车有 3 条路线，坐火车有 2 条路线。你无法同时选择两者，因此总选项数为 \(3 + 2 = 5\)。

- 当事件有重叠时，需要减去被重复计数的结果。如果 \(A\) 和 \(B\) 可以同时发生，计数为 \(|A \cup B| = |A| + |B| - |A \cap B|\)。这就是容斥原理，它将在我们讨论概率加法规则时再次出现。

- 非负整数 \(n\) 的阶乘是从 1 到 \(n\) 的所有正整数的乘积：

\[n! = n \times (n-1) \times (n-2) \times \cdots \times 2 \times 1\]

- 可以将阶乘理解为：将 \(n\) 个不同的物体排成一列有多少种方式？三本书在书架上有 \(3! = 3 \times 2 \times 1 = 6\) 种排列方式。按约定，\(0! = 1\)。

- 阶乘的增长速度极快。\(10! = 3{,}628{,}800\)，而 \(20!\) 已经超过 \(2.4 \times 10^{18}\)。这种爆炸式增长正是暴力搜索在组合问题中变得不切实际的原因。

- 排列是对物体的有序安排。当你从 \(n\) 个不同的物体中选取 \(r\) 个且顺序重要时，排列数为：

\[P(n, r) = \frac{n!}{(n - r)!}\]

- 想象从一个 10 人的俱乐部中选出会长、副会长和财务主管。第一个职位有 10 个候选人，第二个有 9 个，第三个有 8 个。因此 \(P(10, 3) = 10 \times 9 \times 8 = 720\)。公式也印证了这一点：\(\frac{10!}{7!} = 720\)。

- 组合是无序的选择。当你从 \(n\) 个中选取 \(r\) 个且顺序无关紧要时，需要除去重复的排列顺序：

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

- 符号 \(\binom{n}{r}\) 读作"n 选 r"。核心思想是：每个组合对应 \(r!\) 种排列（选出的 \(r\) 个物品可以有 \(r!\) 种重新排列的方式），因此我们将排列数除以 \(r!\)。

- 示例：从 10 人中组成一个 3 人委员会有多少种方式？顺序无关紧要（没有会长或副会长之分，只有成员），因此我们使用组合：

\[\binom{10}{3} = \frac{10!}{3! \cdot 7!} = \frac{10 \times 9 \times 8}{3 \times 2 \times 1} = 120\]

- 同样的 10 个人产生 720 种排列，但只有 120 种组合，因为每个 3 人组内部有 \(3! = 6\) 种排序方式。

- 组合在概率中至关重要。二项式系数 \(\binom{n}{r}\) 统计了在 \(n\) 次试验中恰好获得 \(r\) 次成功的方式数，这正是二项分布（见文件 03）的核心。

- 让我们通过一个经典的委员会问题来综合运用多种计数思路。

- 问题：一个俱乐部有 8 名男性和 6 名女性。要组成一个 5 人委员会，其中恰好包含 3 名男性和 2 名女性，有多少种方式？

- 第 1 步：从 8 人中选 3 名男性。

\[\binom{8}{3} = \frac{8!}{3! \cdot 5!} = \frac{8 \times 7 \times 6}{3 \times 2 \times 1} = 56\]

- 第 2 步：从 6 人中选 2 名女性。

\[\binom{6}{2} = \frac{6!}{2! \cdot 4!} = \frac{6 \times 5}{2 \times 1} = 15\]

- 第 3 步：应用乘法规则。每种男性选择可以与每种女性选择配对：

\[56 \times 15 = 840 \text{ 个委员会}\]

- 这种将复杂计数问题分解为独立子选择再相乘的模式，是组合数学中的标准方法。

- 还有可重复的排列。当物品可以重复时，从 \(n\) 种类型中选 \(r\) 个会产生 \(n^r\) 种结果。一个使用数字 0-9 的 4 位 PIN 码有 \(10^4 = 10{,}000\) 种可能性。每一位都有 10 种选择，乘法规则即可解决。

- 可重复的组合（也称"星条法"）统计从 \(n\) 种类型中选 \(r\) 个、允许重复且顺序无关的方式数：

\[\binom{n + r - 1}{r} = \frac{(n + r - 1)!}{r!(n - 1)!}\]

- 示例：从 4 种冰淇淋口味中选择 3 勺（允许重复）有 \(\binom{4 + 3 - 1}{3} = \binom{6}{3} = 20\) 种选项。

- 总结计数工具箱：

场景 | 公式
--- | ---
有序，无重复（排列） | \(P(n,r) = \frac{n!}{(n-r)!}\)
无序，无重复（组合） | \(\binom{n}{r} = \frac{n!}{r!(n-r)!}\)
有序，可重复 | \(n^r\)
无序，可重复 | \(\binom{n+r-1}{r}\)

- 每个涉及等可能结果（等概率结果）的概率计算都使用公式 \(P(\text{事件}) = \frac{\text{有利结果数}}{\text{总结果数}}\)。计数为我们提供了这两个数字。有了这个基础，我们将在下一个文件中正式定义概率本身。

## 编程练习（在 CoLab 或 notebook 中完成）

1. 使用阶乘公式和直接计算两种方式计算 \(P(10, 3)\) 和 \(\binom{10}{3}\)。验证排列数总是组合数的 \(r!\) 倍。

       import jax.numpy as jnp
       from math import factorial

       n, r = 10, 3

       perm = factorial(n) // factorial(n - r)
       comb = factorial(n) // (factorial(r) * factorial(n - r))

       print(f"P({n},{r}) = {perm}")
       print(f"C({n},{r}) = {comb}")
       print(f"P / C = {perm // comb} (应等于 {r}! = {factorial(r)})")

2. 通过程序解决委员会问题（8 人中选 3 名男性，6 人中选 2 名女性），并通过枚举所有有效委员会来验证。

       from itertools import combinations
       from math import factorial

       def comb_count(n, r):
           return factorial(n) // (factorial(r) * factorial(n - r))

       # 公式法
       men_ways = comb_count(8, 3)
       women_ways = comb_count(6, 2)
       print(f"公式法: {men_ways} × {women_ways} = {men_ways * women_ways}")

       # 枚举法
       men = [f"M{i}" for i in range(1, 9)]
       women = [f"W{i}" for i in range(1, 7)]
       count = sum(1 for _ in combinations(men, 3) for _ in combinations(women, 2))
       print(f"枚举法: {count}")

3. 统计由 26 个小写字母组成的 4 位密码有多少种（允许重复）。然后统计没有重复字母的密码有多少种。

       from math import factorial

       n = 26
       r = 4

       with_rep = n ** r
       without_rep = factorial(n) // factorial(n - r)

       print(f"允许重复:    {with_rep:>10,}")
       print(f"不允许重复: {without_rep:>10,}")
       print(f"含重复的比例: {1 - without_rep/with_rep:.2%}")

4. 模拟生日问题：在 \(k\) 人的群体中，至少两人共享生日的概率是多少？绘制 \(k = 1\) 到 \(60\) 的概率曲线，并找出概率超过 50% 的位置。

       import jax
       import jax.numpy as jnp
       import matplotlib.pyplot as plt

       def birthday_prob_exact(k):
           \"\"\"k 人群体中至少有一对共享生日的概率。\"\"\"
           p_no_match = 1.0
           for i in range(k):
               p_no_match *= (365 - i) / 365
           return 1 - p_no_match

       ks = list(range(1, 61))
       probs = [birthday_prob_exact(k) for k in ks]

       plt.figure(figsize=(8, 4))
       plt.plot(ks, probs, color="#3498db", linewidth=2)
       plt.axhline(y=0.5, color="#e74c3c", linestyle="--", alpha=0.7, label="50%")
       cross = next(k for k, p in zip(ks, probs) if p >= 0.5)
       plt.axvline(x=cross, color="#e74c3c", linestyle="--", alpha=0.7)
       plt.xlabel("群体大小 (k)")
       plt.ylabel("P(至少两人共享生日)")
       plt.title(f"生日问题（在 k={cross} 时超过 50%）")
       plt.legend()
       plt.grid(alpha=0.3)
       plt.show()