多项式 学习笔记

记录多项式以及一些相关数论前置芝士qwq

拉格朗日插值

n n n 个点 ( x i , y i ) (x_i,y_i) (xi,yi) 可以唯一确定一个 n − 1 n-1 n1 次多项式 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(xk)+a2(x2k2)++an(xnkn)0(modxk),则 f ( x ) ≡ f ( k ) ( m o d x − k ) f(x)\equiv f(k)\pmod{x-k} f(x)f(k)(modxk)

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(modkxi)) (1in)

套用 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=1n(kxi),mi=kxiM,则有:
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=1nyi×mi×mi1
  = ∑ 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=1nyi×j=i(kxj)×mi1
  = ∑ 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=1nyi×j=i(kxi+xixj)×j=ixixj1
  = ∑ 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=1nyi×j=ixixjkxj

中间在 k − x i k-x_i kxi 剩余系中转化了一下逆元,最后求出的就是拉格朗日插值的式子,在模意义和非模意义下通用。

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(ij)=(1)ni(i1)! (ni)!j=i(kj)=kij=1n(kj)
   ⟹    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=1nyi×(1)ni (i1)! (ni)!(ki)j=1n(kj)

分子的和分母都可以预处理,于是可以做到 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]=(pp/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=1nyi×j=ixixjkxj
  = ∑ 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=1nyi×(kxi)j=i(xixj)j=1nkxj
  = ∏ 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=1n(kxi)i=1n(kxi)j=i(xixj)yi

g ( k ) = ∏ i = 1 n ( k − x i ) g(k)=\prod\limits_{i=1}^n(k-x_i) g(k)=i=1n(kxi) t i = ∏ j ≠ i 1 x i − x j t_i=\prod\limits_{j\ne i}\dfrac{1}{x_i-x_j} ti=j=ixixj1,则 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=1nkxiyiti,每次新增一个点 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} tn1,再 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 n1 次多项式, 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 n1,第 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++anxn1 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++an2xn2)+(a1x+a3x3++an1xn1)
  = ( 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++an2(x2n2)2)+x(a1+a3(x1)2+an2(x2n2)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++an2x2n2 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++an1x2n2,则 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} k2n 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,,yn1),构造函数 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++yn1xn1,把 w n 0 , w n − 1 , ⋯   , w n − ( n − 1 ) w_n^0,w_n^{-1},\cdots,w_n^{-(n-1)} wn0,wn1,,wn(n1) 代入得到 ( z 0 , z 1 , ⋯   , z n − 1 ) (z_0,z_1,\cdots,z_{n-1}) (z0,z1,,zn1)

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=0n1yi×(wnk)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=0n1j=0n1aj×(wni)j×(wnk)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=0n1aji=0n1(wnjk)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=0n1(wnjk)i=j=knj=kwnjk1(wnjk)n1=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,wn1,,wn(n1) 之后求 DFT 的结果除以 n n n 就可以得到 f ( x ) f(x) f(x) 的各项系数,而形如 w n − 1 w_n^{-1} wn1 的就是共轭复数。

学了一大顿之后就又可以写高精乘法了:(亲测不预处理 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} an1(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) an1(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 aiaj(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 gkn1(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=ij=kaibj,考虑根据 i ∣ k = k , j ∣ k = k    ⟺    ( i ∣ j ) ∣ k = k i|k=k,j|k=k\iff (i|j)|k=k ik=k,jk=k(ij)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=ik=kai

考虑分治,把长为 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=kaibj,考虑根据 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=kai

同理 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=kaibj,设 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');
}

坑填的差不多,大概就这样?

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值