【算法学习笔记】组合数与 Lucas 定理

卢卡斯定理是一个与组合数有关的数论定理,在算法竞赛中用于求组合数对某质数的模。

第一部分是博主的个人理解,第二部分为 Pecco 学长的介绍

一篇很好的 卢卡斯定理 博文

第一部分

一般情况下,我们计算大组合数取模问题是用递推公式进行计算的:

Cnm=(Cn1m+Cn1m1)mod p

其中p相对较小的素数。但是当n和m过大时,计算的耗费就急剧增加O(mn),在实践中不适用。当这时候就需要Lucas定理进行快速运算:

Cnm=i=0kCnimi mod p

其中:

m=mkpk+mk1pk1+...+m1p+m0

n=nkpk+nk1pk1+...+n1p+n0

证明方法也很简单,主要用到如下等式:

Cpj0 mod p (1jp1)

(1+x)p1+xp mod p

应用这个公式,可以的到如下递归式

这里的Lucas(n,m,p)就是Cnm mod p,递归终点就是当n=0的时候。时间复杂度是O(logp(n)p).


如果上面的解释没有理解的话请往下看一下 Pecco 学长的介绍

第二部分

开篇说的就很清楚了,

卢卡斯定理是一个与组合数有关的数论定理,在算法竞赛中用于求组合数对某质数的模。

在我们谈论卢卡斯定理前,我们先来看看朴素的求组合数的方法有哪些。

如果直接根据定义 Cmn=m!n!(mn)! 直接计算,显然很容易溢出,事实上当 m=21 时,21!=51,090,942,171,709,440,000 就已经大于64位整数可以表示的范围了。当然我们可以边乘边除,但有点麻烦。于是我们有另外一种思路,利用递推式:Cmn=Cm1n1+Cm1n (这个递推式可以从杨辉三角看出)。这种方法相对不容易溢出,时间复杂度为 O(n2) 其实如果对精度要求不高的话,最简单快捷的方法是利用对数。由于 :

lnCmn=ln m!ln n!ln(mn)!=x=1mlnxx=1nlnxx=1mnlnx

所以只需要用 O(n) 预处理出 ln x 的前缀和,即可 O(1) 求出结果,但可能有浮点误差。

然而,实际上,组合数的增长速度是非常快的,C10050 已经是30位数,C300150 则有89位数,比宇宙中的原子数还多。(宇宙中的原子数:怎么总是拿我来对比?)所谓递推不容易溢出,那如果结果本身就溢出了,你又怎么办呢?

所幸算法竞赛中的题目常常会要求将结果对某个质数 p 取模,这样一来,溢出的问题就不用太担心了。我们干脆直接回到最原始的方法:Cmn=m!n!(mn)!。只不过,现在我们要把除法变成求 逆元,也即:Cmn=m!·inv(n!)·inv[(mn)!] (mod p)

p 意义下阶乘和逆元都可以 O(n)预 处理出来,然后直接 O(1) 查询即可(实际上不预处理逆元直接 O(log n) 求也绰绰有余)。这基本上是最常用的求组合数方法。

绕了一圈,怎么还没提到卢卡斯定理呢?嗯……一般来说,这个方法够用了。偏偏,有时候, p 可能比 m 小....

这下麻烦了。如果 pm 小,就不能保证 nmn 的逆元存在了(它们可能是 p 的倍数)。当然还是可以用杨辉三角递推,但 O(n2)还是太不理 想。于是,本文的主角——卢卡斯定理终于要出场了。

卢卡斯定理(Lucas's theorem):

对于非负整数 m,n 和质数 pCmn=i=0k (mod p) 其中

m=mkpk++m1p+m0n=nkpk++n1p+n0mnp 进制展开

但其实,我们一般使用的是这个可以与之互推的式子:

Cmn=Cm mod pn mod p·Cmpnp (mod p)

m<n 时,规定 Cmn=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;
}

网上说卢卡斯定理的复杂度是 O(p logp m) ,但如果阶乘和逆元都采取递推的方法预处理,(只需要预处理 p 以内的),每次调用C()函数应该都是 的O(1),一共要调用 logp m 次,那么复杂度应该是 O(p+logp m) 才对。洛谷上这道模板题的范围才给到 O(105) ,屈才了。

接下来我们来证明这个式子。如果你对数学推导没有兴趣可以走了(雾

demo (2)

demo (2)

posted @   RioTian  阅读(822)  评论(0编辑  收藏  举报
编辑推荐:
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
阅读排行:
· Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!
· 分享4款.NET开源、免费、实用的商城系统
· 解决跨域问题的这6种方案,真香!
· 一套基于 Material Design 规范实现的 Blazor 和 Razor 通用组件库
· 5. Nginx 负载均衡配置案例(附有详细截图说明++)
历史上的今天:
2020-04-14 最小生成树算法【图解】:一文带你理解什么是Prim算法和Kruskal算法
点击右上角即可分享
微信分享提示

📖目录