多項式:從門都沒入到剛邁過門檻
還記得我上次講多項式的文章么?經過CSP的洗禮和最近一段時間的學習后,我對多項式的理解又加深了吧。
初三黨文化課壓力是大,盡管上次月考年級第一,但是文化課還是不能掉以輕心,畢竟要中考,也不能沒學上吧。
零、快速數論變換(NTT)
多項式乘法是多項式其他操作的一個基礎。上次我們研究了一下FFT。但是不少題目都是要求取模的。顯然,FFT中的復數是不能取模的。而且,很多時候題目中多項式的系數也都是整數。這個時候,NTT就派上用場了!
前置知識:原根
(其實我本來是打算寫一篇博客講這玩意的,但是被我咕了)
階:當 \((a,m)=1\)時,令最小的\(x\)使得\(a^x\equiv 1\pmod{m}\)為\(a\pmod{m}\)的階。
有一個很重要的性質:\(x|\varphi(m)\)。
當\(a\)的取值為\(g\)時,如果它的階就是\(\varphi(m)\),那么我們稱\(g\)為\(m\)的原根。
原根怎么求呢?考慮到大部分情況下原根都比較小,我們只需從2開始枚舉原根,然后從\(1\)到 \(\varphi(m)\)都試一下就行了。
常見的模數\(998244353,1004535809,469762049\)的原根都是\(3\)。
下面是關於原根的幾個結論。我這里就不證明了。(畢竟重點在NNT不在原根上)
1.\(g^0,g^1,g^2,\dots,g^{\varphi(m)}\)兩兩不同,構成了\(m\)的完全剩余系。
2.只有\(2,4,p^k,2p^k\)存在原根。
3.若\(g\)為\(p\)的原根,\(g\)也是\(p^k\)原根。
我們回顧一下FFT的過程。FFT結合了單位根的美妙性質,完成了蝴蝶操作,從而使復雜度降低為\(O(n \log n)\)。
而在模意義下,原根也具有單位根的性質!
我們只需要把FFT中的\(\pi\)換成模數,把單位根換成原根,就是NTT了!
核心代碼:
inline void NTT(int *a,int len,int f){
for(int i=0;i<len;i++) if(i<r[i]) swap(a[i],a[r[i]]);
for(int mid=1;mid<len;mid<<=1){//枚舉合並區間的一半長度
int wn=ksm((f==1?g:gi),(MOD-1)/(mid<<1));//求當前的原根
for(int j=0;j<len;j+=(mid<<1)){//進行蝴蝶操作
int w=1;
for(int k=j;k<j+mid;k++){
int u=a[k];
int t=(1ll*w*a[k+mid])%MOD;
a[k]=(u+t)%MOD;
a[k+mid]=(u-t+MOD)%MOD;
w=(1ll*w*wn)%MOD;
}
}
}
}
那么NTT和FFT的效率對比?


好吧,至少在這道題里,以及我寫的代碼里,NTT是完爆FFT的。
在我們開始多項式的其他操作之前,我們先給大家講個概念:卷積
設有兩個數列\(a\),\(b\),那么這兩個數列的卷積\(c\)定義為:
我們常常記作:
我們很容易的得到,多項式的乘法與多項式的卷積是一致的:
設\(F(x)=\sum a_i x^i\),\(G(x)=\sum b_ix^i\),\(A(x)=\sum c_ix^i\),且\(c=a*b\),於是我們可以得到:
下面就讓我們看看多項式的一些其他操作吧:
一、多項式求逆
已知一個多項式\(G(x)\),求一個多項式\(F(x)\),使得:
當\(n=1\)時,\(G(x)\)是一個常數\(c\),那么,\(F(x)=c^{-1}\)
假設我們已經知道了一個多項式\(F_0(x)\),使得:
那么顯然,我們有:
將上述兩式相減,我們得到:
由於\(G(x)\)有常數項,所以我們有:
將上式平方得:
展開:
因為\(F(x)\)現在是二次的,我們想方法把他變成一次。根據最開始的式子,我們有:
所以:
每次多項式乘法是\(O(n \log n )\)的,所以:
核心代碼:
inline void P_inv(int* a,int* ans,int n){
static int tmp[maxn<<1];
if(n==1){
ans[0]=ksm(a[0],MOD-2);
return;
}
P_inv(a,ans,(n+1)>>1);
int len=1,cnt=0;
while(len<(n<<1)) len<<=1,cnt++;
for(int i=0;i<len;i++) r[i]=(r[i>>1]>>1)|((i&1)<<(cnt-1));
for(int i=0;i<n;i++) tmp[i]=a[i];
for(int i=n;i<len;i++) tmp[i]=0;
NTT(tmp,len,1); NTT(ans,len,1);
for(int i=0;i<len;i++) ans[i]=1ll*(2ll-1ll*tmp[i]*ans[i]%MOD+MOD)%MOD*ans[i]%MOD;
NTT(ans,len,-1);
for(int i=n;i<len;i++) ans[i]=0;
}
二、多項式\(\ln\)
已知一個多項式\(G(x)\),求一個多項式\(F(x)\),使得:
等一下,多項式有ln這種東西?
沒錯,它有,定義如下:
事實上,你可以把它看作多項式與麥克勞林級數的復合。
所以根據定義,它的常數項一定為1,否則不存在ln了。
見到\(\ln\),我們一般都是要求導的:
然后我們再積分就行了:
時間復雜度\(O(n \log n)\)
核心代碼:
inline void P_der(int *a,int *ans,int n){
for(int i=1;i<n;i++) ans[i-1]=1ll*i*a[i]%MOD;
ans[n-1]=0;
}
inline void P_int(int *a,int *ans,int n){
for(int i=1;i<n;i++) ans[i]=1ll*ksm(i,MOD-2)*a[i-1]%MOD;
ans[0]=0;
}
inline void P_ln(int *a,int *ans,int n){
static int inva[maxn<<1],dera[maxn<<1];
int len=1,cnt=0;
while(len<(n<<1)) len<<=1,cnt++;
for(int i=0;i<len;i++) r[i]=(r[i>>1]>>1)|((i&1)<<(cnt-1));
for(int i=0;i<len;i++) inva[i]=dera[i]=0;
P_inv(a,inva,n);
P_der(a,dera,n);
NTT(inva,len,1); NTT(dera,len,1);
for(int i=0;i<len;i++) inva[i]=(1ll*inva[i]*dera[i])%MOD;
NTT(inva,len,-1);
P_int(inva,ans,n);
for(int i=n;i<len;i++) ans[i]=0;
}
三、多項式牛頓迭代
已知一個函數\(G(x)\),求一個多項式\(F(x)\),使得:
當\(n=1\)時,\(G(F(x))\equiv 0 \pmod{x}\),這個單獨求。
假設我們已經知道了一個多項式\(F_0(x)\),使得:
考慮將\(G(F(x))\)在\(F_0(x)\)處泰勒展開:
由於\(F(x)-F_0(x))\)最低的非\(0\)次項次數最低為\(\lceil n/2 \rceil\),所以我們有:
移項,合並同類項,我們有:
四、多項式exp
已知一個多項式\(G(x)\),求一個多項式 \(F(x)\),使得:
多項式有exp?
和多項式對數函數一樣,我們也可以把它看作是麥克勞林級數:
這里,多項式的常數項必須為0,否則常數項不收斂。
兩邊取自然對數得:
移項得:
令函數\(f(F(x))=\ln F(x) -G(x)\),對其進行牛頓迭代,我們就有了遞推式:
通過計算得到:
代入,經過計算,我們便得到:
時間復雜度
核心代碼:
inline void P_exp(int *a,int *ans,int n){
static int lna[maxn<<1],tmp[maxn<<1];
if(n==1){
ans[0]=1;
return;
}
P_exp(a,ans,(n+1)>>1);
P_ln(ans,lna,n);
int len=1,cnt=0;
while(len<(n<<1)) len<<=1,cnt++;
for(int i=0;i<len;i++) r[i]=(r[i>>1]>>1)|((i&1)<<(cnt-1));
for(int i=0;i<n;i++) tmp[i]=a[i];
for(int i=n;i<len;i++) tmp[i]=0;
NTT(tmp,len,1); NTT(lna,len,1);
NTT(ans,len,1);
for(int i=0;i<len;i++) ans[i]=1ll*ans[i]*((1+tmp[i]-lna[i]+MOD)%MOD)%MOD;
NTT(ans,len,-1);
for(int i=n;i<len;i++) ans[i]=0;
}
(這看似短短的代碼調了我兩天。。。主要是前面寫的ln和inv中有很多數組清空的不徹底。。。)
五、多項式開根
已知一個多項式\(G(x)\),求一個多項式\(F(x)\),使得:
移項得:
令函數\(f(F(x))=F^2(x)-G(x)\),對其進行牛頓迭代,我們就有了遞推式:
通過計算得到:
代入,經過計算,我們便得到:
將式子變形一下:
時間復雜度\(O(n \log n)\)
剛剛還看到了一種解法,和大家分享一下:
兩邊同取自然對數:
所以
所以
牛頓迭代版:
inline void P_sqrt(int *a,int *ans,int n){
static int tmp[maxn<<1],inva[maxn<<1];
if(n==1){
ans[0]=1;
return;
}
P_sqrt(a,ans,(n+1)>>1);
int len=1,cnt=0;
while(len<(n<<1)) len<<=1,cnt++;
for(int i=0;i<len;i++) r[i]=(r[i>>1]>>1)|((i&1)<<(cnt-1));
for(int i=0;i<len;i++) inva[i]=0;
P_inv(ans,inva,n);
for(int i=0;i<n;i++) tmp[i]=a[i];
for(int i=n;i<len;i++) tmp[i]=0;
NTT(tmp,len,1); NTT(inva,len,1);
for(int i=0;i<len;i++) tmp[i]=(1ll*tmp[i]*inva[i])%MOD;
NTT(tmp,len,-1);
for(int i=0;i<len;i++) ans[i]=1ll*(tmp[i]+ans[i])%MOD*inv2%MOD;
for(int i=n;i<len;i++) ans[i]=0;
}
exp版:
inline void P_sqrt(int *a,int *ans,int n){
static int lna[maxn<<1];
P_ln(a,lna,n);
for(int i=0;i<n;i++) lna[i]=(1ll*lna[i]*inv2)%MOD;
P_exp(lna,ans,n);
}
然而用exp寫的T了3個點。。。還是老老實實寫牛迭吧。
大家有沒有發現,在上面牛頓迭代的代碼中,我們默認了常數項是1?那么如果常數項不是1呢?
這時候,我們就要計算二次剩余了。
(又是一個被我咕了的東西)
六、多項式快速冪
已知一個多項式\(G(x)\),求一個多項式\(F(x)\),使得
其中\(k\in \Q\)
因為
所以
時間復雜度\(O(n \log n)\)
核心代碼:
inline void P_power(int* a,int *ans,int n){
static int lna[maxn];
for(int i=0;i<n;i++) lna[i]=0;
P_ln(a,lna,n);
for(int i=0;i<n;i++) lna[i]=(1ll*lna[i]*k)%MOD;
P_exp(lna,ans,n);
}
在洛谷的模板題里,指數達到了\(10^{10^5}\)級別,那么我們該怎么辦呢?其實很簡單,我們只需將指數對\(MOD\)取模就行了。(這個還是比較顯然的吧)
注意,因為ln的緣故,這里常數項也必須是1。
好了,先將這么多操作,以后我會補充的。
那我們來一道例題吧:
求:
其中\(S(i,j)\)為第二類斯特林數
首先我我們呢得知道第二類斯特林數是啥。
\(S(i,j)\)是用來解決LUB問題的(就是把\(i\)個不同的球放入\(j\)個相同的盒子里,且盒子不能為空。有興趣的可以去了解TwelveFoldway)。
我們先考慮盒子不同時怎么做。考慮枚舉空盒的個數,那么我們就可以大力容斥了,最后因為盒子相同,除以\(j!\)即可。於是我們得到:
現在我們開始推式子。
改變枚舉順序,將\(2^j\)提取出來,我們得到:
(注意當\(i<j\)時,\(S(i,j)=0\))
代入第二類斯特林數的計算公式,我們得到:
將\(k\)的枚舉提前,我們得到:
我們可以看到,后面兩個sigma相乘,其實可以把他們寫成多項式卷積。
設
所以我們有:
於是我們直接NTT就好了。
我們再來一道題:P3321 [SDOI2015]序列統計
給定整數 \(x\),求所有可以生成出的,且滿足數列中所有數的乘積 \(\bmod \ m\)的值等於 \(x\) 的不同的數列的有多少個。
既然是計數,那么我們考慮\(dp\)。
令\(dp[i][j]\)表示選了\(i\)個數,數列的積模\(m\)的值為\(j\),那么我們有以下的狀態轉移方程:
我們觀察這個式子,發現如果是\(a+b=c\)的話,我們就完全可以卷積嘛!
那么我們這里就有了一個奇技淫巧:取對數
我們在高一學必修一的時候就學過:(雖然我還沒有高一)
注意我們是模意義下的對數,聯想到原根的一些性質(我保證這坑我以后會補),我們只需以原根為底取對數即可
令\(A=\log_g a \%m,B=\log _g b\%m,C=\log_g c\%m\),於是式子就被我們改寫成:
好了,我今天就分享到這里了。
如有不足敬請指正,謝謝!
參考資料:
1.pmt巨佬的講義(%%%pmt巨佬)
2.OI wiki
3.Miskcoo's space
