模板:基於NTT的多項式操作
因爲實在是太多東西啦,所以就全部整理了以下,持續更新ing~
前置知識:NTT
順便說一句,代碼採用重載vector封裝的形式(因爲懶得自己開結構體)
下面是幾個已經封裝的基礎代碼,爲了方便瀏覽,先貼出來:
typedef std::vector<int> VI;
int fix(int x) {return (x >> 31 & P) + x;}
int Pow(int x, int k) {
int r = 1;
for(;k; k >>= 1, x = 1LL * x * x % P)
if(k & 1)
r = 1LL * r * x % P;
return r;
}
int Inv(int x) {return Pow(x, P - 2);}
void Pre(int m) {
int x = 0; L = 1;
for(;(L <<= 1) < m; ++x) ;
for(int i = 1;i < L; ++i)
R[i] = R[i >> 1] >> 1 | (i & 1) << x;
int wn = Pow(3, (P - 1) / L); w[0] = 1;
for(int i = 1;i < L; ++i)
w[i] = 1LL * w[i - 1] * wn % P;
InvL = Inv(L);
}
void NTT(int *F) {
for(int i = 0;i < L; ++i)
if(R[i] > i)
std::swap(F[i], F[R[i]]);
for(int i = 1, d = L >> 1; i < L; i <<= 1, d >>= 1)
for(int j = 0;j < L; j += i << 1) {
int *l = F + j, *r = F + i + j, *p = w, tp;
for(int k = i; k--; ++l, ++r, p += d)
tp = 1LL * *p * *r % P, *r = (*l - tp) % P, *l = (*l + tp) % P;
}
}
void Fill(const VI &a, int *A, int m) {
m = std::min(m, (int)a.size());
for(int i = 0;i < m; ++i)
A[i] = a[i];
for(int i = m; i < L; ++i)
A[i] = 0;
}
void Fill(int *A, int *B, int m) {
for(int i = 0;i < m; ++i)
B[i] = A[i];
for(int i = m; i < L; ++i)
B[i] = 0;
}
VI operator * (const VI &a, const VI &b) {
int m = a.size() + b.size() - 1;
static VI c; static int A[N], B[N];
c.resize(m, 0); Pre(m);
Fill(a, A, a.size());
Fill(b, B, b.size());
NTT(A); NTT(B);
for(int i = 0;i < L; ++i)
A[i] = 1LL * A[i] * B[i] % P;
NTT(A);
for(int i = 0;i < m; ++i)
c[i] = fix(1LL * A[L - i & L - 1] * InvL % P);
return c;
}
VI operator - (const VI &a, const VI &b) {
int n = std::max(a.size(), b.size());
static VI c; c.resize(n, 0);
for(int i = 0;i < a.size(); ++i)
c[i] = a[i];
for(int i = 0;i < b.size(); ++i)
c[i] = fix(c[i] - b[i]);
return c;
}
迭代相關:多項式求逆,開根,求exp
這些我們熟悉的多項式操作都是採用迭代法解決,既然都是迭代法,必有其共性,因此下面便脫離傳統的證明方法,採用牛頓迭代法來進行一波推導
知識點:泰勒展開
下面首先介紹一下泰勒展開:
對於一個函數,其在處的泰勒公式如下:
知識點:牛頓迭代
這是一種逼近零點的科技。(高中數學課本上有,文化課選手應該瞭解)
對於一個函數,我們考慮以作爲起點逼近其零點。
方法就是,作處的切線,設其交軸於,以作爲下一次的起點重複上述過程不斷逼近零點。
推一推式子:
令,得到
有沒有發現式子其實挺熟悉的?
考慮直線方程:
發現直線方程就是在處的一階泰勒展開。
形式化地,我們可以這樣描述牛頓迭代法:
用函數在處的一階展開方程的零點作爲下一次迭代的起點,這樣的迭代法就是牛頓迭代法。
這個東西對於多項式有什麼好處呢?
我們不難發現定義一個以多項式爲自變量的函數,泰勒公式仍然成立。
我們首先將題目的形式轉化爲求
迭代法的一般考慮步驟是:
已知
求
考慮在處的泰勒展開逼近
事實上,我們有
那麼
這一步推導是因爲的最低次冪是,平方之後最低次冪就變成了
於是我們有
也就是說,爲牛頓迭代的結果。
根據牛頓迭代的結果,我們可以直接得到:
基於這個結論,我們可以採用類似倍增的方法來求得最終需要的
多項式求逆
已知,求
原式等價於
注意構造的時候一般讓在常數位置上,這樣求導的時候直接消掉
構造函數
求導
假設求得
由剛纔的結論
初值直接費馬小定理。
結合多項式乘法,結束了!
複雜度由主定理得到
代碼
VI Inv(const VI &a, const int m) { //多項式求逆
static int A[N], B[N], C[N];
for(int i = 0;i < m; ++i)
A[i] = 0;
A[0] = Inv(a[0]);
int n = 1;
for(;n < m;) {
Pre(n << 2);
Fill(a, B, n << 1);
Fill(A, C, n);
NTT(B); NTT(C);
for(int i = 0;i < L; ++i)
B[i] = 1LL * B[i] * C[i] % P * C[i] % P;
NTT(B);
n <<= 1;
for(int i = 0;i < n; ++i)
A[i] = ((A[i] << 1) - 1LL * B[L - i & L - 1] * InvL) % P;
}
static VI c; c.resize(m);
for(int i = 0;i < m; ++i)
c[i] = fix(A[i]);
return c;
}
多項式開根
已知,求
同樣地讓在常數位上
構造函數
求導
迭代
初值一般爲1,否則的話要採用二次剩餘或者原根BSGS
結合多項式求逆,多項式乘法可以解決,複雜度同上。
代碼
VI Sqrt(const VI &a) { //多項式開根
static VI b, c;
b.resize(1, 1); //僅僅在a0=1的時候成立,否則的話要用BSGS
int n = 1, m = a.size();
for(;n < m;) {
n <<= 1;
c = Inv(b, n);
b = b * b; b.resize(n);
for(int i = 0;i < std::min(n, m); ++i)
b[i] = 1LL * (b[i] + a[i]) * inv2 % P;
b = b * c;
b.resize(n);
}
b.resize(m);
return b;
}
多項式求exp
前置知識:多項式求ln
這個東西簡單很多
兩邊同時對求導(注意和之前對多項式求導不一樣!)
結合求逆再積分回去即可,複雜度同。
求導積分就不多說了吧!
VI deri(const VI &a) { //多項式求導
static VI c;
if(a.size() == 1) return VI(1, 0);
c.resize(a.size() - 1);
for(int i = 0;i < a.size() - 1; ++i)
c[i] = 1LL * a[i + 1] * (i + 1) % P;
return c;
}
VI inter(const VI &a) { //多項式積分
static VI c; c.resize(a.size() + 1);
c[0] = 0;
for(int i = 0;i < a.size(); ++i)
c[i + 1] = 1LL * a[i] * Inv(i + 1) % P;
return c;
}
VI Ln(const VI &a, const int m) { //多項式求Ln
static VI c;
c = deri(a) * Inv(a, m - 1);
c.resize(m - 1);
return inter(c);
}
接下來是求exp
已知,求
構造函數
求導
迭代
初值一般爲1,否則無意義。
結合多項式求逆,多項式求Ln,多項式乘法可以解決,複雜度同上。
代碼
VI Exp(const VI &a, const int m) { //多項式求exp
static VI f, g; f.resize(1, 1);
int n = 1, sz = a.size();
for(;n < m;) {
n <<= 1; g = Ln(f, n);
for(int i = 0;i < n; ++i)
g[i] = i < sz ? fix(a[i] - g[i]) : fix(-g[i]);
(++g[0]) %= P;
f = f * g; f.resize(n);
}
return f;
}