【算法學習筆記】組合數與 Lucas 定理

盧卡斯定理是一個與組合數有關的數論定理,在算法競賽中用於求組合數對某質數的模。算法

第一部分是博主的我的理解,第二部分爲 Pecco 學長的介紹函數

第一部分

通常狀況下,咱們計算大組合數取模問題是用遞推公式進行計算的:ui

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

其中p相對較小的素數。可是當n和m過大時,計算的耗費就急劇增長\(O(mn)\),在實踐中不適用。當這時候就須要Lucas定理進行快速運算:spa

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

其中:code

\[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 \]

證實方法也很簡單,主要用到以下等式:blog

\[C_p^j\equiv 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)\).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)\) 其實若是對精度要求不高的話,最簡單快捷的方法是利用對數。因爲 :

\[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_{\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)\) ,屈才了。

接下來咱們來證實這個式子。若是你對數學推導沒有興趣能夠走了(霧

demo (2)

相關文章
相關標籤/搜索