FFT/FWT 相关理论推导复习

本文下标一般从 0 开始,部分除法用 /

建议前置知识:矩阵乘法,复数四则运算,单位根,模素数下原根。其实也完全可以在阅读过程中到不会的当场搜索即可,需要有点矩阵乘法基础知识

本篇文章完成周期为一周,每个式子均为作者从头到尾手推过的。所以一下子看不懂可以别急,多看看。代码中均有注释,实在看不懂可以对着代码理解。

说实话可能这适合看过一两遍 FFT 的人再来学,但是可能之后我介绍 FFT 就用这篇了。

卷积:记长为 n 的数组 a,b 在运算 下的卷积 ab=c,其中 ck=ij=kaibjc 也为长为 n 的数组。

直接暴力计算卷积复杂度为 O(n2),其中 n 为数组长度。

DFT-IDFT#

一般快速计算特殊卷积的方法为构造 DFT 变换

欲构造可逆DFT 变换 把长度为 n 的数组映射到长度为 n 的数组,满足 DFT(a)i×DFT(b)i=DFT(c)i

即把卷积通过 DFT 变换 转化成对应位置的点乘。

DFT 的逆变换 IDFT=DFT1,此时 c=IDFT({DFT(a)i×DFT(b)i}),于是我们只需快速做 DFT,IDFT 变换即可。

一般代码流程为:把 a,bDFT 变换 存到原来的 a,b 上,对应位点乘:i,aiai×bi,把 aIDFT 变换 后得到的结果就是 c 了。

一般地,不妨设 DFT 变换为线性变换,于是设 u=[a0a1an1]v,wb,c 同理写成向量。

DFT 变换 即为把 u 左乘一个 n×n 矩阵:DFT(u)=Au,其中 An×n可逆矩阵。

此时需要满足 (Au)i×(Av)i=(Aw)i

做点一般性推导:(Au)i×(Av)i=(Aw)ij=0n1k=0n1Ai,jAi,kajbk=j=0n1k=0n1Ai,jkajbk

于是 A 需要满足:0i,j,k<n,Ai,jAi,k=Ai,jk。称此时的 ADFT 矩阵。

FFT/NTT#

快速计算 a×b=c,其中 ck=i+j=kaibj模板题

:若令 f(x)=i=0naixi,g(x)=i=0mbixi,h(x)=i=0n+mcixi,则我们即需求出 h(x)=f(x)g(x),即求多项式乘法。

范德蒙德矩阵#

给定 x0,x1,,xn1,有 n×n 的范德蒙德矩阵为:A:Ai,j=xji,即 A=[111x0x1xn1x0n1x1n1xn1n1]

wnn 次单位根,则有性质:wnn=1,wnn/2=1,wnk=cos(2kπn)+isin(2kπn)

此时有人类智慧:令上文中 xi=wni,则此时范德蒙德矩阵 A:Ai,j=wnij 即为 DFT 变换 所需的矩阵。

证明:考虑一般性推导,此时 Ai,jAi,k=wnijwnik=wni(j+k)=Ai,j+k

注意到此时 c 的项数不对,设 a,b 的最高次分别为 n,m,在 i>n 时的 ci 依然需要计算。不妨暂时a,b,c 都看成长度为 n+m 的数组,不足补 0 即可。


为何不可取 A:Ai,j=wnj,此时由上述证明显然也满足 (Au)i×(Av)i=(Aw)i。因为此时 A 的行两两相等,detA=0,即 A 不可逆,不满足条件。

考虑如何求逆矩阵:观察 (A×A)i,j=k=0n1wnikwnkj=k=0n1wn(i+j)k=1wn(i+j)n1wn(i+j),注意到一般情况下分母为 0,分子非 0()

做改动:设矩阵 B:Bi,j=wnij,则类似的:(A×B)i,j={n(i=j)1wn(ij)n1wn(ij)=0(ij)

于是此时 A1=1nBA×A1 就被构造为了单位矩阵。

快速做 DFT/IDFT#

wnn 次单位根,A:Ai,j=wnij,欲快速求 U=Au,此时 Ui=j=0n1wnijaj

人类智慧:把 n2k 补齐为 2 的幂次,对这个过程分治:

Ui=j=0n1wnijaj 看做把 x=wni 带入多项式 f(x)=j=0n1ajxj

分治:设 f(x)=f0(x2)+xf1(x2),其中 f0(x)=j=0n/21a2jxj,f1(x)=j=0n/21a2j+1xj

递归下去,不妨设已经求得 U0,i=f0(wn/2i),U1,i=f1(wn/2i)。考虑求 U

Ui=f(wni)=f0(wn2i)+wnif1(wn2i)=f0(wn/2i)+wnif1(wn/2i)=U0,i+wniU1,i

Ui+n/2=f(wni+n/2)=f0(wn2i+n)+wni+n/2f1(wn2i+n)=f0(wn/2i)wnif1(wn/2i)=U0,iwniU1,i

于是只需要在分治的过程中实现一下复数类(要求复数四则运算),然后按上述过程做即可。

注意下 2k 的选取,设看成多项式时,a,b 的最高次数为 N,M,则之前取 n=N+M,于是应有 2k>N+M。取不取等啥的细节取决于你的实现。


IDFT 同理做即可,f,U 等记号类似同上,只不过 Ui=j=0n1wnijaj

并且把 Ui=j=0n1wnijaj 看做把 x=wni 带入多项式 f(x)=j=0n1ajxj

Ui=U0,i+wniU1,i,Ui+n/2=U0,iwniU1,i

由于 A1=1nB,注意最后要 /n,而且除的是补齐后的 2k 并非原始的 n。复杂度 O(nlogn)

但是暴力分治做常数太大了,于是有蝴蝶变换。稍后讲。

FFT 三次变两次优化#

欲求两个整系数多项式 f,g 的卷积 h=f×g。直接朴素做 DFT-IDFT 需要对 f,g 分别做一次 DFT,对乘后的数组做一次 IDFT。总共要三次 DFT/IDFT

考虑由于需要单位根,FFT 需要在复数类下实现。而一般需要做的卷积都是整系数多项式,于是可以充分利用复数性质。

考虑 (f(x)+ig(x))2=f(x)2g(x)2+2f(x)g(x)i,于是 f(x)g(x)=12Im((f(x)+ig(x))2)

于是把 g 放在 f 的虚部上,对 f 做一次 DFT,对 f 的每一项点值平方,对 f 做一次 IDFT,取虚部每一项 /2 即可。总共要两次 DFT/IDFT

由于 FFT 常数较大,于是减少一次优化显著。同时由于 DFT/IDFT 的形式相似,于是代码中我写成了一个函数。

code
#include<bits/stdc++.h>
#define LL long long
#define LD double
using namespace std;
const int N=4e6+5;
const LD Pi=acos(-1.0);
int mmax,n,m,o;
inline int bger(int x){return x|=x>>1,x|=x>>2,x|=x>>4,x|=x>>8,x|=x>>16,x+1;}//bger(n) 表示 >n 的第一个 2 的幂次
struct comp
{
	LD x,y;
	inline comp operator+(const comp &xx)const{return {x+xx.x,y+xx.y};}
	inline comp operator-(const comp &xx)const{return {x-xx.x,y-xx.y};}
	inline comp operator*(const comp &xx)const{return {x*xx.x-y*xx.y,x*xx.y+y*xx.x};}
}a[N],b[N],w[N];//写个复数类,支持加减乘 
void DFT(int l,int r)
{
	if(l==r) return;int mid=(l+r)>>1;
	for(int i=l;i<=r;i++) b[i]=a[i];int t=l-1;
	for(int i=l;i<=r;i+=2) a[++t]=b[i];
	for(int i=l+1;i<=r;i+=2) a[++t]=b[i];//提取奇偶项 
	DFT(l,mid);DFT(mid+1,r);int L=mid-l+1;//递归求解 
	for(int i=l,k=0;i<=mid;i++,k++)
	{
		comp u=a[i],w={cos(o*Pi*k/L),sin(o*Pi*k/L)},v=a[i+L]*w;
		a[i]=u+v,a[i+L]=u-v;//转移合并出现在的 U,即这里的 a,
		//注意这里 pi*k/L 实际上是 2pi*k/(2L),2L 才是当前长度 n 
	}
}
int main()
{
	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);cin>>n>>m;
	for(int i=0;i<=n;i++) cin>>a[i].x;
	for(int i=0;i<=m;i++) cin>>a[i].y;mmax=bger(n+m);
	o=1;DFT(0,mmax-1);
	for(int i=0;i<mmax;i++) a[i]=a[i]*a[i];
	o=-1;DFT(0,mmax-1);
	for(int i=0;i<=n+m;i++) cout<<(int)(a[i].y/2/mmax+0.5)<<" ";
	return 0;
}

蝴蝶变换#

可是上面的 FFT 还是有点慢,考虑不递归实现。

只考虑上面实现中 DFT 的前半部分,观察所有递归下去的奇偶项交换都执行完后的 a 数组会如何变换。

void DFT(int l,int r)
{
	if(l==r) return;int mid=(l+r)>>1;
	for(int i=l;i<=r;i++) b[i]=a[i];int t=l-1;
	for(int i=l;i<=r;i+=2) a[++t]=b[i];
	for(int i=l+1;i<=r;i+=2) a[++t]=b[i];
	DFT(l,mid);DFT(mid+1,r);int L=mid-l+1;
}

n 为补齐 2 的幂次后的数组长度。观察 n=8 的情况:

称这个「位逆序置换」为蝴蝶变换。记 i 变换后的位置为 revi,则变换过程即为交换所有位置 (i,revi)。考虑如何求 rev0,1,,n1

对着代码理解即可
// 同样需要保证 len 是 2 的幂
// 记 rev[i] 为 i 翻转后的值
void change(Complex y[], int len) {
  for (int i = 0; i < len; ++i) {
    rev[i] = rev[i >> 1] >> 1;
    if (i & 1) {  // 如果最后一位是 1,则翻转成 len/2
      rev[i] |= len >> 1;
    }
  }
  for (int i = 0; i < len; ++i) {
    if (i < rev[i]) {  // 保证每对数只翻转一次
      swap(y[i], y[rev[i]]);
    }
  }
  return;
}

之后需要枚举层数,然后合并出对应的 i,i+n/2 位置。代码挺短的,看代码理解吧。

code
#include<bits/stdc++.h>
#define LL long long
#define LD double
using namespace std;
const int N=4e6+5;
const LD Pi=acos(-1.0);
int mmax,n,m,rev[N];
inline int bger(int x){return x|=x>>1,x|=x>>2,x|=x>>4,x|=x>>8,x|=x>>16,x+1;}//bger(n) 表示 >n 的第一个 2 的幂次
struct comp
{
	LD x,y;
	inline comp operator+(const comp &xx)const{return {x+xx.x,y+xx.y};}
	inline comp operator-(const comp &xx)const{return {x-xx.x,y-xx.y};}
	inline comp operator*(const comp &xx)const{return {x*xx.x-y*xx.y,x*xx.y+y*xx.x};}
}a[N],b[N],w[N];//写个复数类,支持加减乘 
void DFT(comp *a,int n,int o)
{
	for(int i=1;i<n;i++) rev[i]=rev[i>>1]>>1,(i&1)&&(rev[i]|=n/2);//求 rev 
	for(int i=0;i<n;i++) if(i<rev[i]) swap(a[i],a[rev[i]]);//交换,注意别交换两次了 
	for(int i=1;i<n;i<<=1)//枚举层 
	{
		for(int j=0;j<n;j+=(i<<1))//枚举区间起点 
		{
			for(int k=j,K=0;k<j+i;k++,K++)//枚举区间中每一个点
			//这里建议自己画一个 n=8 的图理解枚举的 i,j,k 
			{
				comp u=a[k],w={cos(o*Pi*K/i),sin(o*Pi*K/i)},v=a[k+i]*w;
				a[k]=u+v,a[k+i]=u-v;//转移文中写的 i,i+n/2 
			}
		}
	}
}
int main()
{
	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);cin>>n>>m;
	for(int i=0;i<=n;i++) cin>>a[i].x;
	for(int i=0;i<=m;i++) cin>>a[i].y;mmax=bger(n+m);
	DFT(a,mmax,1);
	for(int i=0;i<mmax;i++) a[i]=a[i]*a[i];
	DFT(a,mmax,-1);
	for(int i=0;i<=n+m;i++) cout<<(int)(a[i].y/2/mmax+0.5)<<" ";
	return 0;
}

同样优化显著。

NTT#

简单程度鉴定为:理解 FFT 就能理解的,代码对着 FFT 改即可。

快速计算 a×b=c,其中 ck=i+j=kaibj结果对 998244353 取模。模板题依然可以用P3803

注意到 FFTwn 可以用模素数下的原根代替,其中由于补齐到 2 的幂次了,n 可以表示为 2k

具体的:设 g 为模素数 p 下的原根,其中 p 为题目给定的模数。此时取 w2k=g(p1)/2k 则满足上文单位根需要的所有性质。

注意:此时素模数 p 要满足,对于最高的 2k,有 2kp1

常见模数 998244353=119×223+1,有原根 3167772161=5×225+1,有原根 3原根表 1原根表 2

直接把所有复数运算替换成简单的加法乘法取模即可。然后原根幂次要处理一下。常数和代码量均减少许多。

尽管此时要三次 DFT/IDFT,但由于不需要复数类速度依然吊打 FFT

code
#include<bits/stdc++.h>
#define LL long long
//#define LD double
using namespace std;
const int N=4e6+5,mod=998244353,g=3,ig=332748118;//ig 为 3 的逆元 
//const LD Pi=acos(-1.0);
int mmax,n,m,rev[N],a[N],b[N];
inline int md(int x){return x>=mod?x-mod:x;}
inline int ksm(int x,int p){int s=1;for(;p;(p&1)&&(s=1ll*s*x%mod),x=1ll*x*x%mod,p>>=1);return s;}
inline int bger(int x){return x|=x>>1,x|=x>>2,x|=x>>4,x|=x>>8,x|=x>>16,x+1;}//bger(n) 表示 >n 的第一个 2 的幂次
void DFT(int *a,int n,int o)
{
	for(int i=1;i<n;i++) rev[i]=rev[i>>1]>>1,(i&1)&&(rev[i]|=n/2);//求 rev 
	for(int i=0;i<n;i++) if(i<rev[i]) swap(a[i],a[rev[i]]);//交换,注意别交换两次了 
	for(int i=1;i<n;i<<=1)
	{
		int s=ksm(o?g:ig,(mod-1)/(i<<1));//表示 w_{2*i} 单位根所对应的数 
		for(int j=0;j<n;j+=(i<<1))
		{
			for(int k=j,w=1;k<j+i;k++,w=1ll*w*s%mod)//w 循环遍历了 w_{2*i}^K
			{
				int u=a[k],v=1ll*a[k+i]*w%mod;
				a[k]=md(u+v),a[k+i]=md(u-v+mod);
			}
		}
	}
}
int main()
{
	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);cin>>n>>m;
	for(int i=0;i<=n;i++) cin>>a[i];
	for(int i=0;i<=m;i++) cin>>b[i];mmax=bger(n+m);
	DFT(a,mmax,1);DFT(b,mmax,1);
	for(int i=0;i<mmax;i++) a[i]=1ll*a[i]*b[i]%mod;
	DFT(a,mmax,0);int I=ksm(mmax,mod-2);
	for(int i=0;i<=n+m;i++) cout<<1ll*a[i]*I%mod<<" ";
	return 0;
}

为了更简洁(方便下面优化),对 IDFT 的算法进行一些小修改。

考虑 ()。有:A2:Ai,j2={n(ni+j)0(ni+j)。即 A2=[n00000000n000n000n000n000]()

于是对 aIDFT 相当于先对 a 做一次 DFT,翻转 a1,2,,n1,然后对 a0,2,,n1/n 即可。这时就不需要 g 的逆元了。

code
#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int N=4e6+5,mod=998244353,g=3;
int mmax,n,m,rev[N],a[N],b[N];
inline int md(int x){return x>=mod?x-mod:x;}
inline int ksm(int x,int p){int s=1;for(;p;(p&1)&&(s=1ll*s*x%mod),x=1ll*x*x%mod,p>>=1);return s;}
inline int bger(int x){return x|=x>>1,x|=x>>2,x|=x>>4,x|=x>>8,x|=x>>16,x+1;}//bger(n) 表示 >n 的第一个 2 的幂次
void DFT(int *a,int n,int o)
{
	for(int i=1;i<n;i++) rev[i]=rev[i>>1]>>1,(i&1)&&(rev[i]|=n/2);//求 rev 
	for(int i=0;i<n;i++) if(i<rev[i]) swap(a[i],a[rev[i]]);//交换,注意别交换两次了 
	for(int i=1;i<n;i<<=1)
	{
		int s=ksm(g,(mod-1)/(i<<1));//都用 g 来做 DFT 
		for(int j=0;j<n;j+=(i<<1))
		{
			for(int k=j,w=1;k<j+i;k++,w=1ll*w*s%mod)//w 循环遍历了 w_{2*i}^K
			{
				int u=a[k],v=1ll*a[k+i]*w%mod;
				a[k]=md(u+v),a[k+i]=md(u-v+mod);
			}
		}
	}
	if(!o)
	{
		reverse(a+1,a+n);int I=ksm(n,mod-2);
		for(int i=0;i<n;i++) a[i]=1ll*a[i]*I%mod;
	}//做 IDFT 要 rev 一下然后 /n 
}
int main()
{
	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);cin>>n>>m;
	for(int i=0;i<=n;i++) cin>>a[i];
	for(int i=0;i<=m;i++) cin>>b[i];mmax=bger(n+m);
	DFT(a,mmax,1);DFT(b,mmax,1);
	for(int i=0;i<mmax;i++) a[i]=1ll*a[i]*b[i]%mod;
	DFT(a,mmax,0);
	for(int i=0;i<=n+m;i++) cout<<a[i]<<" ";
	return 0;
}

DFT/IDFT 的线性性#

注意到 DFT/IDFT 是线性变换,对于矩阵乘法形式,有:Au+Av=A(u+v)

考虑一种情况:要求 f(x)g(x)+F(x)G(x) 的各项系数。若直接两次多项式乘法相加需要 4DFT,两次 IDFT

而考虑 4DFT 后变成了 u,v,U,V,此时令 ui=uivi+UiVi,其中 ui 表示 [xi]u(x)。然后对 u 做一次 IDFT 即可。

此时减少了一次 IDFT!


考虑若求 f(x)g(x)h(x),则可以对三个多项式做一次 DFT,然后把三处点值相乘,再做一次 IDFT 即可。

通过线性性可以减少许多冗余的 DFT/IDFT

Super NTT(DIT-DIF)#

超级快速科技啊!科技目的为省去蝴蝶变换,优化常数。能写出飞速的 NTT,可以再也别担心多项式题卡常了!

参考文章 1参考文章 2,有些大神看这里的转移图就能理解了。

下文记 H(a) 为蝴蝶变换。显然这是线性变换,写成矩阵形式为:uHu,其中 H 矩阵满足 Hi,rev(i)=1

注意由于蝴蝶变换的性质,有:H1=H,H2=I,其中 I 为单位矩阵。

考虑从 IDFT 开刀。若令 IDFT(a)=IDFT(H(a)),则按照先前 IDFT 流程做就可以免去蝴蝶变换。

记此时 DFT 矩阵为 C,则 C1=A1×H,于是 C=HA

Ci,j=k=0n1Hi,kAk,j=Arev(i),j,第二个等号是由于只有 Hi,rev(i)=1

验证下 C 是否满足 DFT 矩阵条件:Ci,j+k=Arev(i),j+k=Arev(i),j×Arev(i),k=Ci,j×Ci,k,显然是满足的。

考虑如何快速计算此时的 DFT(a)IDFT(a) 请自行推导。

(Cu)i=j=0n1Ci,juj=j=0n1wnrev(i)juj=j=0n/21(wnrev(i)juj+wnrev(i)(j+n/2)uj+n/2)=j=0n/21wnrev(i)j(uj+(1)rev(i)uj+n/2)

0i<n/2 时,2rev(i),2rev(i+n/2)。有:

(Cu)i=j=0n/21wnrev(i)j(uj+uj+n/2)

(Cu)i+n/2=j=0n/21wnrev(i+n/2)j(ujuj+n/2)=j=0n/21wn(rev(i)+1)j(ujuj+n/2)=j=0n/21wnrev(i)j×wnj(ujuj+n/2)

于是你从 nn/2n/4 这样枚举,然后枚举对应的 i,i+n/2 进行 (ui,ui+n/2)(ui+ui+n/2,wni(uiui+n/2)) 变换,然后就能递归到下一层做了。也省去了蝴蝶变换!

把变换 Cu 称为做 DIF-FFT,变换 C1u 称为 DIT-FFT,此时省去了两次蝴蝶变换。同时原来 DFT 变换的所有性质也是几乎保留的。


考虑在枚举单位根的时候有 O(nlogn) 次乘法取模,考虑优化。由于上文修改了 IDFT 形式,我们只需处理 g 的若干幂次即可,不需要考虑 g1

注意到有用的单位根形如:wnk,其中 n2 的幂次,0k<n

于是我们把所有合法的 wnk 放在 n+k 位置,然后找单位根直接顺序遍历一个区间即可。

这样只需要 O(n) 预处理加上 O(nlogn)连续内存的访问下标,有优秀的常数。

code
//洛谷 P3803
//https://www.luogu.com.cn/problem/P3803
#include<bits/stdc++.h>
#define fr(x) freopen(#x".in","r",stdin);freopen(#x".out","w",stdout);
using namespace std;
const int mod=998244353,N=4e6+5;
int n,m,a[N],b[N],w[N],mmax;
inline int rd()
{
    int x=0,zf=1;
    char ch=getchar();
    while(ch<'0'||ch>'9') (ch=='-')and(zf=-1),ch=getchar();
    while(ch>='0'&&ch<='9') x=x*10+ch-'0',ch=getchar();
    return x*zf;
}
inline void wr(int x)
{
    if(x==0) return putchar('0'),putchar(' '),void();
    int num[35],len=0;
    while(x) num[++len]=x%10,x/=10;
    for(int i=len;i>=1;i--) putchar(num[i]+'0');
    putchar(' ');
}
inline int bger(int x){return x|=x>>1,x|=x>>2,x|=x>>4,x|=x>>8,x|=x>>16,x+1;}
inline int md(int x){return x>=mod?x-mod:x;}
inline int ksm(int x,int p){int s=1;for(;p;(p&1)&&(s=1ll*s*x%mod),x=1ll*x*x%mod,p>>=1);return s;}
//和之前一样的三个优化常数的函数
inline void init(int mmax)
{
	for(int i=1,j,k;i<mmax;i<<=1)
		for(w[j=i]=1,k=ksm(3,(mod-1)/(i<<1)),j++;j<(i<<1);j++)
			w[j]=1ll*w[j-1]*k%mod;
}//按照上面所述的方法预处理单位根
inline void DNT(int *a,int mmax)
{
	for(int i,j,k=mmax>>1,L,*W,*x,*y,z;k;k>>=1)//n->n/2->... 的枚举顺序
		for(L=k<<1,i=0;i<mmax;i+=L)
			for(j=0,W=w+k,x=a+i,y=x+k;j<k;j++,W++,x++,y++)
			//用了指针优化访问内存,注意 k 枚举的是 n/2,各个指针代表的含义请自行理解
				*y=1ll*(*x+mod-(z=*y))* *W%mod,*x=md(*x+z);
}//做 DFT 矩阵为 C 的 DFT 变换
inline void IDNT(int *a,int mmax)
{
	for(int i,j,k=1,L,*W,*x,*y,z;k<mmax;k<<=1)
		for(L=k<<1,i=0;i<mmax;i+=L)
			for(j=0,W=w+k,x=a+i,y=x+k;j<k;j++,W++,x++,y++)
				z=1ll* *W* *y%mod,*y=md(*x+mod-z),*x=md(*x+z);
	//和之前 IDFT 没啥区别,就是少了个蝴蝶变换
	//各个变量的含义和 DFT 时区别不大,区别在于 i,i+n/2 位置转移的系数
	reverse(a+1,a+mmax);//记得 rev
	for(int inv=ksm(mmax,mod-2),i=0;i<mmax;i++) a[i]=1ll*a[i]*inv%mod;//记得 /n
}//做 IDFT 矩阵为 C^{-1} 的 IDFT 变换
inline void NTT(int *a,int *b,int n,int m)
{
	mmax=bger(n+m);init(mmax);DNT(a,mmax);DNT(b,mmax);
	for(int i=0;i<mmax;i++) a[i]=1ll*a[i]*b[i]%mod;IDNT(a,mmax);
}//封装
int main()
{
	n=rd();m=rd();
	for(int i=0;i<=n;i++) a[i]=rd();
	for(int i=0;i<=m;i++) b[i]=rd();
	NTT(a,b,n,m);
	for(int i=0;i<=n+m;i++) wr(a[i]);
    return 0;
}

跑得飞快,足够应对所有多项式题了。

练习#

在此博客中有一大车总结的练习。拉一道出来讲一下:

P7844,求长为 2k 的数组在 kDFT 变换后的结果。即求 Aku,其中 A 为范德蒙德矩阵。

考虑 ()。有:A2=[n00000000n000n000n000n000]

于是有:A4=(A2)2=n2In,其中 In 为单位矩阵。

于是 Aku=(n2)k/4Akmod4u,于是复杂度同 DFT 的复杂度,为 O(nlogn)

FWT#

快速计算 ab=c,其中 ck=ij=kaibj,需要做 =and,or,xor 的这些位运算的情况。模板题

由于位运算,下文一般设数组长度为 2n,不足补齐即可。下文 一般表示二进制下的属于。

与、或时的 FWT#

考虑一般性推导,当 =or 时,注意到 [j or ki]=[ji][ki],于是取 Ai,j=[ji] 即可。

同理,当 =and 时,取 Ai,j=[ij] 即可。


考虑快速做 DFT 变换,拿 or 举例,考虑快速求 Au,即要快速求出 b:bi=jiaj

考虑 0n1 枚举二进制位,设枚举到第 k 位,所有第 k 位为 0 的值 i,看成做 dp 转移 bi+2kbi+2k+bi,初值 b=a 即可,复杂度 O(n2n)

注:不明白可以想象 bk,i 表示对于前 kji 的所有 aj 的和,然后做 dp 转移,发现可以简洁得做成上述过程。

inline void OR(modint *f) {
	for (int o = 2, k = 1; o <= n; o <<= 1, k <<= 1)
		for (int i = 0; i < n; i += o)
			for (int j = 0; j < k; j++)
				f[i+j+k] += f[i+j];
}

考虑快速做 IDFT 变换,即对上述数组知 ba,只需修改转移:bi+2kbi+2k  bi,初值依然不变,复杂度 O(n2n)

inline void IOR(modint *f) {
	for (int o = 2, k = 1; o <= n; o <<= 1, k <<= 1)
		for (int i = 0; i < n; i += o)
			for (int j = 0; j < k; j++)
				f[i+j+k] -= f[i+j];
}

异或时的 FWT#

此时不好直接构造 DFT 变换。由于位运算的按位独立性,尝试对 i,j,k 二进制拆位,分别拆成 p0,1,,n1;q0,1,,n1;r0,1,,n1

此时与、或情况下的 Ai,j 可以表示为 t=0n1fpi,qi,其中 f2×2 的矩阵。猜测 xor 时的 A 矩阵也可以如此拆位表示!

观察此时 f 需要满足的条件,即 fp,qfp,r=fp,qr,此时 =xor。显然 f 同样需要满足 f 存在逆矩阵。

q,r 取遍 {0,1}2 列出方程组:{fp,02=fp,0fp,12=fp,0fp,0fp,1=fp,1

解得:{fp,0=0fp,1=0{fp,0=1fp,1=±1

由于 f 可逆,于是:前者可以舍去,而后者当 p 分别取 0,1fp,1 的值应该不同。于是 f=[1111][1111]

由于前者关于对角线对称更加优美,于是选用前者。此时有 f1=12f

注:可以思考当 f=[1111] 时如何模仿所述流程做,口胡一下。

此时可以刻画:fp,q=(1)p and q,带入 A 中:Ai,j=(1)popcount(i and j),其中 popcount 表示二进制位为 1 的个数。


考虑快速做 DFT 变换,类似的,求 Au,即要快速求出 b:bi=j(1)popcount(i and j)aj

设枚举到第 k 位,转移:{bibi+bi+2kbi+2kbibi+2k,即考虑 popcount 的变化即可。IDFT 转移时乘个 12 系数即可。

inline void XOR(modint *f, modint x = 1) {// x 取 1 和 1/2 时分别表示 DFT 和 IDFT 变换
	for (int o = 2, k = 1; o <= n; o <<= 1, k <<= 1)
		for (int i = 0; i < n; i += o)
			for (int j = 0; j < k; j++)
				f[i+j] += f[i+j+k],
				f[i+j+k] = f[i+j] - f[i+j+k] - f[i+j+k],
				f[i+j] *= x, f[i+j+k] *= x;
}

为位运算时,一般也称 fDFT 矩阵。

:注意到上文中的 f 矩阵只是为了刻画 A 优美形式的过渡式子,但看着这样简单的 2×2 矩阵形式就应该有更大的用处。详见「其他进制下的其他位运算」。

OR/AND/XOR 卷积练习#

类似的,位运算的 DFT(或者称为 FWT) 也具有线性性,部分练习需要用到。

子集卷积,请自行查看题解。

一些直接的应用:P3175CF1208FCF1034ECF1713FARC100CCF1234F

妙妙题AGC034F,需要用到 FWT 的线性性推式子。

更多练习可以在学习子集卷积、集合幂级数后搜索。参考 cmd 博客的最后一个部分。

其他进制下的其他位运算#

再次回顾下 xor 时的 dp 转移,发现即为:[bibi+2k]=f[bibi+2k]。而 and,or 的情况也是如此。

考虑设 2k 时的 DFT 矩阵 AAk。有 f=A1Ak=[f0,0Ak1f0,1Ak1f1,0Ak1f1,1Ak1]

[bibi+2k] 相当于在 ×Ak1 中提取出来,而 [bibi+2k] 相当于在 ×Ak 中提取出来。

于是 [bibi+2k]=f[bibi+2k] 就容易理解了。

同时这也说明:对于一般进制的一般位运算,只需找到可逆矩阵 f=A1,就可以用类似 or,and,xor 的方法做转移,完成 DFT/IDFT。其中 IDFT 的转移矩阵自然就是 f1

一些练习#

ABC288G,相当于要对 a 做特殊位运算下的 IDFT,而此题 f 矩阵时容易找到的,求下逆矩阵按上述流程做即可。

code
const int N=6e5+5;
int n,pw[N],a[N],b[3][3],A[3];
inline void FWT(int *A)
{
	static int B[3];B[0]=B[1]=B[2]=0;
	for(int i=0;i<3;i++) for(int j=0;j<3;j++) B[i]+=A[j]*b[i][j];
	for(int i=0;i<3;i++) A[i]=B[i];
}//矩阵成向量,按 IDFT 流程转移
int main()
{
	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);cin>>n;
	for(int i=pw[0]=1;i<=n;i++) pw[i]=3*pw[i-1];
	for(int i=0;i<pw[n];i++) cin>>a[i];
	b[0][0]=0;b[0][1]=1;b[0][2]=-1;
	b[1][0]=1;b[1][1]=-1;b[1][2]=1;
	b[2][0]=-1;b[2][1]=1;b[2][2]=0;//b 即为求得的 f 的逆矩阵
	for(int i=0;i<n;i++) for(int j=0;j<pw[n];j++) if(!((j/pw[i])%3))
	{
		for(int k=0;k<3;k++) A[k]=a[j+k*pw[i]];FWT(A);
		for(int k=0;k<3;k++) a[j+k*pw[i]]=A[k];
	}
	for(int i=0;i<pw[n];i++) cout<<a[i]<<" ";
	return 0;
}

gym102129A,思考当找不到(或不好找)可逆的 f 时有啥处理方法,详见题解

k 进制异或(不进位加法)#

考虑当 表示 k 进制异或(不进位加法)时如何求卷积。

考虑 k=3 时的 f,求得为:[1111ωω21ω2ω4],其中 ω=w3,即 3 次单位根。

发现这即是 k 时的范德蒙德矩阵,证明按 fp,qfp,r=fp,qr 直接代数计算即可。

对长度为 kn 的数组,复杂度为 O(nkn×k2),其中 n 为枚举位数,k2 为矩阵乘向量的复杂度。


下文为作者还不会的,todo

注意有时单位根 wk 在模意义下不存在,考虑造分圆多项式为“单位根”,参考 cmd 博客中「每一维加起来 mod k」部分。

例题:CF1103E Radix sumP5577

posted @   HaHeHyt  阅读(598)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示
主题色彩
主题色彩