Loading

多项式入门

多项式入门

1 复数

  • \(i^2=-1\)

  • 复数为 \(z=a+bi\)

  • \(a+bi,a-bi\) 互为共轭复数

  • 一个复数对应复平面上的任意一个点,一个复数与复平面上 \((a,b)\) 一一对应。复数与向量之间的运算有相似的地方,假设复数到复平面原点的距离为 \(r\) ,那么复数也可以用 \((r\cos \alpha,r\sin \alpha)\) 来表示。

1.2 运算

  • 加法为向量加法:\(z_1+z_2=(a+c)+(b+d)i\)

  • 乘法:模长相乘幅角相加。\((ac-bd)+(ad+bc)i\)

    也可以表示成 \((r_1r_2\cos(\theta_1+\theta_2),r_1r_2\sin(\theta_1+\theta_2))\)。实质是向量旋转。

1.3 单位根

\(n\) 次单位根为 \(x^n=1\)\(n\) 个根。

\(\omega_n=\cos\frac{2\pi}{n}+\sin\frac{2\pi}{n}i\)

所谓的 \(n\) 个根为 \(\omega_n^1,\omega_n^2...\omega_n^n\)

注意到:

\[\omega_n^2=\cos\frac{2\pi}{n}\times \cos\frac{2\pi}{n}+\sin\frac{2\pi}{n}\times \sin\frac{2\pi}{n}+(\sin\frac{2\pi}{n}\times \cos\frac{2\pi}{n}+\cos\frac{2\pi}{n}\times \sin\frac{2\pi}{n})i\\ =\cos \frac{2\pi\times2}{n}+\sin\frac{2\pi\times 2}{n}i \]

推广后得到:

\[\omega_{n}^k=\cos\frac{2k\pi}{n}+\sin\frac{2k\pi}{n}i \]

为什么单位根是对的?发现把 \(n\) 个单位根全部画到平面直角坐标系上时,这些单位根恰好在单位圆上,且对于一个单位根 \(\omega_n^k\) 来说,\(\omega_n^{k+1}\) 相当于把这个点沿着单位圆逆时针转了 \(\frac{2\pi}{n}\) 不难发现这些单位根的 \(n\) 次方都是 \((\omega_{n}^n)^k\) ,而 \(\omega_n^n=1\) ,所以不难知道这 \(n\) 个根就是 \(x^n=1\)\(n\) 个根。

1.3.1 单位根的性质

  • \(\omega_n^k=\omega_{2n}^{2k}\)

  • 证明:\(\omega_n^k=(\cos\frac{2k\pi}{n},\sin\frac{2k\pi}{n})=(\cos\frac{4k\pi}{2n},\sin\frac{4k\pi}{2n})=\omega_{2n}^{2k}\)

  • \(\omega_n^{k+\frac n2}=-\omega_{n}^k\)

  • 证明:根据单位根的几何意义,\(n\) 次是转了一圈,\(\frac n2\) 是转了半圈,所以自然是对的。

    我们也可以把坐标写出来用三角函数诱导公式来证明,也不难证明。

  • \(\omega_n^0=\omega_n^n=1\)

  • 这个刚刚已经说过了。

2 多项式

2.1 点值表示法

  • 原始表示法:\(f(x)=\sum\limits_{i=0}^na_ix^i\)

  • 原始的多项式乘法:\(\sum\limits_{i=0}^{n_1}\sum\limits_{j=0}^{n_2}a_ib_jx^{i+j}\) 。不难发现这样是 \(n^2\) 的。

  • 点值表示法:\(n+1\) 个点确定一个 \(n\) 次多项式,所以我们可以用 \((x_1,f(x_1)),(x_2,f(x_2)),...(x_{n+1},f(x_{n+1}))\) ,来表示一个 \(n\) 次多项式。

  • 点值表示法多项式相乘:

    不难发现,多项式相乘后的点值为 \((x_1,g(x_1)f(x_1)),(x_2,g(x_2)f(x_2)...x_{n+1}g(x_{n+1})f(x_{n+1}))\)

    这样的话是 \(O(n)\) 的。

我们现在的核心位置是原始表示法和点值表示法的相互转化,我们称 \(DFT(f)\) 为原始表示法 \(f\) 化为点值表示法,\(IDFT(f)\) 表示点值表示法 \(f\) 化为原始表示法。 那么我们需要完成的步骤有三个:

  • \(DFT(f),DFT(g)\)
  • \(DFT(f\times g)=DFT(f)\times DFT(g)\)
  • \(IDFT(DFT(f\times g))\)

其中,第二步可以 \(O(n)\)。现在只需要解决第一个步骤和第三个步骤。

2.2 FFT 快速傅里叶变换

2.2.1 DFT

以下我们假设多项式项数 \(n\)\(2^k\) 我们可以强行补到 \(2^k\) 次方的形式。令多项式 \(A(x)=\sum\limits_{i=0}^{n-1}a_ix^i\)

而我们选择的 \(n\) 个点(此时多项式次数为 \(n-1\) )为 \(\omega_n^0,\omega_n^1...\omega_n^{n-1}\)。有什么好处?

我们令:

\[A_1(x)=a_0+a_2x+...a_{n-2}x^{\frac n 2-1}\\ A_2(x)=a_1+a_3x+...a_{n-1}x^{\frac n2-1} \]

不难发现,这时有:

\[A(x)=A_1(x^2)+xA_2(x^2) \]

我们令 \(x=\omega_n^k\)

发现 \(A(\omega_{n}^k)=A_1(\omega_n^{2k})+\omega_{n}^kA_2(\omega_{n}^{2k})=A_1(\omega_{\frac{n}{2}}^k)+\omega_{n}^kA_2(\omega_{\frac{n}{2}}^k)\)

于是我们可以分治去做这个事情,每次递归一层,多项式的个数会加倍,所以复杂度为 \(n^2\)

但是我们还有 \(A(\omega_n^{k+\frac n2})=A_1(\omega_{\frac n2}^k)-\omega_n^kA_2(\omega_{\frac n2}^k)\)

所以把下面的式子也应用上的话,每一层多项式的个数不会增加,这样一共递归 \(\log n\) 层,总复杂度为 \(n\log n\)

2.2.2 代码实现(可能会假)

inline void fft(complex_ *z,int n,int inv){ 
    if(n==1) return;
    int h=n>>1;
    static complex_ y[maxn];
    for(int a=0;a<h;a++){
        y[a]=z[a<<1];
        y[a+h]=z[a<<1|1];
    }
    for(int a=0;a<n;a++) z[a]=y[a];
    fft(z,h,inv);
    fft(z+h,h,inv);
    for(int k=0;k<h;k++){
        complex_ x=complex_ (cos(2*k*pi/n),sin(2*k*pi/n));
        y[k]=z[k]+z[k+h]*x; 
        y[k+h]=z[k]-z[k+h]*x;
    }
    for(int a=0;a<n;a++) z[a]=y[a];
}

2.2.3 代码解释

目前这段代码只需要完成 DFT

我们的目的是当 DFT 完成时。\(z\) 数组返回的值为点值表示法,显然当多项式项数为 \(1\) 时,此时他的系数就是它的点值表示法,所以我们什么也不用做。否则,我们首先把 \(z\) 数组的奇次项和偶次项分别处理,存回 \(z\) 数组,然后对每一个部分做 DFT 最后利用我们上面的两个式子,得到 \(z\) 的点值表示法。

2.2.4 IDFT

我们的目标是把点值表示法换回多项式表示法。

我们令 \(B=DFT(A)\) ,即我们让 \(A\) 的点值表示法变成一个多项式,即 \(b_i=A(\omega_n^i)\) ,我们设 \(C(x)=\sum\limits_{i=0}^{n-1} b_ix^i\),然后让 \(x=\omega_n^{-k}\)

我们就有了下面这个式子:

\[C(\omega_n^{-k})=\sum\limits_{i=0}^{n-1}\sum\limits_{j=0}^{n-1}a_j(\omega_{n}^i)^j(\omega_n^{-k})^i\\ =\sum\limits_{j=0}^{n-1}a_j\sum\limits_{i=0}^{n-1}(\omega_{n}^{j-k})^i \]

\(S(i)=\sum\limits_{i=0}^{n-1}(\omega_{n}^{j-k})^i\),然后我们讨论一下会发现:

  • \(j=k\Rightarrow S(i)=n\)
  • \(j\not =k\Rightarrow S(i)=0\)

所以有:

\[C(\omega_n^{-k})=a_k\times n \]

我们的目的是求 \(A\) 的原始表示法,所以只需要求 \(\frac{C(\omega_{n}^{-k})}{n}\) 就可以了。

注意到我们实际上干了一件什么事情,我们所做的实际上与 DFT 没有多少区别,我们只不过把 \(A\) 的点值表示法当做原始表示法又做了一遍 DFT 而已,只不过这里我们带入的是 \(\omega_{n}^{-k}\) 。不过把 \(k\) 换成 \(-k\) ,上面的性质依然成立,所以我们同样可以用 DFT 的方法做 IDFT。

通过考虑单位根几何意义这个 \(k\)\(-k\) 的区别我们只用看

2.2.5 总代码(可能会假)

inline void FFT(complex_ *a,int n,int inv){
    if(n==1) return;
    static complex_ b[N];
    int h=n>>1;
    for(int i=0;i<h;i++){
        b[i]=a[i<<1];
        b[i+h]=a[i<<1|1];
    }
    for(int i=0;i<n;i++) a[i]=b[i];
    FFT(a,h,inv);
    FFT(a+h,h,inv);
    for(int i=0;i<h;i++){
        complex_ x=complex_ (cos(2*k*pi)/n,inv*sin(2*k*pi/n));
        b[k]=a[k]+a[k+h]*x;
        b[k+h]=a[k]-a[k+h]*x; 
    }
    for(int i=0;i<n;i++) a[i]=b[i]; 
}

int main(){
    FFT(f,n,1);
    FFT(g,n,1);
    for(int i=0;i<n;i++) f[i]=f[i]*g[i];
    FFT(f,n,-1);
    for(int i=0;i<n;i++) f[i]=f[i]/n;
}

2.2.6 迭代卡常与最后实现

可以发现这个代码是过不了模板的,会 T。

所以我们卡一卡常。

首先我们看是否能够避免数组拷贝操作:

首先是优化换位置的这个操作,我们能否一步到位?

注意到换完之后的位置实际上是原来位置的二进制翻转。而二进制翻转我们可以 \(O(n)\) 的去做这个东西。

注意到我们可以在转移上加以优化,这样就避免了所有数组的复制操作。

然后我们还可以迭代去做这个事情。

注意到我们令 \(P(x)=F(x)+G(x)i\),然后我们做 \(P(x)^2\) ,会发现:

\[P(x)^2=F(x)^2-G(x)^2+2F(x)G(x)i \]

所以我们直接把虚部除以 \(2\) 就可以。

代码:

inline void fft(cp *f,bool flag){
	for(int i=0;i<n;i++) if(i<tr[i]) swap(f[i],f[tr[i]]);
	for(int p=2;p<=n;p<<=1){
		int len=p>>1;
		cp tg(cos(2*pi/p),sin(2*pi/p));
		if(!flag) tg.y*=-1;
		for(int k=0;k<n;k+=p){
			cp buf(1,0);
			for(int l=k;l<k+len;l++){
				cp tt=buf*f[len+l];
				f[len+l]=f[l]-tt;
				f[l]=f[l]+tt;
				buf=buf*tg;
			}
		}
	}
}

int main(){
	n=read();m=read();
	for(int i=0;i<=n;i++) scanf("%lf",&f[i].x);
	for(int i=0;i<=m;i++) scanf("%lf",&f[i].y);
	for(m+=n,n=1;n<=m;n<<=1);
	for(int i=0;i<n;i++) tr[i]=(tr[i>>1]>>1)|((i&1)?n>>1:0);
	fft(f,1);
	for(int i=0;i<n;i++) f[i]=f[i]*f[i];
	fft(f,0);
	for(int i=0;i<=m;i++) printf("%d ",(int)(f[i].y/n/2+0.49));
	return 0;
}

2.3 NTT 快速数论变换

FFT 一个最大的问题在于精度问题,因为参与运算的都是实数。

NTT 适用于在模意义下做多项式乘法。

在 FFT 中我们用到了的性质:

  • \(\omega_n^n=1\)
  • \(\omega_{n}^1,\omega_n^2...\omega_n^n\) 互不相等
  • \(\omega_n^k=\omega_{2n}^{2k},\omega_{n}^{k+\frac{n}{2}}=-\omega_n^k\)
  • \(\sum\limits_{i=0}^{n-1}(\omega_n^k)^i=n\times [k=0]\)

假设我们在 \(\bmod p\) 意义下,其中 \(p\) 为一个质数。设 \(g\)\(p\) 的原根。

\(p-1=q\times 2^r=q\times n\) 我们令 \(n\) 为多项式项数。

\(\omega_n=g^q\),我们来证明这个东西仍然满足上面的性质。

  • \(\omega_{n}^n=g^{qn}=g^{p-1}\equiv 1(\bmod p)\)

  • 假设有两个相等,则有 \(g^{qi}\equiv g^{qj}(\bmod p)\Rightarrow g^{qi-qj}\equiv 1(\bmod p)\),显然有 \(i-j\not = n\)。所以如果成立,与原根定义不符合,所以成立。

  • 前半段:\(g^{qk}=g^{\frac{q}{2}\times 2k}\) 。所以前半段成立。

    后半段:只需要证明:\(\omega_n^{\frac n2}=-1\) 。而 \(\omega_n^n=1,(\omega_n^{\frac{n}{2}})^2=1,\omega_n^{\frac{n}{2}}\not = 1\) 。所以成立。

  • 这个性质依赖于前面的性质,显然成立。

所以我们找到了一个单位根的替代品。主要,在实践中,我们通常取模数为 \(998244353=2^{23}\times 7\times 17+1\)。这样,只要这个多项式长度不超过 \(2^{23}\) ,都可以用 NTT 而不是 FFT。

所以我们可以直接在 FFT 的代码上直接改。

同时,一样有卡常之后的非递归写法:

#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define int long long
#define uint unsigned int
#define ull unsigned long long
#define N 1350000
#define M number
using namespace std;

const int INF=0x3f3f3f3f;
const int mod=998244353;
const int gg=3;
const int invg=332748118;

template<typename T> inline void read(T &x) {
    x=0; int f=1;
    char c=getchar();
    for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
    for(;isdigit(c);c=getchar()) x=x*10+c-'0';
    x*=f;
}

int n,m,tr[N<<1],f[N<<1],g[N<<1];

inline int ksm(int a,int b,int mod){
    int res=1;
    while(b){
        if(b&1) res=1ll*res*a%mod;
        a=1ll*a*a%mod;
        b>>=1;
    }
    return res;
}

inline void NTT(int *f,bool op){
    for(int i=0;i<n;i++) if(i<tr[i]) swap(f[i],f[tr[i]]);
    for(int p=2;p<=n;p<<=1){
        int len=p>>1;
        int tg=ksm((op==1?gg:invg),(mod-1)/p,mod);
        for(int k=0;k<n;k+=p){
            int buf=1;
            for(int l=k;l<k+len;l++){
                int now=buf*f[l+len]%mod;
                f[l+len]=f[l]-now;
                if(f[l+len]<0) f[l+len]+=mod;
                f[l]=f[l]+now;
                if(f[l]>mod) f[l]%=mod;
                buf=buf*tg%mod;
            }
        }
    }
}

signed main(){
    // freopen("my.in","r",stdin);
    // freopen("my.out","w",stdout);
    read(n);read(m);n++;m++;
    for(int i=0;i<n;i++) read(f[i]);
    for(int i=0;i<m;i++) read(g[i]);
    for(m+=n,n=1;n<m;n<<=1);
	for(int i=0;i<n;i++) tr[i]=(tr[i>>1]>>1)|((i&1)?n>>1:0);
    NTT(f,1);NTT(g,1);for(int i=0;i<n;i++) f[i]=1ll*f[i]*g[i]%mod;
    NTT(f,0);int invn=ksm(n,mod-2,mod);
    for(int i=0;i<=m-2;i++) printf("%lld ",1ll*f[i]*invn%mod);
	return 0;
}

FFT NTT 小结

在做 DFT 的时候 我们位置 \(i\) 上的值是 \(F(g^{qi})\),是一个值,然后我们用这个值去做 IDFT 得到最终的多项式。其中 DFT 和 IDFT 我们可以分治来做。这就是多项式乘法优化的本质。

2.4 FWT 快速沃尔什变换

我们希望快速的解决下面这个问题:

\[c_{i\oplus j}=\sum\limits_{i=1}^n\sum\limits_{j=1}^na_i\times b_j \]

同样,我们认为 \(n\)\(2\) 的正整数次幂。

上面的这个式子我们下面统一写成 \(C=A\oplus B\)

和 FFT 以及 NTT 一样,我们希望能够找到一个 FWT ,使得 \(FWT(A\oplus B)=FWT(A)\times FWT(B)\) 其中右边的这个乘法就是各个位置相乘的形式。

我们下面给出或卷积和和卷积,并推导异或卷积,而实际上,我们可以自己定义位运算并推导它的卷积。

2.4.1 或卷积

2.4.1.1 或卷积 FWT

我们设 \(A_0\)\(A\) 前半段,\(A_1\)\(A\) 后半段。

定义 \(A=(Q,W)\) 为数组 \(A\) 是由 \(Q,W\) 组成的,其中前半段为 \(Q\) ,后半段为 \(W\)

根据上面的那个卷积式,我们显然有 \(A|B=(A_0|B_0,A_0|B_1+A_1|B_0+A_1|B_1)\)

看不懂上面那个式子的读者可以这样想:因为 \(n\) 是一个 \(2\) 的整数次幂,所以能够为 \(A| B\) 前半段做贡献的只能是 \(A_0,B_0\) ,且 \(A_0| B_0\) 就是前半段,这是因为它们下标二进制的最高位为 \(0\)

或卷积的 FWT 是这样的:

\[FWT(A)=(FWT(A_0),FWT(A_0)+FWT(A_1)),(n>1)\\ FWT(A)=A,(n=1) \]

显然,这样的 FWT 能够满足 \(FWT(A+B)=FWT(A)+FWT(B)\)

我们来证明这个式子满足 \(FWT(A)\times FWT(B)=FWT(A| B)\)

显然有:

\[FWT(A)\times FWT(B)\\ =(FWT(A_0)\times FWT(B_0),FWT(A_0)\times FWT(B_0)+\\FWT(A_0)\times FWT(B_1)+FWT(A_1)\times FWT(B_0)+FWT(A_1)\times FWT(B_1)) \]

有:

\[FWT(A| B)=FWT((A_0| B_0,A_0| B_1+A_1| B_0+A_1| B_1))\\ =(FWT(A_0| B_0),FWT(A_0| B_0)+FWT(A_0| B_1+A_1| B_0+A_1| B_1)) \]

我们用数学归纳法,假设 \(FWT(A| B)=FWT(A)\times FWT(B)\)\(\le \frac n2\) 时成立,其中,当 \(n=1\) 时,左式显然成立。

注意到:

\[FWT(A| B)=FWT((A_0| B_0,A_0| B_1+A_1| B_0+A_1| B_1))\\ =(FWT(A_0| B_0),FWT(A_0| B_0)+FWT(A_0| B_1+A_1| B_0+A_1| B_1))\\ =(FWT(A_0)\times FWT(B_0),FWT(A_0)\times FWT(B_0)+\\FWT(A_0)\times FWT(B_1)+FWT(A_1)\times FWT(B_0)+FWT(A_1)\times FWT(B_1))\\ =FWT(A)\times FWT(B) \]

所以这个 FWT 可行。

2.4.1.2 或卷积 IFWT

但是光有 FWT 不行。我们仍然需要 IFWT。考虑一下我们做 FWT 时,我们只需要把过程逆一下,就可以复原整个序列,由于递归,我们的复原时从底到上的,显然可以复原整个序列。具体来说是这样:

\[IFWT(FWT(A))=IFWT((FWT(A_0),FWT(A_0)+FWT(A_1)) \]

我们只需要让 \(IFWT(A)=(IFWT(A_0),IFWT(A_1)-IFWT(A_0))\)

显然,这样的 \(IFWT(A+B)=IFWT(A)+IFWT(B)\)

所以有:

\[IFWT(FWT(A))=IFWT((FWT(A_0),FWT(A_0)+FWT(A_1))\\ =(IFWT(FWT(A_0)),IFWT(FWT(A_1))) \]

于是归纳可以证明 IFWT 可行。

代码我们与和卷积一起给出。

2.4.2 和卷积

在和卷积中:

\[FWT(A)=(FWT(A_0)+FWT(A_1),FWT(A_1))\\ IFWT(A)=(IFWT(A_0)-IFWT(A_1),IFWT(A_1)) \]

证明方法和或卷积类似,这里不再赘述。

2.4.3 和,或卷积代码

  • 代码解释:
  • op=1 为或卷积,op=2 为和卷积。
  • inv=0 为 FWT,inv=1 为IFWT
inline void fwt(int *z,int n,int op,int inv){
    if(n==1) return;
    int h=n>>1;
    fwt(z,h,op,inv);fwt(z+h,h,op,inv);
    static int y[N];
    for(int i=0;i<h;i++){
        if(op==1){
            if(inv==0){
                y[i]=z[i];
                y[i+h]=z[i]+z[i+h];
            }
            else{
                y[i]=z[i];
                y[i+h]=z[i+h]-z[i];
            }
        }
        else if(op==2){
            if(inv==0){
                y[i]=z[i]+z[i+h];
                y[i+h]=z[i+h];
            }
            else{
                y[i]=z[i]-z[i+h];
                y[i+h]=z[i+h];
            }
        }
    }
    for(int i=0;i<n;i++) z[i]=y[i];
}

在主函数中我们可以参照 FFT 的写法。

2.4.4 异或卷积

在这里说明如何自己手退 FWT 和 IFWT。

  • 下文中所涉及到的 FWT 和 IFWT 都是满足线性的,即:

    \(FWT(A+B)=FWT(A)+FWT(B),IFWT(A+B)=IFWT(A)+IFWT(B)\)

首先根据异或的计算法则,我们有:

\[A\hat{}B=(A_0\hat{}B_0+A_1\hat{}B_1,A_0\hat{}B_1+A_1\hat{}B_0) \]

根据前面或卷积和和卷积的经验,我们设:

\[FWT(A)=(aFWT(A_0)+bFWT(A_1),cFWT(A_0)+dFWT(A_1)) \]

我们的目标是:

\[FWT(A)\times FWT(B)=FWT(A\hat{}B) \]

我们直接代入可以得到:

\[FWT(A\hat{}B)=FWT((A_0\hat{}B_0+A_1\hat{}B_1,A_0\hat{}B_1+A_1\hat{}B_0))\\ =(aFWT(A_0\hat{}B_0+A_1\hat{}B_1)+bFWT(A_0\hat{}B_1+A_1\hat{}B_0),\\cFWT(A_0\hat{}B_0+A_1\hat{}B_1)+dFWT(A_0\hat{}B_1+A_1\hat{}B_0))\\ =(aFWT(A_0\hat{}B_0)+aFWT(A_1\hat{}B_1)+bFWT(A_0\hat{}B_1)+bFWT(A_1\hat{}B_0),\\ cFWT(A_0\hat{}B_0)+cFWT(A_1\hat{}B_1)+dFWT(A_0\hat{}B_1)+dFWT(A_1\hat{}B_0))\\ =FWT(A)\times FWT(B)\\ =(aFWT(A_0)+bFWT(A_1),cFWT(A_0)+dFWT(A_1))\times \\(aFWT(B_0)+bFWT(B_1),cFWT(B_0)+dFWT(B_1))\\ =(a^2FWT(A_0)FWT(B_0)+abFWT(A_0)FWT(B_1)+abFWT(B_0)FWT(A_1)+b^2FWT(A_1)FWT(B_1),\\ c^2FWT(A_0)FWT(B_0)+cdFWT(A_0)FWT(B_1)+cdFWT(B_0)FWT(A_1)+d^2FWT(A_1)FWT(B_1))\\ \]

根据上面这些式子,我们对应项系数相等,可以得到一个方程:

\[\begin{cases} a^2=a\\ ab=a\\ b^2=a\\ c^2=c\\ cd=c\\ d^2=c \end{cases} \]

很明显,这个方程有很多解,比如说 \(a=b=c=d=0\) ,又比如说 \(a=b=c=d=1\) ,但是,这些解都不合法,为什么?

其实,任何一个满足这个方程的解我们都可以拿来做 FWT ,但是并不是所有的解都能用来做 IFWT!

我们看一下做 IFWT 需要什么条件。注意到 IFWT 其实就是 FWT 的逆变换,所以我们只需要把 FWT 的计算反过来就可以了,FWT 是赋值,那么反过来就是解方程。我们需要知道给我们赋值的数是什么。这相当于我们要解这样的一个方程:

\[\begin{cases} FWT(A_0)=alastFWT(A_0)+blastFWT(A_1)\\ FWT(A_1)=clastFWT(A_0)+dlastFWT(A_1) \end{cases} \]

其中假设 \(a,b,c,d\) 我们是知道的。不理解这个方程的可以把或卷积那里的值代过来试一下。这种写法应该不是很合法,但可以这样想,其实每一次回溯,数组所有位置的值都被新覆盖掉了,我们 IFWT 做的就是找到新覆盖之前的那个值是什么。

我们可以解出来:

\[\begin{cases} x=FWT(A_0)\\ y=FWT(A_1)\\ lastFWT(A_0)=\frac{dx-by}{ad-bc}\\ lastFWT(A_1)=\frac{cx-ay}{bc-ad} \end{cases} \]

不难发现,要求有解必须满足 \(ad-bc\not =0\)

所以我们把上面这个方程完善一下:

\[\begin{cases} a^2=a\\ ab=a\\ b^2=a\\ c^2=c\\ cd=c\\ d^2=c\\ ad\not=bc \end{cases} \]

在这样的条件下,有两组解:

\[\begin{cases} a=1\\ b=1\\ c=1\\ d=-1\\ \end{cases} \]

\[\begin{cases} a=1\\ b=-1\\ c=1\\ d=1\\ \end{cases} \]

这两组解都满足条件,我们就可以用来做 FWT。

至于 IFWT,我们已经知道如何复原上一次的数组了,也可以做,公式就是:

\[lastFWT(A_0)=\frac{dx-by}{ad-bc}\\ lastFWT(A_1)=\frac{cx-ay}{bc-ad} \]

于是异或卷积也可以做了。

于是自定义的位运算也可以做了。

这里附上或并还有异或卷积的代码,用的是非递归实现,不得不说,这个要比 NTT 好写的多。


#include<bits/stdc++.h>
#define dd double
#define ld long double
#define ll long long
#define uint unsigned int
#define ull unsigned long long
#define N 1000100
#define M number
using namespace std;

const int INF=0x3f3f3f3f;
const int mod=998244353;
const int inv2=499122177;

template<typename T> inline void read(T &x) {
    x=0; int f=1;
    char c=getchar();
    for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
    for(;isdigit(c);c=getchar()) x=x*10+c-'0';
    x*=f;
}

inline void FWT1(int *f,int n,int op){
    for(int mid=1;mid<=(n>>1);mid<<=1)
        for(int l=0;l<n;l+=(mid<<1))
            for(int i=l;i<=l+mid-1;i++){
                if(op==0){(f[i+mid]+=f[i])%=mod;}
                else{(f[i+mid]-=f[i])%=mod;}
            }
}

inline void FWT2(int *f,int n,int op){
    for(int mid=1;mid<=(n>>1);mid<<=1)
        for(int l=0;l<n;l+=(mid<<1))
            for(int i=l;i<=l+mid-1;i++){
                if(op==0){(f[i]+=f[i+mid])%=mod;}
                else (f[i]-=f[i+mid])%=mod;
            }
}

inline void FWT3(int *f,int n,int op){
    for(int mid=1;mid<=(n>>1);mid<<=1)
        for(int l=0;l<n;l+=(mid<<1))
            for(int i=l;i<=l+mid-1;i++){
                int a=f[i],b=f[i+mid];
                if(op==0){f[i]=(a+b)%mod;f[i+mid]=(a-b)%mod;}
                else{f[i]=1ll*((a+b)%mod)%mod*inv2%mod;f[i+mid]=1ll*((a-b)%mod)*inv2%mod;}
            }
}

int a[N],n,b[N],c[N];

int main(){
    // freopen("my.in","r",stdin);
    // freopen("my.out","w",stdout);
    read(n);n=1<<n;
    for(int i=0;i<n;i++) read(a[i]);
    for(int i=0;i<n;i++) read(b[i]);
    FWT1(a,n,0);
    FWT1(b,n,0);for(int i=0;i<n;i++) c[i]=1ll*a[i]*b[i]%mod;
    FWT1(a,n,1);FWT1(b,n,1);FWT1(c,n,1);
    for(int i=0;i<n;i++) printf("%d ",(c[i]%mod+mod)%mod);puts("");
    FWT2(a,n,0);FWT2(b,n,0);for(int i=0;i<n;i++) c[i]=1ll*a[i]*b[i]%mod;
    FWT2(a,n,1);FWT2(b,n,1);FWT2(c,n,1);
    for(int i=0;i<n;i++) printf("%d ",(c[i]%mod+mod)%mod);puts("");
    FWT3(a,n,0);FWT3(b,n,0);for(int i=0;i<n;i++) c[i]=1ll*a[i]*b[i]%mod;
    FWT3(a,n,1);FWT3(b,n,1);FWT3(c,n,1);
    for(int i=0;i<n;i++) printf("%d ",(c[i]%mod+mod)%mod);puts("");
    return 0;
}

关于异或卷积 IFWT,其实运算的逆就是IFWT,证明这个只需要想或卷积一样归纳就可以。

2.5 多项式算法变形

2.5.1 多项式求逆

我们考虑给定 \(A(x)\) 要求一个 \(B(x)\),使得 \(A(x)B(x)\equiv 1 (\bmod x^n)\)

如果 \(n=1\) 显然这个问题就是一个求逆元的事情。这启发我们尝试把答案逐渐缩小。

这里假设我们已经找到了一个 \(C(x)\) 使得 \(A(x)C(x)\equiv 1\bmod x^{\lceil\frac x2\rceil}\)

我们尝试用 \(A(x),C(x)\) 去表示 \(B(x)\)

不难发现,有:

\[A(x)B(x)\equiv 1\bmod x^{\lceil\frac x2\rceil}\Rightarrow A(x)(B(x)-C(x))\equiv 0 \bmod x^{\lceil\frac x2\rceil}\\ \Rightarrow B(x)\equiv C(x)\bmod x^{\lceil\frac x2\rceil}\Rightarrow x^{\lceil\frac x2\rceil}|B(x)-C(x)\\ \Rightarrow x^n|(B(x)-C(x))^2\Rightarrow B^2(x)-2B(x)C(x)+C^2(x)\equiv 0\bmod x^n\\ \Rightarrow A(x)B^2(x)-2A(x)B(x)C(x)+A(x)C^2(x)\equiv 0\bmod x^n\\ B(x)-2C(x)+A(x)C^2(x)\equiv 0 \bmod x^n\\ B(x)\equiv 2C(x)-A(x)C^2(x)\bmod x^n \]

所以我们就可以用多项式乘法来做了。

时间复杂度上限为 \(O(n\log^2 n)\)

2.5.2 多项式开根

给定 \(A(x)\) 找到一个 \(B(x)\) 使得 \(B^2(x)\equiv A(x)\bmod x^n\)

我们同样考虑缩小这个问题,假设我们已经求到了一个 \(C(x),s.t.C^2(x)\equiv A(x)\bmod x^{\lceil\frac x2\rceil}\)

那么我们有:

\[(C^2(x)-A(x))^2\equiv 0\bmod x^{n}\\ \Rightarrow C^4(x)-2C^2(x)A(x)+A^2(x)\equiv 0\bmod x^{n}\\ \Rightarrow C^4(x)+2C^2(x)A(x)+A^2(x)\equiv 4C^2(x)A(x)\bmod x^n\\ \Rightarrow (\frac{C^2(x)+A(x)}{2C(x)})^2\equiv A(x)\bmod x^n\\ \Rightarrow B(x)=\frac{C^2(x)+A(x)}{2C(x)} \]

运用多项式求逆就可以做了。

2.5.3 多项式除法

给定 \(A(x),B(x)\) 要求找到 \(C(x),D(x)\) 使得 \(A(x)=C(x)B(x)+D(x)\)。其中需要满足 \(D(x)\) 的次数小于 \(B(x)\) 的次数。

我们设 \(A\) 的次数为 \(n\)\(B\) 的次数为 \(m\) ,那么 \(C\) 的次数为 \(n-m\) ,可以认为 \(D\) 的次数为 \(m-1\)

我们设 \(inv(A)=x^n\times A(\frac 1x)\) ,那么显然有 \(A=inv(inv(A))\) 。通俗来讲,\(inv\) 就是把系数倒着写一遍。

那么有:

\[inv(A)=inv(C\times B+D)\\ \Rightarrow x^n\times A(\frac 1x)=x^{n-m}\times C(\frac 1x)\times x^m\times B(\frac 1x)+x^{n-m+1}\times x^{m-1}\times D(\frac 1x)\\ \Rightarrow inv(A)=inv(C)\times inv(B)+x^{n-m+1}inv(D)\\ \Rightarrow inv(A)\equiv inv(C)\times inv(B)\bmod x^{n-m+1}\\ \]

我们可以利用多项式求逆来求解一个 \(C\) ,然后代入原式求 \(D\) 即可。

2.5.4 多项式求对数

给定 \(f(x)\) 要求找到 \(g(x),s.t.g(x)\equiv \ln f(x)\bmod x^n\)

我们首先关注这里的对数的定义是什么,我们对两边求导可以得到:

\[g'(x)\equiv \frac{f'(x)}{f(x)}\bmod x^n \]

很明显等式右边是可以算的,那么我们算出后用不定积分算一下就可以得到 \(g\)

一般来说,题目保证常数项等于 \(1\)

2.5.5 多项式 exp

给定 \(f(x)\) 要求求解 \(g(x),s.t.g(x)\equiv e^{f(x)}\bmod x^n\)

首先引入 \(f(x)\) 的泰勒级数:

\[f(x)=\sum\limits_{i=0}^{\infty}\frac{f^{(i)}(x_0)}{i!}\times (x-x_0)^i \]

我们还是首先要考虑这里 exp 的定义是什么,我们对两边求对可以得到:

\[\ln g(x)\equiv f(x)\bmod x^n\Rightarrow \ln g(x)-f(x)\equiv 0\bmod x^n \]

我们令 \(h(g(x))=\ln g(x)-f(x)\) ,注意这里函数 \(h\) 的自变量是多项式 \(g(x)\) 而不是 \(x\) ,也就是说,这并不是一个复合函数,这里的 \(f(x)\) 相当于一个常量。而我们期望得到一个 \(h(g(x))=0\) 的一个解。

我们对 \(h\) 多次求导可以得到:

\[h^{(1)}(g(x))=\frac{1}{g(x)}\\ h^{(2)}(g(x))=-\frac{1}{g^2(x)}\\ h^{(3)}(g(x))=2\frac{1}{g^{3}(x)}\\ h^{(n)}(g(x))=(n-1)!\times (-1)^{n-1}\times \frac{1}{g^n(x)} \]

我们还是和以前一样,设 \(\ln g_0(x)\equiv f(x)\bmod x^{\lceil\frac n2\rceil}\),假设这个 \(g_0(x)\) 是已知的。我们现在要求 \(g_1(x),s.t.\ln g_1(x)\equiv f(x)\bmod x^n\)。我们写出 \(h(g_1(x))\)\(x_0=g_0(x)\) 的泰勒级数。有:

\[h(g_1(x))=\sum\limits_{i=0}^{\infty}\frac{h^{(i)}(g_0(x))}{i!}\times (g_1(x)-g_0(x))^i \]

两边都对 \(x^n\) 取模可以得到:

\[h(g_1(x))\equiv \sum\limits_{i=0}^{\infty}\frac{h^{(i)}(g_0(x))}{i!}\times (g_1(x)-g_0(x))^i\bmod x^n \]

注意到实际上有:

\[g_0(x)\equiv g_1(x)\bmod x^{\lceil\frac n2\rceil}\Rightarrow (g_0(x)-g_1(x))^2\equiv 0\bmod x^n\Rightarrow (g_0(x)-g_1(x))^i\equiv 0\bmod x^n(i\ge 2,i\in \N) \]

所以有:

\[h(g_1(x))\equiv \sum\limits_{i=0}^{\infty}\frac{h^{(i)}(g_0(x))}{i!}\times (g_1(x)-g_0(x))^i\bmod x^n\\ \equiv \sum\limits_{i=0}^{1}\frac{h^{(i)}(g_0(x))}{i!}\times (g_1(x)-g_0(x))^i\bmod x^n\\ \equiv h(g_0(x))+h'(g_0(x))\times (g_1(x)-g_0(x))\bmod x^n \]

\(h(g_1(x))\equiv 0\)

\[h(g_0(x))+ h'(g_0(x))\times (g_1(x)-g_0(x))\equiv 0\bmod x^n\\ \Rightarrow \ln g_0(x)-f(x)+\frac{1}{g_0(x)}\times (g_1(x)-g_0(x))\equiv 0 \bmod x^n\\ \Rightarrow g_1(x)\equiv g_0(x)(f(x)-\ln g_0(x))+g_0(x)\equiv 0 \bmod x^n \]

所以 \(g_1(x)\) 就可以算了。

2.5.6 多项式快速幂

\(f^k(x)\bmod x^n\)。首先可以直接用快速幂。

还有一种方法:

\[f^k(x)=e^{k\ln f(x)} \]

所以可以用多项式取对数加多项式 exp 解决。

3 例题

3.1 例题 1

给定一个字符串 \(s\) 和一个字符串 \(t\) ,其中 \(t\) 中可能有通配符:也就是可以是任何字符的字符。

\(s\)\(t\) 有多少个位置可以匹配。

我们先不考虑通配符,如何用多项式乘法来做这个事情。

一下我们省略所有的 \(x\)

考虑到如果我们把 \(s\)\(t\) 看做两个多项式的话,我们实际上是要算这个东西:

\[\sum\limits_{i=0}^n[\sum\limits_{j=0}^m(s_{i+j}-t_j)^2=0] \]

我们令 \(a_i=\sum\limits_{j=0}^m(s_{i+j}-t_j)^2\)

然后有:

\[a_i=\sum\limits_{j=0}^m(s_{i+j}^2-2s_{i+j}t_j+t_j^2) \]

不难发现 \(s_{i+j}^2,t_j^2\) 都可以预处理出来,所以我们不用考虑它们,我们只需要考虑 \(2s_{i+j}t_j\) 怎么做。

观察多项式乘法的形式,我们可以考虑让 \(t_j\) 翻转,即 \(t'_j=t_{m-j}\)

然后我们令:

\[b_{i+m}=\sum\limits_{j=0}^ms_{i+j}t'_{m-j} \]

其实这个是满足多项式乘法的形式的,我们来证明一下。

\[b_{i+m}=\sum\limits_{i=0}^n\sum\limits_{j=0}^ms_{i+j}t'_{m-j}\\ J=m-j,I=i+j\\ b_{I+J}=\sum\limits_{I=0}^n\sum\limits_{J=0}^ms_I+t'_J \]

最后这个式子就是标准的多项式乘积的形式。

至于通配符,我们只需要让通配符的 \(t_i=0\) ,然后令:

\[a_i=\sum\limits_{j=0}^m(s_{i+j}^2-2s_{i+j}t'_j+t'_{j}{^2}) \]

就可以做了。

3.2 例题2

\(n\) 个线段 \(a_i\) 从中任意选出 \(3\) 条,问能够组成三角形的概率是多少。

\(n,a_i\le 10^5\)

这个问题本质上是一个计数问题。

我们首先令 \(c_i\) 表示长度为 \(i\) 的线段有多少条。

然后令 \(C(x)=\sum\limits_{i=0}^nc_ix^i\) 。我们令 \(D=C\times C\) 。那么 \(D_i\) 表示的就是能够选两条线段组成长度为 \(i\) 的方案数是多少。我们考虑用所有的方案减去不合法的方案,所有的方案就是 \(\dbinom{n}{3}\)

而不合法的方案呢,我们考虑枚举最长的那一条边 \(l\) ,那么 \(\sum\limits_{i=1}^lD_i\) 就是不合法的方案。

这个题就做完了。

posted @ 2021-06-15 16:44  hyl天梦  阅读(315)  评论(4编辑  收藏  举报