记录多项式以及一些相关数论前置芝士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−xi),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 剩余系中转化了一下逆元,最后求出的就是拉格朗日插值的式子,在模意义和非模意义下通用。
x i x_i 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),这个点就插入成功了。
快速傅里叶变换
前置芝士:
- 多项式的系数表示法:即常见的函数形式,形如 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;
- 多项式的点值表示法:对于一个 n − 1 n-1 n−1 次多项式, n n n 个不同的 x x x 代入能得到 n n n 个不同的点 ( x i , y i ) (x_i,y_i) (xi,yi),这 n n n 个点唯一确定了该多项式;
- 多项式的系数表示和点值表示可以相互转化。
快速傅里叶变换(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 k n 2 π , sin k n 2 π ) (\cos\frac{k}{n}2π,\sin\frac{k}{n}2π) (cosnk2π,sinnk2π),表示的复数为 w n k w_n^k wnk,其中 w n 1 w_n^1 wn1 叫作 n n n 次单位根。
这种点值选取的方式就是离散傅里叶变换(DFT)。
根据复数和三角函数的一些知识,得到(设 a a a 为常数):
- w n k = w a n a k w_n^k=w_{an}^{ak} wnk=wanak
- w n k = − w n k + n 2 w_n^k=-w_{n}^{k+\frac{n}{2}} wnk=−wnk+2n
- w n k = w n k + a n w_n^k=w_n^{k+an} wnk=wnk+an
快速傅里叶变换
在 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 中选择的点代入:
- k < n 2 k<\dfrac{n}{2} k<2n, f ( w n k ) = g ( w n 2 k ) + w n k h ( w n 2 k ) = g ( w n 2 k ) + w n k h ( w n 2 k ) f(w_n^k)=g(w_n^{2k})+w_n^k\ h(w_n^{2k})=g(w_\frac{n}{2}^k)+w_n^k\ h(w_\frac{n}{2}^k) f(wnk)=g(wn2k)+wnk h(wn2k)=g(w2nk)+wnk h(w2nk)
- k ≥ n 2 k\ge \dfrac{n}{2} k≥2n, f ( w n k ) = g ( w n 2 k + n ) + w n k + n 2 h ( w n 2 k + n ) = g ( w n 2 k ) − w n k h ( w n 2 k ) f(w_n^k)=g(w_n^{2k+n})+w_n^{k+\frac{n}{2}}h(w_n^{2k+n})=g(w_\frac{n}{2}^k)-w_n^kh(w_\frac{n}{2}^k) f(wnk)=g(wn2k+n)+wnk+2nh(wn2k+n)=g(w2nk)−wnkh(w2nk)
那么这就是分治了,但是由于大常数选手的递归版跑得过慢等原因,代码就不放了/kk
递归本身常数就是比较大的,考虑能不能优化。模拟一下每次选奇偶点分前后两半的过程,发现如果把 i i i 的二进制表示前后翻转所得的数为 j j j,那么递归后原本在位置 i i i 的数会跑到位置 j j j,这就是位逆序变换。因此可以预处理出每个数的最终位置,一点点向上合并即可。
实现的时候,复数可以用 STL 的 complex,复数建议手写因为 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 的就是共轭复数。
学了一大顿之后就又可以写高精乘法了:(亲测不预处理 w n k w_n^k wnk 会更快)
#include<bits/stdc++.h>
#define ll long long
#define rep(i,s,e) for(int i=(s);i<=(e);++i)
#define Rep(i,s,e) for(int i=(e);i>=(s);--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 n1,n2,n=1,m,pos[N],ans[N];
char s1[N],s2[N];
struct qwq{
double x,y;
}a[N],b[N];
inline qwq operator + (const qwq &a,const qwq &b){
return {a.x+b.x,a.y+b.y};
}
inline qwq operator - (const qwq &a,const qwq &b){
return {a.x-b.x,a.y-b.y};
}
inline qwq operator * (const qwq &a,const qwq &b){
return {a.x*b.x-a.y*b.y,a.x*b.y+b.x*a.y};
}
inline void fft(qwq a[],int flag){
rep(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;
qwq x={cos(2*pai/len),flag*sin(2*pai/len)};
for(int i=0;i<n;i+=len){
qwq now={1,0};
rep(j,0,m-1){
qwq tmp=a[i+j+m]*now;
a[i+j+m]=a[i+j]-tmp,a[i+j]=a[i+j]+tmp;
now=now*x;
}
}
}
}
signed main(){
scanf("%s",s1),n1=strlen(s1);
scanf("%s",s2),n2=strlen(s2);
rep(i,0,n1-1) a[i].x=s1[n1-i-1]-'0';
rep(i,0,n2-1) b[i].x=s2[n2-i-1]-'0';
while(n<n1+n2) n<<=1,++m;
rep(i,0,n-1){
int tmp=0;
rep(j,0,m-1) if(i&(1<<j)) tmp|=(1<<m-j-1);
pos[i]=tmp;
}
fft(a,1),fft(b,1);
rep(i,0,n-1) a[i]=a[i]*b[i];
fft(a,-1);
rep(i,0,n-1){
ans[i]+=floor(a[i].x/n+0.5);
if(ans[i]>=10) ans[i+1]+=ans[i]/10,ans[i]%=10;
}
while(n>1&&ans[n-1]==0) --n;
Rep(i,0,n-1) putchar(ans[i]+'0');
putchar('\n');
}
快速数论变换
感觉 OI Wiki 上原根之外的前置知识都可以直接背下来?
原根
阶:使得 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)。
一些性质:
- ∀ a n ≡ 1 ( m o d m ) , δ m ( a ) ∣ n ⟹ δ m ( a ) ∣ ϕ ( m ) \forall a^n\equiv 1\pmod{m},\delta_m(a)\mid n\implies\delta_m(a)\mid\phi(m) ∀an≡1(modm),δm(a)∣n⟹δm(a)∣ϕ(m)
- ∀ i , j ∈ [ 1 , δ m ( a ) ] , i ≠ j a i ≢ a j ( m o d m ) \forall_{i,j\in[1,\delta_m(a)],i\ne j}\ a^i\not\equiv a^j\pmod{m} ∀i,j∈[1,δm(a)],i=j ai≡aj(modm)
- gcd ( a , m ) = 1 , δ m ( a k ) = δ m ( a ) gcd ( k , δ m ( a ) ) \gcd(a,m)=1,\delta_m(a^k)=\dfrac{\delta_m(a)}{\gcd(k,\delta_m(a))} gcd(a,m)=1,δm(ak)=gcd(k,δ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 的原根。
- 判定定理: ∀ p ∣ ϕ ( m ) a ϕ ( m ) p ≢ 1 ( m o d m ) ⟺ a \forall_{p\mid \phi(m)} a^{\frac{\phi(m)}{p}}\not\equiv1\pmod{m}\iff a ∀p∣ϕ(m)apϕ(m)≡1(modm)⟺a 是 m m m 的原根;
- 存在定理:只有 2 , 4 , p a , 2 p a 2,4,p^a,2p^a 2,4,pa,2pa 才存在原根,其中 p p p 为奇素数;
- 原根个数:若 m m m 有原根,则其原根个数为 ϕ ( ϕ ( m ) ) \phi(\phi(m)) ϕ(ϕ(m));
- m m m 的最小原根 g g g 不超过 m 1 4 m^{\frac{1}{4}} m41,所有其它原根均为 g k ( gcd ( k , ϕ ( m ) = 1 ) ) g^k\ (\gcd(k,\phi(m)=1)) gk (gcd(k,ϕ(m)=1))。
快速数论变换
FFT 的缺点在于浮点数运算会产生精度问题,实际上在模数是一些特定质数 p = k × 2 n + 1 p=k\times 2^n+1 p=k×2n+1 时可以将单位根替换为原根进行计算。这些质数被称为 NTT 模数,常见的是 998244353 998244353 998244353,原根为 g = 3 g=3 g=3。
根据原根的性质可知 g k n ≡ 1 ( m o d p ) g^{kn}\equiv 1\pmod p gkn≡1(modp) 且 i ∈ [ 0 , p ) i\in[0,p) i∈[0,p) 范围内 g i m o d p g_i\bmod p gimodp 两两不同,于是单位根满足的性质原根全部满足,就可以类似 FFT 来求解,IDFT 的求共轭复数变为求逆元即可。
模板代码:
#include<bits/stdc++.h>
#define ll long long
#define rep(i,s,e) for(int i=(s);i<=(e);++i)
#define Rep(i,s,e) for(int i=(e);i>=(s);--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=3e6+5,mod=998244353;
int n1,n2,m,n=1,p[N];
ll a[N],b[N];
inline ll ksm(ll x,int y){
ll res=1;
for(;y;y>>=1){
if(y&1) res=res*x%mod;
x=x*x%mod;
}
return res;
}
inline void NTT(ll a[],int flag){
rep(i,0,n-1) if(p[i]<i) swap(a[i],a[p[i]]);
for(int len=2;len<=n;len<<=1){
int m=len>>1;ll x=ksm(3,(mod-1)/len);
if(flag==-1) x=ksm(x,mod-2);
for(int i=0;i<n;i+=len){
ll now=1;
rep(j,0,m-1){
ll u=a[i+j],v=a[i+j+m]*now%mod;
a[i+j]=(u+v)%mod,a[i+j+m]=(u-v+mod)%mod;
now=now*x%mod;
}
}
}
if(flag==-1){
int inv=ksm(n,mod-2);
rep(i,0,n-1) a[i]=a[i]*inv%mod;
}
}
signed main(){
n1=read(),n2=read();
rep(i,0,n1) a[i]=read();
rep(i,0,n2) b[i]=read();
while(n<(n1+n2+2)) n<<=1,++m;
rep(i,1,n){
int now=0;
rep(j,0,m-1) if(i&(1<<j)) now|=(1<<m-j-1);
p[i]=now;
}
NTT(a,1),NTT(b,1);
rep(i,0,n-1) a[i]=a[i]*b[i]%mod;
NTT(a,-1);
rep(i,0,n1+n2) printf("%lld ",a[i]);
putchar('\n');
}
快速莫比乌斯变换 & 快速沃尔什变换
感谢可爱的猫告诉我基于分治的 and 和 or 卷积叫 FWT,基于 dp 卷积的叫 FMT。好强大的樱雪喵。
与卷积、或卷积和异或卷积的本质都是仿照 FFT 将多项式 A , B A,B A,B 转化为点值表示 F W T [ A ] , F W T [ B ] FWT[A],FWT[B] FWT[A],FWT[B],根据 F W T [ C ] = F W T [ A ] × F W T [ B ] FWT[C]=FWT[A]\times FWT[B] FWT[C]=FWT[A]×FWT[B] 得到 F W T [ C ] FWT[C] FWT[C],再逆运算得到多项式 C C C(以下称逆运算为 U F W T [ A ] UFWT[A] UFWT[A])
OR 卷积
要求 c k = ∑ i ∣ j = k a i b j c_k=\sum\limits_{i|j=k} a_ib_j ck=i∣j=k∑aibj,考虑根据 i ∣ k = k , j ∣ k = k ⟺ ( i ∣ j ) ∣ k = k i|k=k,j|k=k\iff (i|j)|k=k i∣k=k,j∣k=k⟺(i∣j)∣k=k,设 F W T [ A ] k = ∑ i ∣ k = k a i FWT[A]_k=\sum\limits_{i|k=k}a_i FWT[A]k=i∣k=k∑ai。
考虑分治,把长为
2
n
2^n
2n 的数组写成分成前后两段,前一段二进制最高位全为
0
0
0,后一段全为
1
1
1,更低位前后两段的情况是一样的。把这两段分别记为
A
0
A0
A0 和
A
1
A1
A1,则
A
1
A1
A1 的子集也包括最高位为
0
0
0 的子集,即:
F
W
T
[
A
]
=
m
e
r
g
e
(
F
W
T
[
A
0
]
,
F
W
T
[
A
0
]
+
F
W
T
[
A
1
]
)
U
F
W
T
[
A
]
=
m
e
r
g
e
(
U
F
W
T
[
A
0
]
,
U
F
W
T
[
A
1
]
−
U
F
W
T
[
A
0
]
)
FWT[A]=merge(FWT[A0],FWT[A0]+FWT[A1])\\UFWT[A]=merge(UFWT[A0],UFWT[A1]-UFWT[A0])
FWT[A]=merge(FWT[A0],FWT[A0]+FWT[A1])UFWT[A]=merge(UFWT[A0],UFWT[A1]−UFWT[A0])
AND 卷积
要求 c k = ∑ i & j = k a i b j c_k=\sum\limits_{i\&j=k} a_ib_j ck=i&j=k∑aibj,考虑根据 i & k = k , j & k = k ⟺ ( i & j ) & k = k i\&k=k,j\&k=k\iff (i\&j)\&k=k i&k=k,j&k=k⟺(i&j)&k=k,设 F W T [ A ] = ∑ i & k = k a i FWT[A]=\sum\limits_{i\&k=k}a_i FWT[A]=i&k=k∑ai。
同理 OR 卷积可知
F
W
T
[
A
]
=
m
e
r
g
e
(
F
W
T
[
A
0
]
+
F
W
T
[
A
1
]
,
F
W
T
[
A
0
]
)
U
F
W
T
[
A
]
=
m
e
r
g
e
(
U
F
W
T
[
A
0
]
−
U
F
W
T
[
A
1
]
,
U
F
W
T
[
A
1
]
)
FWT[A]=merge(FWT[A0]+FWT[A1],FWT[A0])\\UFWT[A]=merge(UFWT[A0]-UFWT[A1],UFWT[A1])
FWT[A]=merge(FWT[A0]+FWT[A1],FWT[A0])UFWT[A]=merge(UFWT[A0]−UFWT[A1],UFWT[A1])
XOR 卷积
要求 c k = ∑ i xor j = k a i b j c_k=\sum\limits_{i\operatorname{xor}j=k}a_ib_j ck=ixorj=k∑aibj,设 d ( x ) d(x) d(x) 为 x x x 二进制下 1 1 1 个数的奇偶性,根据 d ( i & k ) xor d ( j & k ) = d ( ( i xor j ) & k ) d(i\&k)\operatorname{xor}d(j\&k)=d((i\operatorname{xor}j)\&k) d(i&k)xord(j&k)=d((ixorj)&k),设 F W T [ A ] = ∑ i ( − 1 ) d ( i & k ) a i FWT[A]=\sum\limits_i(-1)^{d(i\&k)}a_i FWT[A]=i∑(−1)d(i&k)ai。
由于只有
1
&
1
=
1
1\&1=1
1&1=1,会使得
d
(
)
d()
d() 的奇偶性改变,所以可得:
F
W
T
[
A
]
=
m
e
r
g
e
(
F
W
T
[
A
0
]
+
F
W
T
[
A
1
]
,
F
W
T
[
A
0
]
−
F
W
T
[
A
1
]
)
U
F
W
T
[
A
]
=
m
e
r
g
e
(
U
F
W
T
[
A
0
]
+
U
F
W
T
[
A
1
]
2
,
U
F
W
T
[
A
0
]
−
U
F
W
T
[
A
1
]
2
)
FWT[A]=merge(FWT[A0]+FWT[A1],FWT[A0]-FWT[A1])\\UFWT[A]=merge(\dfrac{UFWT[A0]+UFWT[A1]}{2},\dfrac{UFWT[A0]-UFWT[A1]}{2})
FWT[A]=merge(FWT[A0]+FWT[A1],FWT[A0]−FWT[A1])UFWT[A]=merge(2UFWT[A0]+UFWT[A1],2UFWT[A0]−UFWT[A1])
于是就可以在 O ( n log n ) O(n\log n) O(nlogn) 的复杂度内分别求出三个卷积。
#include<bits/stdc++.h>
#define ll long long
#define rep(i,s,e) for(int i=(s);i<=(e);++i)
#define Rep(i,s,e) for(int i=(e);i>=(s);--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<<17)+5,mod=998244353;
int n;
ll a[N],b[N],c[N],x[N],y[N],inv;
inline ll ksm(ll x,int y){
ll res=1;
for(;y;y>>=1){
if(y&1) res=res*x%mod;
x=x*x%mod;
}
return res;
}
inline void OR(ll a[],int flag){
for(int len=2;len<=n;len<<=1){
int m=len>>1;
for(int i=0;i<n;i+=len) rep(j,0,m-1){
int u=a[i+j],v=a[i+j+m];
a[i+j+m]=flag==1?(u+v)%mod:(v-u+mod)%mod;
}
}
}
inline void AND(ll a[],int flag){
for(int len=2;len<=n;len<<=1){
int m=len>>1;
for(int i=0;i<n;i+=len) rep(j,0,m-1){
int u=a[i+j],v=a[i+j+m];
a[i+j]=flag==1?(u+v)%mod:(u-v+mod)%mod;
}
}
}
inline void XOR(ll a[],int flag){
for(int len=2;len<=n;len<<=1){
int m=len>>1;
for(int i=0;i<n;i+=len) rep(j,0,m-1){
int u=a[i+j],v=a[i+j+m];
a[i+j]=flag==1?(u+v)%mod:(u+v)%mod*inv%mod;
a[i+j+m]=flag==1?(u-v+mod)%mod:(u-v+mod)%mod*inv%mod;
}
}
}
signed main(){
n=1<<read(),inv=ksm(2,mod-2);
rep(i,0,n-1) a[i]=x[i]=read();
rep(i,0,n-1) b[i]=y[i]=read();
OR(a,1),OR(b,1);
rep(i,0,n-1) c[i]=a[i]*b[i]%mod,a[i]=x[i],b[i]=y[i];
OR(c,-1);
rep(i,0,n-1) printf("%lld ",c[i]);putchar('\n');
AND(a,1),AND(b,1);
rep(i,0,n-1) c[i]=a[i]*b[i]%mod,a[i]=x[i],b[i]=y[i];
AND(c,-1);
rep(i,0,n-1) printf("%lld ",c[i]);putchar('\n');
XOR(a,1),XOR(b,1);
rep(i,0,n-1) c[i]=a[i]*b[i]%mod;
XOR(c,-1);
rep(i,0,n-1) printf("%lld ",c[i]);putchar('\n');
}
坑填的差不多,大概就这样?