zoukankan      html  css  js  c++  java
  • 【算法学习笔记】组合数与 Lucas 定理

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

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

    第一部分

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

    [C_n^m=(C_{n-1}^m+C_{n-1}^{m-1}) mod p ]

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

    [C_n^m=prod_{i=0}^{k}C_{n_i}^{m_i} mod p ]

    其中:

    [m=m_kp^k+m_{k-1}p^{k-1}+...+m_1p+m_0 ]

    [n=n_kp^k+n_{k-1}p^{k-1}+...+n_1p+n_0 ]

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

    [C_p^jequiv 0 mod p ( 1 leq j leq p-1 ) ]

    [(1+x)^{p}equiv 1+x^p mod p ]

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

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


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

    第二部分

    开篇说的就很清楚了,

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

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

    如果直接根据定义 (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)) 其实如果对精度要求不高的话,最简单快捷的方法是利用对数。由于 :

    [ln C_m^n = ln m! - ln n! - ln(m - n)! = sum_{x=1}^mlnx - sum_{x=1}^nlnx - sum_{x=1}^{m-n}lnx ]

    所以只需要用 (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_{lfloorfrac{m}{p} floor}^{lfloorfrac{n}{p} floor} (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)) ,屈才了。

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

    demo (2)

    The desire of his soul is the prophecy of his fate
    你灵魂的欲望,是你命运的先知。

  • 相关阅读:
    Java8-Stream-No.10
    Java8-Stream-No.09
    Java8-Stream-No.08
    Java8-Stream-No.07
    Java8-Stream-No.06
    Java8-Stream-No.05
    Java8-Stream-No.04
    Java8-Stream-No.03
    Java8-Stream-No.02
    Java8-Stream-No.01
  • 原文地址:https://www.cnblogs.com/RioTian/p/14659917.html
Copyright © 2011-2022 走看看