# 國立新竹科學園區實驗高級中學 H10335廖丞胤 數學科學習歷程檔案:費馬小定理與模反元素 ## 前言 某天的下午我正坐在座位上寫著競賽程式的題目,我的一位朋友(我們暫且稱他為$P_L$)$P_L$傳了一個有趣的題目給我,是跟排列組合有關的,題目的詳細我也忘了,就在我花了半個小時想出解法後,我卡在一個瓶頸:執行時限。 競賽程式對於執行時間的限制十分嚴謹,有時不用特定的演算法而單用暴力搜尋,執行的結果往往會是`TLE`(Time Limit Exceeded,執行時間超時的意思),而在那個題目限制住我的瓶頸就是求$C^n_k$的速度不夠快($1\le n,k\le10^5$,因為這個數字出來的$(^n_k)$會超大,所以題目要求要先模$10^9+7$:這是一個質數)。 後來問了$P_L$到底是怎麼解出來的?會不會是我多想了甚麼導致沒有看到優化的捷徑,他給我看他的程式碼,構想跟我的完全一樣,只差在求$C^n_k$的部分,他用了$O(n\cdot logMod)$就預處理完$C^n_k$的部分,他只要每次花$O(1)$就可以查詢出來,完全海放我的$O(nk)$,他解釋的時候講到了兩個聽起來很難的東西: 費馬小定理跟模反元素。 ## 費馬小定理 "假設a,p皆為整數,且p為質數,則式(1)必成立" $a^p\equiv a\pmod p$ <font size=2>式(1)</font> "又若a不是p的倍數,則可以將式(1)改寫為式(2)" $a^{p-1}\equiv 1\pmod p$ <font size=2>式(2)</font> ### 證明: 考慮 $(^p_n)=\frac{p!}{n!(p-n)!}$且限定$n$不為$p$或$0$,則分子有質數$p$,但分母不含$p$,故分子的$p$必定會被保留不被除去,即$(^p_n)$恆為$p$的倍數。 再考慮$(b+1)^{p}$的展開模$p$,得: $(b+1)^p\equiv (^p_p)b^p+(^p_{p-1})b^{p-1}+(^p_{p-2})b^{p-2}+\dots+ (^p_{1})b^{1}+(^p_0)b^0\pmod p$ $\equiv(^p_p)b^p+(^p_0)b^0\pmod p$ $\equiv b^p+1\pmod p$ 因此 $(b+1)^p \equiv b^p+1\pmod p$ $\equiv (b-1)^p+1+1\pmod p$ $\equiv (b-2)^p+1+1+1\pmod p$ $\dots$ $\equiv 1+1+1+1+1\dots+1\pmod p$ (共$b+1$個) $\equiv b+1\pmod p$ 令$b=a-1$,即得$a^p\equiv a\pmod p$ <font size=3>證畢$\#$</font> ## 模反元素 一整數$a$對模$n$的模反元素為$b$,則式(3)成立 $a^{-1}\equiv b\pmod n$ <font size=2>式(3)</font> 也可整理為 $ab\equiv 1 \pmod n$ 這個$b$存在的充分必要條件是$a,n$戶質 若$b$存在,**在模數$n$的除法下可以用和對應的模反元素的乘法來達成** ### 求模反元素 根據費馬小定理$a^p\equiv a\pmod p$,如果$p$為質數,則可以改寫為$a^{p-1}\equiv 1\pmod p$,$a^{p-1} \equiv a \cdot a^{p-2}\equiv 1 \pmod p$,$a$模$p$的模反元素即為$a^{p-2}$ ## 關於$O(NlogMod)$求$C^n_k$的實現 > 明明就可以直接線性求階乘然後直接代公式啊?用這個幹嘛? 我們都知道這個方式在$n,k$很小的時候是可以運作的,但你要考慮到: ### 題目要求我們模$10^9+7$ 就算我們先算出階乘,乘出$C^n_k$後再模,也會因為$overflow(溢位)$造成錯誤 "C++ `unsigned long long`範圍:$0 \sim 2^{64}-1$,就算開這個也存不下$(10^5)!$" ### C++的實現 我們就是先$O(N)$算出$1\sim n$的所有的階乘,然後每次求出$i!$的時候花$O(logMod)$求出模反元素,之後查詢只要花$O(1)$這樣算 $n!\times rev(k!)\times rev((n-k)!)\pmod {Mod}$ ($x$的模反元素表示為$rev(x)$) 這樣我們就達成$O(N\cdot logMod)$求$C^n_k$ ```cpp= #include <bits/stdc++.h> #define int long long #define MOD Mod1 using namespace std; const int Mod1=1000000007; int factorial[200005],rev[200005]; int po(int xx,int yy){//矩陣快速冪,競賽的另一個技巧,下次有機會我們再來深入討論 if(yy==0)return 1; if(yy==1)return xx; int zz=po(xx,yy/2); int ww=(zz*zz)%MOD; if(yy%2==1)return (ww*xx)%MOD; return ww; } void precalculate(){ factorial[0]=factorial[1]=rev[0]=rev[1]=1;//factorial[i]存i!模MOD for(int i=2;i<100005;i++){ //rev[i]存factorial[i]模MOD的模反元素 factorial[i]=factorial[i-1]*i%MOD; rev[i]=po(factorial[i],MOD-2); } } int cmod(int a,int b){ return factorial[a]*rev[b]%MOD*rev[a-b]%MOD; } signed main(){ precalculate(); int n,k;cin>>n>>k; cout<<cmod(n,k)<<endl; }//基本上這個程式就是輸入n,k然後輸出Cn取k模10^9+7 ``` ## 後續與結語 當下我改完求$C^n_k$的部分後上傳就順利拿到滿分了,我真的是很佩服$P_L$,感覺他總是能找到神奇的數學性質並且運用在競賽上。所以為了不輸給他太多,在學完這個後我又上網研究了一些其他經典的數學理論,並且發現許多曾經在競賽時看到的題目,或是一些可以運用在競賽上的技巧,這讓我更加的對數學有興趣,研究這些理論讓我覺得蠻快樂的,未來我還會不斷探索之前從未學習過的東西,希望能夠有更多有趣的收穫。