记录多项式以及一些相关数论前置芝士qwq
由 n n n 个点 ( x i , y i ) (x_i,y_i) (xi,yi) 可以唯一确定一个 n − 1 n-1 n−1 次多项式 f ( x ) f(x) f(x),要求出 f ( k ) f(k) f(k) 的值可以 O ( n 3 ) O(n^3) O(n3) 高斯消元,而拉格朗日插值可以做到 O ( n 2 ) O(n^2) O(n2) 求解。
由 f ( x ) = a 0 + a 1 x + a 2 x 2 + ⋯ + a n x n f(x)=a_0+a_1x+a_2x^2+\cdots +a_nx^n f(x)=a0+a1x+a2x2+⋯+anxn, f ( k ) = a 0 + a 1 k + a 2 k 2 + ⋯ + a n k n f(k)=a_0+a_1k+a_2k^2+\cdots+a_nk^n f(k)=a0+a1k+a2k2+⋯+ankn 得 f ( x ) − f ( k ) = a 1 ( x − k ) + a 2 ( x 2 − k 2 ) + ⋯ + a n ( x n − k n ) ≡ 0 ( m o d x − k ) f(x)-f(k)=a_1(x-k)+a_2(x^2-k^2)+\cdots+a_n(x^n-k^n)\equiv0\pmod{x-k} f(x)−f(k)=a1(x−k)+a2(x2−k2)+⋯+an(xn−kn)≡0(modx−k),则 f ( x ) ≡ f ( k ) ( m o d x − k ) f(x)\equiv f(k)\pmod{x-k} f(x)≡f(k)(modx−k)。
把 n n n 个点都带入方程,可得同余方程组 f ( k ) ≡ y i ( m o d k − x i ) ) ( 1 ≤ i ≤ n ) f(k)\equiv y_i\pmod{k-x_i})\ (1\le i\le n) f(k)≡yi(modk−xi)) (1≤i≤n)
套用 CRT 的求解,令 M = ∏ i = 1 n ( k − x [ i ] ) , m i = M k − x i M=\prod\limits_{i=1}^n(k-x[i]),m_i=\dfrac{M}{k-x_i} M=i=1∏n(k−x[i]),mi=k−xiM,则有:
f ( k ) = ∑ i = 1 n y i × m i × m i − 1 f(k)=\sum\limits_{i=1}^ny_i\times m_i\times m_i^{-1} f(k)=i=1∑nyi×mi×mi−1
= ∑ i = 1 n y i × ∏ j ≠ i ( k − x j ) × m i − 1 \quad\quad\ =\sum\limits_{i=1}^ny_i\times\prod\limits_{j\ne i}(k-x_j)\times m_i^{-1} =i=1∑nyi×j=i∏(k−xj)×mi−1
= ∑ i = 1 n y i × ∏ j ≠ i ( k − x i + x i − x j ) × ∏ j ≠ i 1 x i − x j \quad\quad\ =\sum\limits_{i=1}^ny_i\times\prod\limits_{j\ne i}(k-x_i+x_i-x_j)\times \prod\limits_{j\ne i}\dfrac{1}{x_i-x_j} =i=1∑nyi×j=i∏(k−xi+xi−xj)×j=i∏xi−xj1
= ∑ i = 1 n y i × ∏ j ≠ i k − x j x i − x j \quad\quad\ =\sum\limits_{i=1}^ny_i\times\prod\limits_{j\ne i}\dfrac{k-x_j}{x_i-x_j} =i=1∑nyi×j=i∏xi−xjk−xj
中间在 k − x i k-x_i k−xi 剩余系中转化了一下逆元,最后求出的就是拉格朗日插值的式子,在模意义和非模意义下通用。
对于 ∀ i = 1 n x i = i \forall_{i=1}^n x_i=i ∀i=1nxi=i,可以将拉格朗日插值的式子进一步简化:
{ ∏ j ≠ i ( i − j ) = ( − 1 ) n − i ( i − 1 ) ! ( n − i ) ! ∏ j ≠ i ( k − j ) = ∏ j = 1 n ( k − j ) k − i \begin{cases}\prod\limits_{j\ne i}(i-j)=(-1)^{n-i}(i-1)!\ (n-i)!\\\prod\limits_{j\ne i}(k-j)=\dfrac{\prod\limits_{j=1}^n (k-j)}{k-i}\end{cases} ⎩ ⎨ ⎧j=i∏(i−j)=(−1)n−i(i−1)! (n−i)!j=i∏(k−j)=k−ij=1∏n(k−j)
⟹ f ( k ) = ∑ i = 1 n y i × ( − 1 ) n − i ∏ j = 1 n ( k − j ) ( i − 1 ) ! ( n − i ) ! ( k − i ) \implies f(k)=\sum\limits_{i=1}^n y_i\times (-1)^{n-i}\ \dfrac{\prod\limits_{j=1}^n (k-j)}{(i-1)!\ (n-i)!(k-i)} ⟹f(k)=i=1∑nyi×(−1)n−i (i−1)! (n−i)!(k−i)j=1∏n(k−j)
分子的和分母都可以预处理,于是可以做到 O ( n ) O(n) O(n)。
永远记不住的线性求逆元柿子: i n v [ i ] = ( p − p / i ) × i n v [ p % i ] inv[i]=(p-p/i)\times inv[p\%i] inv[i]=(p−p/i)×inv[p%i]
当需要新加入一个点的时候,用朴素的拉插式子算是 O ( n 2 ) O(n^2) O(n2) 的,但可以推柿子做到 O ( n ) O(n) O(n):
f ( k ) = ∑ i = 1 n y i × ∏ j ≠ i k − x j x i − x j f(k)=\sum\limits_{i=1}^ny_i\times\prod\limits_{j\ne i}\dfrac{k-x_j}{x_i-x_j} f(k)=i=1∑nyi×j=i∏xi−xjk−xj
= ∑ i = 1 n y i × ∏ j = 1 n k − x j ( k − x i ) ∏ j ≠ i ( x i − x j ) \quad\quad\ =\sum\limits_{i=1}^ny_i\times\dfrac{\prod\limits_{j=1}^nk-x_j}{(k-x_i)\prod\limits_{j\ne i}(x_i-x_j)} =i=1∑nyi×(k−xi)j=i∏(xi−xj)j=1∏nk−xj
= ∏ i = 1 n ( k − x i ) ∑ i = 1 n y i ( k − x i ) ∏ j ≠ i ( x i − x j ) \quad\quad\ =\prod\limits_{i=1}^n(k-x_i)\sum\limits_{i=1}^n\dfrac{y_i}{(k-x_i)\prod\limits_{j\ne i}(x_i-x_j)} =i=1∏n(k−xi)i=1∑n(k−xi)j=i∏(xi−xj)yi
设 g ( k ) = ∏ i = 1 n ( k − x i ) g(k)=\prod\limits_{i=1}^n(k-x_i) g(k)=i=1∏n(k−xi), t i = ∏ j ≠ i 1 x i − x j t_i=\prod\limits_{j\ne i}\dfrac{1}{x_i-x_j} ti=j=i∏xi−xj1,则 f ( k ) = g ( k ) ∑ i = 1 n y i t i k − x i f(k)=g(k)\sum\limits_{i=1}^n\dfrac{y_it_i}{k-x_i} f(k)=g(k)i=1∑nk−xiyiti,每次新增一个点 n n n 可以 O ( 1 ) O(1) O(1) 更新 g ( k ) g(k) g(k), O ( n ) O(n) O(n) 求 t n t_n tn, O ( n ) O(n) O(n) 更新 t 1 t_1 t1 到 t n − 1 t_{n-1} tn−1,再 O ( n ) O(n) O(n) 更新 f ( k ) f(k) f(k),这个点就插入成功了。
前置芝士:
快速傅里叶变换(FFT)就是一种 O ( n log n ) O(n\log n) O(nlogn) 将系数表示转化为点值表示,进而求出两个多项式的乘积的方法。
傅里叶把多项式先填到最高次项(系数可以为 0 0 0)为 n = 2 k n=2^k n=2k 的形式,然后规定点值表示中的点为 n n n 个模长为 1 1 1 的复数,使得这些复数在平面直角坐标系中把单位圆 n n n 等分。
为了方便,将这些点以 ( 1 , 0 ) (1,0) (1,0) 为起点逆时针编号为 0 0 0 到 n − 1 n-1 n−1,第 k k k 个点的坐标为 ( cos i n 2 π , sin i n 2 π ) (\cos\frac{i}{n}2π,\sin\frac{i}{n}2π) (cosni2π,sinni2π),表示的复数为 w n k w_n^k wnk,其中 w n 1 w_n^1 wn1 叫作 n n n 次单位根。
这种点值选取的方式就是离散傅里叶变换(DFT)。
根据复数和三角函数的一些知识,得到(设 a a a 为常数):
在 DFT 的基础上,我们推一些柿子:将 f ( x ) = a 0 + a 1 x + a 2 x 2 + ⋯ + a n x n − 1 f(x)=a_0+a_1x+a_2x^2+\cdots +a_nx^{n-1} f(x)=a0+a1x+a2x2+⋯+anxn−1 按 x x x 的幂次的奇偶拆开并化简:
f ( x ) = ( a 0 + a 2 x 2 + ⋯ + a n − 2 x n − 2 ) + ( a 1 x + a 3 x 3 + ⋯ + a n − 1 x n − 1 ) f(x)=(a_0+a_2x^2+\cdots+a_{n-2}x^{n-2})+(a_1x+a_3x^3+\cdots+a_{n-1}x^{n-1}) f(x)=(a0+a2x2+⋯+an−2xn−2)+(a1x+a3x3+⋯+an−1xn−1)
= ( a 0 + a 2 ( x 1 ) 2 + ⋯ + a n − 2 ( x n − 2 2 ) 2 ) + x ( a 1 + a 3 ( x 1 ) 2 + ⋯ a n − 2 ( x n − 2 2 ) 2 ) \quad\quad\ =(a_0+a_2(x^1)^2+\cdots+a_{n-2}(x^{\frac{n-2}{2}})^2)+x(a_1+a_3(x^1)^2+\cdots a_{n-2}(x^{\frac{n-2}{2}})^2) =(a0+a2(x1)2+⋯+an−2(x2n−2)2)+x(a1+a3(x1)2+⋯an−2(x2n−2)2)
令 g ( x ) = a 0 + a 2 x + ⋯ + a n − 2 x n − 2 2 g(x)=a_0+a_2x+\cdots+a_{n-2}x^{\frac{n-2}{2}} g(x)=a0+a2x+⋯+an−2x2n−2, h ( x ) = a 1 + a 3 x + ⋯ + a n − 1 x n − 2 2 h(x)=a_1+a_3x+\cdots+a_{n-1}x^{\frac{n-2}{2}} h(x)=a1+a3x+⋯+an−1x2n−2,则 f ( x ) = g ( x 2 ) + x h ( x 2 ) f(x)=g(x^2)+x\ h(x^2) f(x)=g(x2)+x h(x2)。
把 DFT 中选择的点代入:
那么这就是分治了,但是由于大常数选手的递归版跑得过慢等原因,代码就不放了/kk
递归本身常数就是比较大的,考虑能不能优化。模拟一下每次选奇偶点分前后两半的过程,发现如果把 i i i 的二进制表示前后翻转所得的数为 j j j,那么递归后原本在位置 i i i 的数会跑到位置 j j j,这就是位逆序变换。因此可以预处理出每个数的最终位置,一点点向上合并即可。
实现的时候,复数可以用 STL 的 complex,预处理 i i i 翻转二进制位所得的数 p o s i pos_i posi,就可以写出 FFT 了:
inline void fft(com *a,com *omg){
ff(i,0,n-1) if(pos[i]<i) swap(a[pos[i]],a[i]);
for(int len=2;len<=n;len<<=1){
int m=len>>1;
for(int i=0;i<n;i+=len) ff(j,0,m-1){
com tmp=a[i+j+m]*omg[n/len*j];
a[i+j+m]=a[i+j]-tmp,a[i+j]+=tmp;
}
}
}
我们用 FFT 成功加速了系数表示转点值表示的过程,但目的是想加速多项式乘法。FFT 是 O ( n log n ) O(n\log n) O(nlogn) 的,两个点值表示的多项式相乘是 O ( n ) O(n) O(n) 的,想求乘法我们还需要快速将点值表示转化为系数表示,就需要离散傅里叶逆变换(IDFT)。
假设我们对 f ( x ) f(x) f(x) 进行 DFT之后得到了一系列纵坐标 ( y 0 , y 1 , ⋯ , y n − 1 ) (y_0,y_1,\cdots,y_{n-1}) (y0,y1,⋯,yn−1),构造函数 g ( x ) = y 0 + y 1 x + ⋯ + y n − 1 x n − 1 g(x)=y_0+y_1x+\cdots+y_{n-1}x^{n-1} g(x)=y0+y1x+⋯+yn−1xn−1,把 w n 0 , w n − 1 , ⋯ , w n − ( n − 1 ) w_n^0,w_n^{-1},\cdots,w_n^{-(n-1)} wn0,wn−1,⋯,wn−(n−1) 代入得到 ( z 0 , z 1 , ⋯ , z n − 1 ) (z_0,z_1,\cdots,z_{n-1}) (z0,z1,⋯,zn−1):
z k = ∑ i = 0 n − 1 y i × ( w n − k ) i z_k=\sum\limits_{i=0}^{n-1}y_i\times(w_n^{-k})^i zk=i=0∑n−1yi×(wn−k)i
= ∑ i = 0 n − 1 ∑ j = 0 n − 1 a j × ( w n i ) j × ( w n − k ) i \quad\ =\sum\limits_{i=0}^{n-1}\sum\limits_{j=0}^{n-1}a_j\times (w_n^i)^j\times(w_n^{-k})^i =i=0∑n−1j=0∑n−1aj×(wni)j×(wn−k)i
= ∑ j = 0 n − 1 a j ∑ i = 0 n − 1 ( w n j − k ) i \quad\ =\sum\limits_{j=0}^{n-1} a_j\sum\limits_{i=0}^{n-1}(w_n^{j-k})^i =j=0∑n−1aji=0∑n−1(wnj−k)i
而 ∑ i = 0 n − 1 ( w n j − k ) i = { j = k n j ≠ k ( w n j − k ) n − 1 w n j − k − 1 = 0 \sum\limits_{i=0}^{n-1}(w_n^{j-k})^i=\begin{cases}j=k\quad n\\j\ne k\quad \dfrac{(w_n^{j-k})^n-1}{w_n^{j-k}-1}=0\end{cases} i=0∑n−1(wnj−k)i=⎩ ⎨ ⎧j=knj=kwnj−k−1(wnj−k)n−1=0
故当且仅当 j = k j=k j=k, z k = a j × n z_k=a_j\times n zk=aj×n。
所以对 g ( x ) g(x) g(x) 代入 w n 0 , w n − 1 , ⋯ , w n − ( n − 1 ) w_n^0,w_n^{-1},\cdots,w_n^{-(n-1)} wn0,wn−1,⋯,wn−(n−1) 之后求 DFT 的结果除以 n n n 就可以得到 f ( x ) f(x) f(x) 的各项系数,而形如 w n − 1 w_n^{-1} wn−1 的就是共轭复数,可以用 complex 所带的函数 c o n j ( ) conj() conj() 求出。
所以学了一大顿之后就又可以写高精乘法了:
#include
#define ll long long
#define ff(i,s,e) for(int i=(s);i<=(e);++i)
using namespace std;
inline int read(){
int x=0,f=1;
char ch=getchar();
while(ch>'9'||ch<'0'){if(ch=='-') f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x*f;
}
const int N=1<<21;
const double pai=acos(-1);
int len1,len2,n=1,m,ans[N],pos[N];
char s[N];
typedef complex<double> com;
com a[N],b[N],omg[N],inv[N];
inline void fft(com *a,com *omg){
ff(i,0,n-1) if(pos[i]<i) swap(a[pos[i]],a[i]);//注意不要重复交换
for(int len=2;len<=n;len<<=1){
int m=len>>1;
for(int i=0;i<n;i+=len) ff(j,0,m-1){
com tmp=a[i+j+m]*omg[n/len*j];
a[i+j+m]=a[i+j]-tmp,a[i+j]+=tmp;
}
}
}
signed main(){
scanf("%s",s),len1=strlen(s);
ff(i,0,len1-1) a[i].real(s[len1-i-1]-'0');
scanf("%s",s),len2=strlen(s);
ff(i,0,len2-1) b[i].real(s[len2-i-1]-'0');
while(n<len1+len2) n<<=1,++m;
ff(i,0,n-1){
omg[i]=com(cos(2*pai*i/n),sin(2*pai*i/n));
inv[i]=conj(omg[i]);
int tmp=0;
ff(j,0,m-1) if(i&(1<<j)) tmp|=(1<<m-j-1);
pos[i]=tmp;
}
fft(a,omg),fft(b,omg);
ff(i,0,n-1) a[i]*=b[i];
fft(a,inv);
ff(i,0,n-1){
ans[i]+=floor(a[i].real()/n+0.5);//注意四舍五入
ans[i+1]+=ans[i]/10,ans[i]%=10;
}
while(n>1&&ans[n-1]==0) --n;
for(int i=n-1;i>=0;--i) putchar(ans[i]+'0');
putchar('\n');
}
使得 a n ≡ 1 ( m o d m ) a^n\equiv1\pmod{m} an≡1(modm) 成立的最小正整数 n n n 叫做 a a a 模 m m m 的阶,符号 δ m ( a ) \delta_m(a) δm(a)。
一些性质:
若 gcd ( a , m ) = 1 , δ m ( a ) = ϕ ( m ) \gcd(a,m)=1,\delta_m(a)=\phi(m) gcd(a,m)=1,δm(a)=ϕ(m),则 a a a 是 m m m 的原根。
暂时就会这么多,NTT 不知道哪年能学会,留坑代填()
为什么我的 FFT 常数这么大啊/fn,未来如果有更优的写法会更新代码。