FFT/FWT 相关理论推导复习
本文下标一般从 开始,部分除法用 。
建议前置知识:矩阵乘法,复数四则运算,单位根,模素数下原根。其实也完全可以在阅读过程中到不会的当场搜索即可,需要有点矩阵乘法基础知识。
本篇文章完成周期为一周,每个式子均为作者从头到尾手推过的。所以一下子看不懂可以别急,多看看。代码中均有注释,实在看不懂可以对着代码理解。
说实话可能这适合看过一两遍 FFT 的人再来学,但是可能之后我介绍 FFT 就用这篇了。
卷积:记长为 的数组 在运算 下的卷积 ,其中 , 也为长为 的数组。
直接暴力计算卷积复杂度为 ,其中 为数组长度。
DFT-IDFT#
一般快速计算特殊卷积的方法为构造 DFT 变换:
欲构造可逆的 DFT 变换 把长度为 的数组映射到长度为 的数组,满足 。
即把卷积通过 DFT 变换 转化成对应位置的点乘。
设 DFT 的逆变换 ,此时 ,于是我们只需快速做 变换即可。
一般代码流程为:把 做 DFT 变换 存到原来的 上,对应位点乘:,把 做 IDFT 变换 后得到的结果就是 了。
一般地,不妨设 DFT 变换为线性变换,于是设 , 为 同理写成向量。
则 DFT 变换 即为把 左乘一个 矩阵:,其中 为 的可逆矩阵。
此时需要满足 。
做点一般性推导:。
于是 需要满足:。称此时的 为 DFT 矩阵。
FFT/NTT#
快速计算 ,其中 。模板题。
注:若令 ,则我们即需求出 ,即求多项式乘法。
范德蒙德矩阵#
给定 ,有 的范德蒙德矩阵为:,即 。
设 为 次单位根,则有性质:。
此时有人类智慧:令上文中 ,则此时范德蒙德矩阵 即为 DFT 变换 所需的矩阵。
证明:考虑一般性推导,此时 。
注意到此时 的项数不对,设 的最高次分别为 ,在 时的 依然需要计算。不妨暂时把 都看成长度为 的数组,不足补 即可。
为何不可取 ,此时由上述证明显然也满足 。因为此时 的行两两相等,,即 不可逆,不满足条件。
考虑如何求逆矩阵:观察 ,注意到一般情况下分母为 ,分子非 。
做改动:设矩阵 ,则类似的:
于是此时 , 就被构造为了单位矩阵。
快速做 DFT/IDFT#
设 为 次单位根,,欲快速求 ,此时 。
人类智慧:把 补齐为 的幂次,对这个过程分治:
把 看做把 带入多项式 。
分治:设 ,其中 。
递归下去,不妨设已经求得 。考虑求 :
于是只需要在分治的过程中实现一下复数类(要求复数四则运算),然后按上述过程做即可。
注意下 的选取,设看成多项式时, 的最高次数为 ,则之前取 ,于是应有 。取不取等啥的细节取决于你的实现。
IDFT 同理做即可, 等记号类似同上,只不过 。
并且把 看做把 带入多项式 。
。
由于 ,注意最后要 ,而且除的是补齐后的 并非原始的 。复杂度 。
但是暴力分治做常数太大了,于是有蝴蝶变换。稍后讲。
FFT 三次变两次优化#
欲求两个整系数多项式 的卷积 。直接朴素做 DFT-IDFT 需要对 分别做一次 DFT,对乘后的数组做一次 IDFT。总共要三次 DFT/IDFT。
考虑由于需要单位根,FFT 需要在复数类下实现。而一般需要做的卷积都是整系数多项式,于是可以充分利用复数性质。
考虑 ,于是 。
于是把 放在 的虚部上,对 做一次 DFT,对 的每一项点值平方,对 做一次 IDFT,取虚部每一项 即可。总共要两次 DFT/IDFT。
由于 FFT 常数较大,于是减少一次优化显著。同时由于 DFT/IDFT 的形式相似,于是代码中我写成了一个函数。
#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 的前半部分,观察所有递归下去的奇偶项交换都执行完后的 数组会如何变换。
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;
}
记 为补齐 的幂次后的数组长度。观察 的情况:

称这个「位逆序置换」为蝴蝶变换。记 变换后的位置为 ,则变换过程即为交换所有位置 。考虑如何求 。
对着代码理解即可
// 同样需要保证 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;
}
之后需要枚举层数,然后合并出对应的 位置。代码挺短的,看代码理解吧。
#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#
简单程度鉴定为:理解 就能理解的,代码对着 改即可。
快速计算 ,其中 。结果对 取模。模板题依然可以用P3803。
注意到 中 可以用模素数下的原根代替,其中由于补齐到 的幂次了, 可以表示为 。
具体的:设 为模素数 下的原根,其中 为题目给定的模数。此时取 则满足上文单位根需要的所有性质。
注意:此时素模数 要满足,对于最高的 ,有 。
直接把所有复数运算替换成简单的加法乘法取模即可。然后原根幂次要处理一下。常数和代码量均减少许多。
尽管此时要三次 DFT/IDFT,但由于不需要复数类速度依然吊打 FFT。
#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 的算法进行一些小修改。
考虑 。有:。即 。
于是对 做 IDFT 相当于先对 做一次 DFT,翻转 ,然后对 都 即可。这时就不需要 的逆元了。
#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 是线性变换,对于矩阵乘法形式,有:。
考虑一种情况:要求 的各项系数。若直接两次多项式乘法相加需要 次 DFT,两次 IDFT。
而考虑 次 DFT 后变成了 ,此时令 ,其中 表示 。然后对 做一次 IDFT 即可。
此时减少了一次 IDFT!
考虑若求 ,则可以对三个多项式做一次 DFT,然后把三处点值相乘,再做一次 IDFT 即可。
通过线性性可以减少许多冗余的 DFT/IDFT。
Super NTT(DIT-DIF)#
超级快速科技啊!科技目的为省去蝴蝶变换,优化常数。能写出飞速的 NTT,可以再也别担心多项式题卡常了!
参考文章 1,参考文章 2,有些大神看这里的转移图就能理解了。
下文记 为蝴蝶变换。显然这是线性变换,写成矩阵形式为:,其中 矩阵满足 。
注意由于蝴蝶变换的性质,有:,其中 为单位矩阵。
考虑从 IDFT 开刀。若令 ,则按照先前 IDFT 流程做就可以免去蝴蝶变换。
记此时 DFT 矩阵为 ,则 ,于是 。
则 ,第二个等号是由于只有 。
验证下 是否满足 DFT 矩阵条件:,显然是满足的。
考虑如何快速计算此时的 , 请自行推导。
。
当 时,。有:
于是你从 这样枚举,然后枚举对应的 进行 变换,然后就能递归到下一层做了。也省去了蝴蝶变换!
把变换 称为做 DIF-FFT,变换 称为 DIT-FFT,此时省去了两次蝴蝶变换。同时原来 DFT 变换的所有性质也是几乎保留的。
考虑在枚举单位根的时候有 次乘法取模,考虑优化。由于上文修改了 IDFT 形式,我们只需处理 的若干幂次即可,不需要考虑 。
注意到有用的单位根形如:,其中 为 的幂次,。
于是我们把所有合法的 放在 位置,然后找单位根直接顺序遍历一个区间即可。
这样只需要 预处理加上 次连续内存的访问下标,有优秀的常数。
//洛谷 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,求长为 的数组在 次 DFT 变换后的结果。即求 ,其中 为范德蒙德矩阵。
考虑 。有:。
于是有:,其中 为单位矩阵。
于是 ,于是复杂度同 DFT 的复杂度,为 。
FWT#
快速计算 ,其中 ,需要做 的这些位运算的情况。模板题。
由于位运算,下文一般设数组长度为 ,不足补齐即可。下文 一般表示二进制下的属于。
与、或时的 FWT#
考虑一般性推导,当 时,注意到 ,于是取 即可。
同理,当 时,取 即可。
考虑快速做 DFT 变换,拿 举例,考虑快速求 ,即要快速求出 。
考虑 枚举二进制位,设枚举到第 位,所有第 位为 的值 ,看成做 dp 转移 ,初值 即可,复杂度 。
注:不明白可以想象 表示对于前 位 的所有 的和,然后做 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 变换,即对上述数组知 求 ,只需修改转移:,初值依然不变,复杂度 。
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 变换。由于位运算的按位独立性,尝试对 二进制拆位,分别拆成 。
此时与、或情况下的 可以表示为 ,其中 为 的矩阵。猜测 xor 时的 矩阵也可以如此拆位表示!
观察此时 需要满足的条件,即 ,此时 。显然 同样需要满足 存在逆矩阵。
对 取遍 列出方程组:
解得: 或
由于 可逆,于是:前者可以舍去,而后者当 分别取 时 的值应该不同。于是 或 。
由于前者关于对角线对称更加优美,于是选用前者。此时有 。
注:可以思考当 时如何模仿所述流程做,口胡一下。
此时可以刻画:,带入 中:,其中 表示二进制位为 的个数。
考虑快速做 DFT 变换,类似的,求 ,即要快速求出 。
设枚举到第 位,转移:,即考虑 的变化即可。IDFT 转移时乘个 系数即可。
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;
}
在 为位运算时,一般也称 为 DFT 矩阵。
注:注意到上文中的 矩阵只是为了刻画 优美形式的过渡式子,但看着这样简单的 矩阵形式就应该有更大的用处。详见「其他进制下的其他位运算」。
OR/AND/XOR 卷积练习#
类似的,位运算的 DFT(或者称为 FWT) 也具有线性性,部分练习需要用到。
子集卷积,请自行查看题解。
一些直接的应用:P3175,CF1208F,CF1034E,CF1713F,ARC100C,CF1234F。
更多练习可以在学习子集卷积、集合幂级数后搜索。参考 cmd 博客的最后一个部分。
其他进制下的其他位运算#
再次回顾下 时的 dp 转移,发现即为:。而 的情况也是如此。
考虑设 时的 DFT 矩阵 为 。有 ,。
而 相当于在 中提取出来,而 相当于在 中提取出来。
于是 就容易理解了。
同时这也说明:对于一般进制的一般位运算,只需找到可逆矩阵 ,就可以用类似 的方法做转移,完成 DFT/IDFT。其中 IDFT 的转移矩阵自然就是 。
一些练习#
ABC288G,相当于要对 做特殊位运算下的 IDFT,而此题 矩阵时容易找到的,求下逆矩阵按上述流程做即可。
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,思考当找不到(或不好找)可逆的 时有啥处理方法,详见题解。
k 进制异或(不进位加法)#
考虑当 表示 进制异或(不进位加法)时如何求卷积。
考虑 时的 ,求得为:,其中 ,即 次单位根。
发现这即是 时的范德蒙德矩阵,证明按 直接代数计算即可。
对长度为 的数组,复杂度为 ,其中 为枚举位数, 为矩阵乘向量的复杂度。
下文为作者还不会的,todo。
注意有时单位根 在模意义下不存在,考虑造分圆多项式为“单位根”,参考 cmd 博客中「每一维加起来 mod k」部分。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!