排列与组合的直觉
从 n 个元素中有序选 k 个:第一步 n 种选法,第二步 n-1 种……第 k 步 n-k+1 种,乘起来是 P(n,k) = n!/(n-k)!。若不关心顺序,k 个元素有 k! 种排列方式,所以 C(n,k) = P(n,k)/k! = n!/(k!(n-k)!)。
package main
import "fmt"
// 阶乘(小心溢出,n>20 就超 int64)
func factorial(n int) int64 {
if n <= 1 { return 1 }
return int64(n) * factorial(n-1)
}
// 排列数 P(n,k)
func perm(n, k int) int64 {
if k > n { return 0 }
result := int64(1)
for i := n; i > n-k; i-- {
result *= int64(i)
}
return result
}
// 组合数 C(n,k):用帕斯卡 DP,避免阶乘溢出
func combDP(n, k int) [][]int64 {
C := make([][]int64, n+1)
for i := range C {
C[i] = make([]int64, n+1)
C[i][0] = 1
for j := 1; j <= i; j++ {
C[i][j] = C[i-1][j-1] + C[i-1][j] // 帕斯卡公式
}
}
return C
}
func main() {
// 密码强度:8 位字母数字密码有多少种
chars := 26 + 26 + 10 // 62 个字符
space := perm(62, 62) // 实际是 62^8 = 218 trillion
_ = space
fmt.Printf("8 位密码空间: %.2e\n", float64(218_340_105_584_896))
// 组合:从 10 道题选 3 道有几种
C := combDP(10, 10)
fmt.Println("C(10,3) =", C[10][3]) // 120
// 杨辉三角前 6 行
for i := 0; i <= 5; i++ {
for j := 0; j <= i; j++ {
fmt.Printf("%3d ", C[i][j])
}
fmt.Println()
}
}
组合数取模(竞赛常见)
C(n,k) 很快就会超过 int64,竞赛和工程里常要求 C(n,k) % p(p 是大素数)。此时用卢卡斯定理或预计算阶乘逆元。
package main
import "fmt"
const MOD = 1_000_000_007
func powMod(base, exp, mod int64) int64 {
result := int64(1)
base %= mod
for exp > 0 {
if exp&1 == 1 { result = result * base % mod }
exp >>= 1
base = base * base % mod
}
return result
}
// 预计算阶乘和逆元,O(n) 预处理,O(1) 查询
func buildFactorial(n int) (fac, inv []int64) {
fac = make([]int64, n+1)
inv = make([]int64, n+1)
fac[0] = 1
for i := 1; i <= n; i++ {
fac[i] = fac[i-1] * int64(i) % MOD
}
inv[n] = powMod(fac[n], MOD-2, MOD) // 费马小定理
for i := n - 1; i >= 0; i-- {
inv[i] = inv[i+1] * int64(i+1) % MOD
}
return
}
func main() {
fac, inv := buildFactorial(1000)
comb := func(n, k int) int64 {
if k < 0 || k > n { return 0 }
return fac[n] * inv[k] % MOD * inv[n-k] % MOD
}
fmt.Println(comb(100, 50)) // C(100,50) mod 1e9+7
fmt.Println(comb(10, 3)) // 120
}
实战:A/B 测试的样本量计算
A/B 测试需要估算「至少需要多少用户才能区分 1% 的转化率差异」。这是统计功效分析的问题,底层用到二项分布,而二项分布的系数正是 C(n,k)。工程里的公式:n ≈ 2 × (z_α/2 + z_β)² × p(1-p) / Δ²,其中 z 值来自正态分布表(概率论内容将在第 8 章展开)。
✓推荐做法
- 计算 C(n,k) 时用帕斯卡递推或逆元方法,而非直接算阶乘
- 大数组合
%p用费马小定理求逆元(p 是大素数) - 区分有顺序(排列)和无顺序(组合)是解题第一步
✗不推荐
- 用
factorial(n) / factorial(k) / factorial(n-k)计算 C(n,k)——溢出且慢 - 忘记 C(n,0) = C(n,n) = 1 的边界
⚠常见误区
- C(n,k) = C(n, n-k),对称性可以减少一半的计算量
判断标准:能解释帕斯卡公式的组合含义(选 or 不选第 n 个元素)→ 掌握本章。
排列组合是把无限的可能性折叠成有限的数字——每个数字背后都是一片宇宙。
— 具体数学