FWT 学习笔记
FWT 学习笔记
想尽量讲得本质一点。
首先有一个引出问题叫做 集合幂级数
其中, 是集合的并交补运算,而 也都是集合的意思
当我们把 看成二进制表示,那么集合中的每一个元素的选/不选对应二进制的 , 变成了 的一种
所以问题变成了这样:给定两个长度为 的序列 (不够长用 补全),求出一个序列 ,满足
这个时候我们可以使用,FWT , ,快速沃尔什变换。
FFT
发现 和 的英文很像,所以我们考虑,类似的思路
关于 ,我们有一个经典思路是:(图源:pyb 的 ppt)
其中,我们能做到 的 的原因在于我们有两个重要操作叫做 奇偶分段 蝴蝶变换 ,也就是说我们把原来的东西进行分治的操作后可以 的合并回去, 的 在于一个 的性质的应用,我们不妨尝试把这样的思想套进 FWT
记住上图!
FWT
现在我们有这样的目标:找到一种变换 ,使得
其中, 表示对应位相乘,然后通过 复原
显然,我们的 FWT 应该是对于原序列的一种线性组合,即某一项只是若干个原多项式中的若干项的若干倍的和
因为我们本来的 就是数个 的和,而 FWT 是对应位相乘,如果其出现原多项式某几项的乘积,那么显然 GG
所以显然有的是
这里不妨约定一些记号:
默认所有的数列都有 项,即可以理解为集合元素个数有 个
代表数列 的前 项,即最高位(第一个)不选的所有集合, 代表数列 中的后 ,即最高位(第一个)选的所有集合
FWTor
假设现在我们的 就是 ,下面简记
下面讲得可能有点小乱
FWT
总体想法是仿照 FFT ,考虑一个递归的形式构造出 FWT
显然,当 的时候,
下面讨论 的情况,我们首先考虑奇偶分段,假装我们已经知道了
于是,一个粗略的 FWT 出现了:
表示直接拼接,xjb 举一个例子:
我们知道 需要等于
还知道 ,
发现上面那个显然不靠谱,我们对应位相乘后,只会得到 这样一个错误。
也就是说后半部分还需要有 的信息甩进去,所以:(这里不妨将 FWT 当成一个神秘的抽象函数)
(回顾 的记号:)
可是这样还是有问题呀,这样对应位相乘,好像会多一个 项的贡献出现在 中
但值得注意的是, 并不一定需要就是 了,我们不妨将这个问题交给 处理,
至少我们这里保证了信息是完全的,并且符合我们心目中奇偶分段和蝴蝶变换的要求
也就是说像这样给出来后,IFWT 有操作空间,并且 FWT 时间的确是
当然,我们还需要保证
这里采用类似数学归纳法的方式证明,思路来自 pyb 的 ppt ,
即假设已知 ,那么
小心 的括号打的位置
第一个等号:按 FWT 定义展开
第二个等号:按 定义对应位相乘
第三个等号:利用归纳法得到的结论
第四个等号:把 理解成 , 理解成
第五个等号:前文在定义 Merge 的后面一点点所述:
还知道 ,
所以我们归纳证明了
IFWT
(小剧场)
FWT: 至高无上的 IFWT !吾交给汝一个使命:干掉 之后出现在 中的 项!主不需要它!
IFWT:(心里mmp:彩笔 FWT)哈哈哈!看我容斥!
(突然发现自己好智障)
直接给出来:
其中传入的 是经历了 的产物,小证一手:
所以:
FWT 的本质
我们考虑 FWT 的代码怎么写,这里再嫖一张 pyb 的 ppt

发现 做的事情就是不断地让箭头的起点加到箭头的终点,按照红绿蓝的顺序一层一层地加,可以有代码:
void FWT(ll *A){
for(int i=2;i<=N;i<<=1) //i 线段长度
for(int p=i>>1,j=0;j<N;j+=i)//j 哪一个部分
for(int k=j;k<j+p;++k) //k 嫖取信息
A[k+p]+=A[k];
}
我们仔细观察这些箭头究竟都干了什么:
对于 而言,它嫖走了 的信息
对于 而言,它嫖走了 的信息
对于 而言,它嫖走了 的信息
对于 而言,它飘走了 的信息
你发现了吗?也就是说,每个位置获取的是它二进制少掉一个 后的位置的信息
而在获取这些少掉了 的位置的信息之前,这些少掉了 的位置也获取了它们所需要的信息,所以,我们刚刚不是说 可以看成一个“抽象函数”吗,它的“解析式”其实长这样:
表示 的第 项,这里之所以用 是因为 FWT 本身其实就是在解决“集合幂级数”。
可以带入 验证一下看是不是就是
所以,我们也可以从另外一个角度去证明 FWT 的时间复杂度,即是
上面给出 的代码,其实很容易写出 的代码:
void IFWT(ll *A){
for(int i=2;i<=N;i<<=1)
for(int p=i>>1,j=0;j<N;j+=i)
for(int k=j;k<j+p;++k)
A[k+p]-=A[k];//唯一不同点
}
从代码逻辑的直观感受来看,如果我 FWT 的枚举顺序是 ,那 IFWT 的顺序是不是应该从 才对?
正确性可以这样理解,FWT 本质是线性变化,所以每一个二进制其实挺独立的,也就是满足交换律。(没有必要是从低位到高位做,也可以是任意顺序做,只是这样写比较方便)
我们甚至也可以是随便点定一个排列 ,然后按照排列 的顺序做。
更为直观的说明是,如果把每一个数字看成是一个 空间的向量,每一个二进制位代表某一个维度的坐标,那么上面实际上做的就是高维前缀和。而高维前缀和自然可以随便钦定枚举维度的顺序,然后每次 。
所以,我们容易把它整合起来:
void FWT(ll *A,int op){
for(int i=2;i<=N;i<<=1) //i 线段长度
for(int p=i>>1,j=0;j<N;j+=i)//j 哪一个部分
for(int k=j;k<j+p;++k) //k 嫖取信息
A[k+p]+=op*A[k];
}
上面从类似 FFT 的构造线性变化的角度出发,通过一点人类智慧,构造出了点值对应的形式,并证明了这样做是合法的。
但同时我们也发现 FWTor 其实就是高维前缀和的形式,而 IFWTor 就是高维差分,那么,我们能不能直接从高维前缀和/差分的角度来理解这样做 or 卷积的正确性呢?
我们考虑这个高维前缀和的形式:
我们说,与之对应的高维差分: 其实本质就是容斥原理:
考虑我们的高维前缀和实际上解决的是这样的问题:对于两个不同的组合对象,从对象 1 中选出 的方案数为 ,从对象 2 中选出 的方案数是 ,我们想要计算的是 的值
发现实际上能产生贡献的 需要满足如下条件:
发现第一个条件是比较喜欢的,因为实际上它意味着所有在 中的元素都在 中,限制比较强烈
而第二个条件涉及到一个 的问题,比较麻烦,我们考虑把这个条件容斥掉
条件的否定是:
考虑枚举 使得 满足上述条件,考虑此时 满足的条件,有
所以: 为 的高维差分,那么 并卷积的高维前缀和为 ,即 ,,而 。于是,我们从组合容斥的角度,说明了 FWT 的正确性,并了解了其高维前缀和/差分的本质。
下面是题外话,我们可以同样以这样 高维前缀和/差分的思想 去理解莫比乌斯反演,去理解 到底是咋搞出来的。
莫比乌斯反演:
若 ,构造函数 ,使得 ,有
写开:
考虑使用多元组来表示一个数,对于一个 ,我们使用 来表达
重定义 符号表示对于两个二元组 ,若 ,那么
那么,可以表示成: ,其实就是高维前缀和
所以对应的反演形式,也就是 ,可以看做是高维差分 ,而对应的
所以我们得以了解 的构造思路!
想象现在我们在做二维差分 , ,系数都长成这样:
同理,考虑在刚刚所提到的表示法之下的系数,以 为例子
,对应的
由此:
FWTand
懒得写啦!所以直接摆式子
注意到从某种意义上来说, 和 是类似的,以为
void FWT(ll *A,int opt){
for(int i=2;i<=N;i<<=1)
for(int p=i>>1,j=0;j<N;j+=i)
for(int k=j;k<j+p;++k)
A[k]+=A[k+p]*opt;
}
类似上方对于 FWTor 的组合阐述,通过 Jzzhu and Numbers 这道题,我们也可以对 FWTand 做组合阐述。
现在有若干组合对象, 分别表示从这 个组合对象里面选出集合 的方案数,求有多少种选 的方案,使得 。
方法是容斥,假如我求出了 表示 的 的方案数,那么 ,(其实就等价于 IFWTand,只不过把 ppc 相同的先加在一起了)
的求法是容易的,只需要求出 表示 的方案数,等价于对于每一个组合对象,都有 。那么这个其实也就是 FWTand 转成点值然后对应位置相乘。
那么就做完了。
使用一点更为多项式的阐述:
所以这道题的做法就是先在 下用 FWT 把 的幂次做出来,然后带入上式。
FWTxor
其实 FWTxor 的推导过程才和 FFT 是最像的,怎么说呢?
显然有 ,
FFT 的蝴蝶变换叫做:
FWTxor 直接套用:
然后你发现
然后你发现前半部分多了 ,后面部分需要多了 而且还需要变个号
所以你灵光一现,如此构造出了 IFWT:
然后你发现好像是解决了
void fwtxor(int *f,int op){
for(int i=2;i<=len;i<<=1)
for(int p=i>>1,j=0;j<len;j+=i)
for(int k=j;k<j+p;++k){
int x=f[k],y=f[k+p];
f[k]=(x+y)%mod,f[k+p]=(x-y+mod)%mod;
if(op==-1)(f[k]*=inv2)%=mod,(f[k+p]*=inv2)%=mod;
}
}
然后你开始探究 FWTxor 的本质(“解析式”)
然后你懵逼了
然后你上网看了一下:
然后你尝试探究如何从 FWT 的递推式得到它,
然后你发现你想不明白,但是模拟一下它就是对的,于是你有上网一通乱找,你找到了 pyb‘s ppt 的参考文献
(其实一言以蔽之,考虑在 下的乘法其实就是 ,所以可以从 FFT 为什么对理解 FWT 为什么对)
当你看完了巨佬的文章,你开始尝试自己推导:
这个时候你又知道这样一个式子:
其中 代表某一个大小为 的全集, 是随便自己给定了一个集合
它的正确性是很显然的,当 不为空的时候,显然会出现一些 ,从而使得这个 的值小于
当且仅当 为空集的时候,才会使得这个
然后,你利用这个式子,进行你的推导
你发现你的推导还需要一点东西,所以你来证明这个:
其中 是某个集合, 是某个集合。
首先,我们来证明 的情况,即:
(注意,下面没有采用原博客所使用的方法)
我们直接把集合看成二进制(反正上面的文章也一直把集合和二进制在混用qwq) ,下面简记
即证明
(突然发现由于写了太多,导致上面出现了奇怪的人称变化)
设 ,接下来分类讨论,对于二进制每一位,都有:
- 当前位为 , 当前位为 。那么要么是 没有当前位,要么是 均没有当前位,所以 当前位也是
- 当前位为 , 当前位为 。那么肯定是 没有当前位, 均有当前位,所以 当前位也是
- 当前位为 , 当前位为 。那么肯定是 没有当前位, 均有当前位,所以 当前位也是
- 当前位为 , 当前位为 。那么肯定是 均有当前位, 当前位为 ,在 意义下依旧满足
所以现在我们已经证明了 的情况,容易发现 的情况可以看成 的情况,解决
所以我们现在继续我们的推式子大业:
顺理成章的,我们知道了:
!!!
「CF662C」 Binary Table
CF662C Binary Table - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
延续做一道黑题写一篇题解的传统,我们直接把题解写在这里面
Statement
有一个 行 列的表格,每个元素都是 ,每次操作可以选择一行或一列,把 翻转,即把 换为 ,把 换为 。请问经过若干次操作后,表格中最少有多少个 。
Solution
注意到 很小
容易得到暴力算法:暴力枚举第 行是否翻转,这样每一行的状态就已确定,对于每一次完整的行翻转后,取每一列 个数较小的贡献即可。(因为每一列也可以翻转)
时间复杂度是 ,我们考虑优化掉这个
显然行的操作我们可以状压为 ,(二进制为 的位表示那一行要翻转)
每一列的的状态值也可以压缩:
- 用 表示在原表列中, 这个状态数量
- 用 表示 这个状态最小 的个数(即 )
当确定了行的操作为 ,那么所有状态为 的列的贡献为:
令 ,那么
所以 ,直接上 FWT 即可
复杂度:
Code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = (1<<21);
const int M = 1e5+5;
char s[25][M];
int a[N],b[N];
int n,m,ans=1e18;
void fwtxor(int *f,int op){
for(int i=2;i<=(1<<n);i<<=1)
for(int p=i>>1,j=0;j<(1<<n);j+=i)
for(int k=j;k<j+p;++k){
int x=f[k],y=f[k+p];
f[k]=x+y,f[k+p]=x-y;
if(op==-1)f[k]/=2,f[k+p]/=2;
}
}
signed main(){
scanf("%lld %lld",&n,&m);
for(int i=1;i<=n;++i)scanf("%s",s[i]+1);
for(int i=1,k;k=0,i<=m;++i,a[k]++)
for(int j=1;j<=n;++j)k=k<<1|(s[j][i]-'0');
for(int i=0,t;i<(1<<n);++i)
t=__builtin_popcount(i),b[i]=min(t,n-t);
fwtxor(a,1),fwtxor(b,1);
for(int i=0;i<(1<<n);++i)a[i]*=b[i];
fwtxor(a,-1);
for(int i=0;i<(1<<n);++i)ans=min(ans,a[i]);
printf("%lld\n",ans);
return 0;
}
子集卷积
写不动了!!
https://www.luogu.com.cn/problem/P6097
所谓子集卷积,听着很高级,其实也就那样(就最基本的而言),好像有叫做集合占位幂级数什么的
它用来处理求出这样一个 :
我们很容易处理第一个限制 ,直接 FWTand 即可。
对于第二个限制 ,所以我们不妨再开一维记录集合中的元素个数
也就是说设 ,把他们卷起来,
最后的答案即为: ,因为 ,所以相等的时候取到的就是正确的值
(即 )
#include<bits/stdc++.h>
using namespace std;
const int N = 1<<21;
const int mod = 1e9+9;
int read(){
int s=0,w=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')w=-1;ch=getchar();}
while(isdigit(ch))s=s*10+ch-'0',ch=getchar();
return s*w;
}
int a[21][N],b[21][N],c[21][N];
int n,len;
void fwt(int *f,int op){
for(int i=2;i<=len;i<<=1)
for(int p=i>>1,j=0;j<len;j+=i)
for(int k=j;k<j+p;++k)
(f[k+p]+=op*f[k])%=mod;
}
signed main(){
n=read(),len=1<<n;
for(int i=0;i<len;++i)a[__builtin_popcount(i)][i]=read();
for(int i=0;i<len;++i)b[__builtin_popcount(i)][i]=read();
for(int i=0;i<=n;++i)fwt(a[i],1),fwt(b[i],1);
for(int i=0;i<=n;++i)
for(int j=0;j<=i;++j)
for(int k=0;k<len;++k)
(c[i][k]+=(long long)a[j][k]*b[i-j][k]%mod)%=mod;
for(int i=0;i<=n;++i)fwt(c[i],-1);
for(int i=0;i<len;++i)printf("%d ",(c[__builtin_popcount(i)][i]+mod)%mod);
return 0;
}
所谓集合占位幂级数,是这样的形式:
扩展:https://www.luogu.com.cn/blog/user7035/zi-ji-juan-ji-ji-ji-gao-ji-yun-suan (我不会,等我长大后再学习)
[WC2018] 州区划分
WC2018 州区划分 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
延续做一道黑题写一篇题解的传统,我们直接把题解写在这里面
Statement
由于原题面已经概括得很好了,所以这里直接贴图:
Solution
容易考虑状压 DP,设 表示划分了 个州,选出的城市的集合为 的贡献,那么
其中, 表示集合 的 的和, 表示集合 是否可以独立成州
所谓独立成州的条件其实可以转化为 图不连通/存在奇度数点,我们容易在 左右的时间暴力预处理出
设 ,那么:
发现后面的那一坨其实就是子集卷积,所以复杂度 解决
Code
#include<bits/stdc++.h>
#define int long long
#define ppc(x) __builtin_popcount(x)
using namespace std;
const int mod = 998244353;
const int N = 22;
char buf[1<<23],*p1=buf,*p2=buf;
#define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++)
int read(){
int s=0,w=1; char ch=getchar();
while(!isdigit(ch)){if(ch=='-')w=-1;ch=getchar();}
while(isdigit(ch))s=s*10+(ch^48),ch=getchar();
return s*w;
}
int u[N*N],v[N*N],w[N],fa[N],deg[N],inv[(1<<N)+5];
int f[N][(1<<N)+5],g[N][(1<<N)+5];
int n,m,p;
bool vis[N];
void input(){
n=read(),m=read(),p=read();
for(int i=1;i<=m;++i)u[i]=read(),v[i]=read();
for(int i=1;i<=n;++i)w[i]=read();
}
int find(int x){return x==fa[x]?x:fa[x]=find(fa[x]);}
bool merge(int u,int v){
u=find(u),v=find(v);
return u==v?0:fa[u]=v;//////////////
}
int ksm(int a,int b){
int res=1;
while(b){
if(b&1)res=res*a%mod;
a=a*a%mod,b>>=1;
}
return res;
}
void preparation(){
for(int i=0,sum,cnt,flag;sum=cnt=flag=0,i<(1<<n);++i){
for(int j=1;j<=n;++j)fa[j]=j,vis[j]=false,deg[j]=0;
for(int j=0;j<n;++j)if(i&(1<<j))vis[j+1]=true,sum+=w[j+1],cnt++;
for(int j=1;j<=m;++j)if(vis[u[j]]&&vis[v[j]])
merge(u[j],v[j])&&(cnt--,1),deg[u[j]]++,deg[v[j]]++;
flag|=cnt!=1,cnt=0;
for(int j=1;j<=n;++j)cnt+=(deg[j]&1);
flag|=cnt!=0,g[ppc(i)][i]=flag*ksm(sum,p);
inv[i]=ksm(ksm(sum,mod-2),p);
}
}
void fwtor(int *f,int op){
for(int i=2;i<=(1<<n);i<<=1)
for(int j=0,p=i>>1;j<(1<<n);j+=i)
for(int k=j;k<j+p;++k)(f[k+p]+=(op*f[k]+mod)%mod)%=mod;
}
void work(){
for(int i=0;i<=n;++i)fwtor(g[i],1);
f[0][0]=1,fwtor(f[0],1);
for(int i=1;i<=n;++i){
for(int j=0;j<i;++j)//j!=i
for(int k=0;k<(1<<n);++k)
(f[i][k]+=f[j][k]*g[i-j][k]%mod)%=mod;
fwtor(f[i],-1);
for(int j=0;j<(1<<n);++j)
f[i][j]=(ppc(j)==i)*(f[i][j]*inv[j]%mod);
if(i^n)fwtor(f[i],1);
}
}
void output(){
printf("%lld\n",f[n][(1<<n)-1]);
}
signed main(){
input();
preparation();
work();
output();
return 0;
}
「CF1034E」Little C Loves 3 III
Statement
给定 和长度为 的数列 和 ,保证每个元素的值属于
生成序列 ,对于 ,有:
求 ,答案对 取模。
,时限
Solution
显然,暴力 的子集卷积不可过,考虑利用 的性质
普通的,子集卷积为了取到正确的值,采用的方法可以理解为 DP 多设一维状态 ,使得我们加入了 这一项
这里,我们让 (鬼知道是怎么想到的!)
用 FWT 求出 后, 项系数对 取模,再除以一个 即可
解释:
集合占位幂级数是这样的形式: 其中 的取值是任意的, 一般都取 , 意义是在于可能会在 位置瞎 jb 转上一些不计入答案的贡献
而当我们把 取到 , 干掉了后面那个 ,除 相当于还原
于是这样快乐 FWT 即可,
Code
#include<bits/stdc++.h>
#define int long long
using namespace std;
char s[1<<21|5],t[1<<21|5];
int f[1<<21|5],g[1<<21|5];
int n;
void fwt(int *f,int op){
for(int i=2;i<=(1<<n);i<<=1)
for(int p=i>>1,j=0;j<(1<<n);j+=i)
for(int k=j;k<j+p;++k)f[k+p]+=op*f[k];
}
signed main(){
scanf("%lld%s%s",&n,s,t);
for(int i=0;i<(1<<n);++i)f[i]=(s[i]&15ll)<<(__builtin_popcount(i)<<1);fwt(f,1);
for(int i=0;i<(1<<n);++i)g[i]=(t[i]&15ll)<<(__builtin_popcount(i)<<1);fwt(g,1);
for(int i=0;i<(1<<n);++i)f[i]*=g[i]; fwt(f,-1);
for(int i=0;i<(1<<n);++i)putchar(f[i]>>(__builtin_popcount(i)<<1)&3|48);
return 0;
}
完结撒花!!!!✿✿ヽ(°▽°)ノ✿
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 2025年我用 Compose 写了一个 Todo App
· 张高兴的大模型开发实战:(一)使用 Selenium 进行网页爬虫