卢卡斯定理是一个与组合数有关的数论定理,在算法竞赛中用于求组合数对某质数的模。算法
第一部分是博主的我的理解,第二部分为 Pecco 学长的介绍函数
通常状况下,咱们计算大组合数取模问题是用递推公式进行计算的:ui
其中p相对较小的素数。可是当n和m过大时,计算的耗费就急剧增长\(O(mn)\),在实践中不适用。当这时候就须要Lucas定理进行快速运算:spa
其中:code
证实方法也很简单,主要用到以下等式:blog
应用这个公式,能够的到以下递归式递归
这里的\(Lucas(n,m,p)\)就是\(C_n^m\ mod\ p\),递归终点就是当\(n=0\)的时候。时间复杂度是\(O(log_p(n)*p)\).rem
若是上面的解释没有理解的话请往下看一下 Pecco 学长的介绍get
开篇说的就很清楚了,数学
卢卡斯定理是一个与组合数有关的数论定理,在算法竞赛中用于求组合数对某质数的模。
在咱们谈论卢卡斯定理前,咱们先来看看朴素的求组合数的方法有哪些。
若是直接根据定义 \(C_m^n = \frac{m!}{n!(m-n)!}\) 直接计算,显然很容易溢出,事实上当 m=21
时,\(21! = 51,090,942,171,709,440,000\) 就已经大于64位整数能够表示的范围了。固然咱们能够边乘边除,但有点麻烦。因而咱们有另一种思路,利用递推式:\(C_m^n =C_{m-1}^{n - 1} + C_{m - 1}^n\) (这个递推式能够从杨辉三角看出)。这种方法相对不容易溢出,时间复杂度为 \(\mathcal{O}(n^2)\) 其实若是对精度要求不高的话,最简单快捷的方法是利用对数。因为 :
因此只须要用 \(\mathcal{O}(n)\) 预处理出 \(ln\ x\) 的前缀和,便可 \(\mathcal{O}(1)\) 求出结果,但可能有浮点偏差。
然而,实际上,组合数的增加速度是很是快的,\(C_{100}^{50}\) 已是30位数,\(C_{300}^{150}\) 则有89位数,比宇宙中的原子数还多。(宇宙中的原子数:怎么老是拿我来对比?)所谓递推不容易溢出,那若是结果自己就溢出了,你又怎么办呢?
所幸算法竞赛中的题目经常会要求将结果对某个质数 \(p\) 取模,这样一来,溢出的问题就不用太担忧了。咱们干脆直接回到最原始的方法:\(C_m^n=\frac{m!}{n!(m-n)!}\)。只不过,如今咱们要把除法变成求 逆元,也即:\(C_m^n = m!·inv(n!)·inv[(m - n)!]\ (mod\ p)\)。
模 \(p\) 意义下阶乘和逆元均可以 \(\mathcal{O}(n)\)预 处理出来,而后直接 \(\mathcal{O}(1)\) 查询便可(实际上不预处理逆元直接 \(\mathcal{O}(log\ n)\) 求也绰绰有余)。这基本上是最经常使用的求组合数方法。
绕了一圈,怎么还没提到卢卡斯定理呢?嗯……通常来讲,这个方法够用了。恰恰,有时候, \(p\) 可能比 \(m\) 小....
这下麻烦了。若是 \(p\) 比 \(m\) 小,就不能保证 \(n\) 和 \(m - n\) 的逆元存在了(它们多是 \(p\) 的倍数)。固然仍是能够用杨辉三角递推,但 \(\mathcal{O}(n^2)\)仍是太不理 想。因而,本文的主角——卢卡斯定理终于要出场了。
卢卡斯定理(Lucas's theorem):
对于非负整数 \(m,n\) 和质数 \(p\) ,\(C_m^n = \prod_{i = 0}^k\ (mod\ p)\) 其中
\(m = m_kp^k + …… +m_1p + m_0\) 、\(n = n_kp^k + …… +n_1p + n_0\) 是 \(m\) 和 \(n\) 的 \(p\) 进制展开
但其实,咱们通常使用的是这个能够与之互推的式子:
\(C_m^n = C_{m\ mod\ p}^{n\ mod\ p}·C_{\lfloor\frac{m}{p}\rfloor}^{\lfloor\frac{n}{p}\rfloor}\ (mod\ p)\)
当 \(m < n\) 时,规定 \(C_m^n = 0\) (待会儿会将这个规定的意义)。
就像展转相除法那样,能够利用这个式子递归求解,递归出口是 \(n = 0\) 。其实这篇文章只须要这个好记的公式就够了,你甚至能够立刻写出卢卡斯定理的板子:
// 须要先预处理出fact[],即阶乘 inline ll C(ll m, ll n, ll p) { return m < n ? 0 : fact[m] * inv(fact[n], p) % p * inv(fact[m - n], p) % p; } inline ll lucas(ll m, ll n, ll p) { return n == 0 ? 1 % p : lucas(m / p, n / p, p) * C(m % p, n % p, p) % p; }
网上说卢卡斯定理的复杂度是 \(\mathcal{O}(p\ log_p\ m)\) ,但若是阶乘和逆元都采起递推的方法预处理,(只须要预处理 \(p\) 之内的),每次调用C()
函数应该都是 的\(\mathcal{O}(1)\),一共要调用 \(log_p\ m\) 次,那么复杂度应该是 \(\mathcal{O}(p + log_p\ m)\) 才对。洛谷上这道模板题的范围才给到 \(\mathcal{O}(10^5)\) ,屈才了。
接下来咱们来证实这个式子。若是你对数学推导没有兴趣能够走了(雾