在一些计数问题中,有的时候需要预处理组合数。这里有两种方法:
- 动态规划(杨辉三角),用于规模较小的情况;
- 阶乘 + 逆元,用于规模较大的情况。
动态规划(杨辉三角)
计算 $C(n,k)$ ,从 $n$ 个里面取 $k$ 个,从动态规划的角度来思考可以看成以下两种情况的和:
- 第 $n$ 个选,在前 $n-1$ 个里再选 $k-1$ 个;
- 第 $n$ 个不选,在前 $n-1$ 个里再选 $k$ 个。
因此,$C(n,k)=C(n-1,k-1) + C(n-1,k)$ ——将杨辉三角“变个形”,可以发现这和杨辉三角的计算规则是一致的。
代码
1 | d = [[0] * (n + 1) for _ in range(n + 1)] |
注意:因为递推式里有 $i-1$ 和 $j -1 $,所以 $i$ 和 $j$ 都是从 $1$ 开始的。也因此,$d[.][0]$ 要另外处理,其值均为 $1$。
阶乘 + 逆元
逆元
「逆元」这东西,我之前学过好几次都学不明白,今天不追究到底跟 ChatGPT 学了之后,算是明白了一些,至少用在算组合数上大概知道怎么回事了。下面根据学习的内容,总结、整理、推导一下。
聊天内容在这里,详细可以回去再看,下面就简单总结性地写。
开始
首先要强调的是,「逆元」是在「模的世界」里才有的东西。之前看其他视频、文章啥的,上来就类比普通数里倒数关系,我觉得是挺不妥、有点摸不着头脑的。有了这个前提之后,我们再来看什么是逆元:
对于一个数 $a$,如果有一个数 $x$,使得 $a \times x \equiv 1 \pmod{MOD}$,就说 $x$ 是 $a$ 的逆元,记作 $inv(a)$。
这定义是我说的,不一定准确,但够用了。
那么逆元有什么用呢?那就是在「模的世界」里代替除法。在「模的世界」中的运算,是没有除法的,而且就算在普通数里,我们也不喜欢除法,尤其是大数的情况下。
举两个例子。
假设现在 $MOD = 7$ ,我们要计算 $5 \div 3 \equiv \ ? \pmod{7}$。
- 第一种思考方式,就是算 $3$ 乘多少模 $7$ 等于 $5$,通过不断试,得到结果是 $4$;
- 第二种思考方式就是逆元,因为 $3 \times 5 \equiv 1 \pmod{7}$,所以在模 $7$ 的世界里,$3$ 的逆元是 $5$,那么 $ 5 \div 3 = 5 \times inv(3) = 5 \times 5 \equiv 4 \pmod{7}$,和上一种结果一样。
假设现在 $MOD = 11$,我们要计算$7 \div 4 \equiv \ ? \pmod{11}$。
- 第一种思考方式,就是算 $4$ 乘多少模 $11$ 等于 $7$,通过不断试,得到结果是 $10$;
- 第二种思考方式就是逆元,因为 $4 \times 3 \equiv 1 \pmod{11}$,所以在模 $11$ 的世界里,$4$ 的逆元是 $3$,那么 $ 7 \div 4 = 7 \times inv(4) = 7 \times 3 \equiv 10 \pmod{11}$,和上一种结果一样。
用到组合数里
组合数的计算公式:
用 $inv(k!)$ 和 $inv((n-k)!)$ 换掉 $ \div k! $ 和 $(n-k)!$,得到
好啦,这就是后面计算要用到的公式了。
接下来,问题就到了怎么求逆元了。
怎么求
我们要算 $x!$ 的逆元,我们先看这件显然的事情,
瞧啊,这不是除法么,我们在 $MOD$ 的世界里,把它改成逆元的形式,
两边同时再取逆元(这里的运算规则我不会,但 ChatGPT 是这么做的,那我就凭感觉这么做了),
看呐,这不就是个递推式么,那如果我们算出了 $x!$ 的逆元,那不就能一路算出 $(x-1)!, (x-2)!, … 2, 1$ 的逆元了~
那接下来怎么算 $inv(x!)$ 呢?
从 Python3.8 开始,我们可以直接写 $pow(x!, -1, MOD)$,但在 3.8 之前和其他语言,就不能这么写了,而是将 $-1$ 改成 $MOD - 2$。
为什么是 $MOD - 2$ 呢?因为 $x \times x^{MOD - 2} \equiv 1 \pmod{MOD}$,即 $x$ 的逆元就是 $x^{MOD - 2}$。
证明一下。
在 $MOD$ 的世界里,不算 $0$ 只有 $MOD - 1$ 个数字,记它们的所有数的乘积是 $s$ 。现在给所有数都乘上 $x$,于是有
两边都“除以” $s$,
也就是
好了,写代码了。
代码
1 | MOD = 1_000_000_007 |
更加通用的写法是,inv_fac[MX - 1] = pow(fac[MX - 1], MOD - 2, MOD)。
更进一步的,pow()背后做的就是快速幂,所以更通用的可以自己写快速幂。
更新
上面说「逆元」是在「模的世界」里才有,是不对的。根据豆包的解释,不同的代数系统都可以有逆元。
但我确实还是只站在「模的世界」出发,会好理解一些些逆元在算法中的作用。