快速沃尔什变换(FWT)
参考资料
思路
详见 题解:P4717 【模板】快速莫比乌斯/沃尔什变换 (FMT/FWT)
实现
分治递归
- 按位或(or)
- 按位与(and)
- 按位异或(xor)
- 按位同或(xnor)
void fmt_or(ll *a,int n,int type)
{
if(n==1)return;
int mid=n>>1;
ll *a0=a,*a1=a+mid;
fmt_or(a0,mid,type);
fmt_or(a1,mid,type);
for(int i=0;i<mid;i++)a1[i]=(a1[i]+a0[i]*type+mod)%mod;
}
void fmt_and(ll *a,int n,int type)
{
if(n==1)return;
int mid=n>>1;
ll *a0=a,*a1=a+mid;
fmt_and(a0,mid,type);
fmt_and(a1,mid,type);
for(int i=0;i<mid;i++)a0[i]=(a0[i]+a1[i]*type+mod)%mod;
}
void fwt_xor(ll *a,int n,int type)
{
if(n==1)return;
int mid=n>>1;
ll *a0=a,*a1=a+mid;
fwt_xor(a0,mid,type);
fwt_xor(a1,mid,type);
for(int i=0;i<mid;i++)
{
ll x=a0[i],y=a1[i],t=type==1?1:mod+1>>1;
a0[i]=(x+y)*t%mod;
a1[i]=(x-y+mod)*t%mod;
}
}
void fwt_xnor(ll *a,int n,int type)
{
if(n==1)return;
int mid=n>>1;
ll *a0=a,*a1=a+mid;
fwt_xnor(a0,mid,type);
fwt_xnor(a1,mid,type);
for(int i=0;i<mid;i++)
{
ll x=a0[i],y=a1[i],t=type==1?1:mod+1>>1;
a0[i]=(y-x+mod)*t%mod;
a1[i]=(y+x)*t%mod;
}
}
倍增迭代
- 按位或(or)
- 按位与(and)
- 按位异或(xor)
- 按位同或(xnor)
void fmt_or(ll *a,int n,int type)
{
for(int k=1;k<n;k<<=1)
{
for(int i=0;i<n;i+=k<<1)
{
for(int j=0;j<k;j++)
{
a[i+j+k]=(a[i+j+k]+a[i+j]*type+mod)%mod;
}
}
}
}
void fmt_and(ll *a,int n,int type)
{
for(int k=1;k<n;k<<=1)
{
for(int i=0;i<n;i+=k<<1)
{
for(int j=0;j<k;j++)
{
a[i+j]=(a[i+j]+a[i+j+k]*type+mod)%mod;
}
}
}
}
void fwt_xor(ll *a,int n,int type)
{
for(int k=1;k<n;k<<=1)
{
for(int i=0;i<n;i+=k<<1)
{
for(int j=0;j<k;j++)
{
ll x=a[i+j],y=a[i+j+k],t=type==1?1:mod+1>>1;
a[i+j]=(x+y)*t%mod;
a[i+j+k]=(x-y+mod)*t%mod;
}
}
}
}
void fwt_xnor(ll *a,int n,int type)
{
for(int k=1;k<n;k<<=1)
{
for(int i=0;i<n;i+=k<<1)
{
for(int j=0;j<k;j++)
{
ll x=a[i+j],y=a[i+j+k],t=type==1?1:mod+1>>1;
a[i+j]=(y-x+mod)*t%mod;
a[i+j+k]=(y+x)*t%mod;
}
}
}
}
高维前缀和
- 按位或(or)
- 按位与(and)
void fmt_or(ll *a,int n,int type)
{
for(int i=1;i<n;i<<=1)
{
for(int j=0;j<n;j++)
{
if(i&j)a[j]=(a[j]+a[i^j]*type+mod)%mod;
}
}
}
void fmt_and(ll *a,int n,int type)
{
for(int i=1;i<n;i<<=1)
{
for(int j=0;j<n;j++)
{
if(!(i&j))a[j]=(a[j]+a[i^j]*type+mod)%mod;
}
}
}
例题
洛谷 P4717 【模板】快速莫比乌斯/沃尔什变换 (FMT/FWT)
给定长度为 的两个序列 ,设:
分别当 是 时求出 。
题解
原题链接
参考资料
- 快速沃尔什变换 - OI Wiki
- 前缀和 & 差分 - OI Wiki
- 容斥原理 - OI Wiki
- Fast Walsh–Hadamard transform - Wikipedia
- Prefix sum - Wikipedia
- 容斥原理与子集卷积(四) - 好地方bug - 知乎
- FMT(快速莫比乌斯变换) - 秋钧 - 知乎
- FWT(快速沃尔什变换)零基础详解qaq(ACM/OI) - 月下桃子树 - 知乎
题意简述
给定长度为 的两个序列 ,设:
分别当 是 时求出 。
解题思路
约定
- 为统一记号,设数据规模为:
- 为避免歧义,位运算不再使用 ,统一记为:
- 按位或:(或 )
- 按位与:(或 )
- 按位异或:
- 按位同或:
引入
快速傅立叶变换(FFT,Fast Fourier Transform)可以 计算 加法卷积(多项式乘法):
若将求和符号中的 加法()替换为 位运算(),变成 位运算卷积 该如何处理呢?
直接枚举的时间复杂度为 ,效率较低,因此需要使用 FMT 和 FWT。
简介
快速莫比乌斯变换(FMT,Fast Möbius Transform)可以 计算 或卷积 和 与卷积:
快速沃尔什变换(FWT,Fast Walsh Transform)可以 计算 异或卷积 和 同或卷积:
有些地方会将 FMT 和 FWT 统称为 FWT。
思想
FMT 和 FWT 的思想与 FFT 类似。
首先 进行 正变换:
然后 逐位相乘(点积):
最后 通过 逆变换 得到结果:
这样整体时间复杂度为 。
基础知识
位运算 & 集合
每个二进制数可以视为一个 集合,二进制位为 的位置表示对应元素属于该集合。
两个数 按位或 对应集合的 并集,而 按位与 对应 交集。
超集是补集的子集的补集,超集与子集在补集意义上互为对偶。
简单来说,就是将 取反,因此子集和超集是 对称 的。
变换 & 反演
变换(Transform)是指根据特定规则,将一个对象(如数、向量、函数、图形等)映射为另一个对象的过程。
变换的逆过程称为 逆变换,亦称 反演(Inversion)。
在数学和算法中,我们会遇到许多变换,如傅里叶变换、数论变换、莫比乌斯变换和沃尔什变换等。
这里解释一下常见变换的命名方式:
- 不带“快速”前缀的名称,指的是变换本身,并不涉及具体的实现方法。
- 而带“快速”前缀的名称,表示使用一种高效实现该变换的算法。
Zeta 变换
考虑一个集合函数 ,其中 表示 的幂集,即 的所有子集构成的集合。
定义 子集和(Sum Over Subsets)和 超集和(Sum Over Supersets):
计算 子集和 和 超集和 的过程()称为 Zeta 变换(Zeta Transform)。
莫比乌斯反演
Zeta 的逆变换()称为 莫比乌斯反演(Möbius Inversion)。
可以通过 容斥原理 证明:
以上为子集的情形,超集的证明同理。
莫比乌斯反演也广泛出现于其他数学和算法领域,如 数论中的莫比乌斯反演。
它们的共同点是在某种偏序上逆转累加关系以恢复原函数。
快速莫比乌斯变换(FMT)
原理
考虑如何构造变换规则。
若 (并集为 ),则 和 必为 的子集:
若 (交集为 ),则 和 必为 的超集:
我们可以构造:
则有:
以上两式分别对应 子集和 和 超集和,即 Zeta 变换 和 莫比乌斯反演。
分治递归
正变换
考虑如何计算 Zeta 变换。
直接枚举的时间复杂度为 ,效率较低。借鉴 FFT,我们可以考虑分治。
设序列 的前一半为 ,后一半为 。不难发现, 最高位都为 , 的最高位都为 。
那么, 的子集就是它本身的子集,而 满足条件的子集是它本身和 的子集:
其中, 表示将两个序列首尾相连(类似字符串拼接),加法()表示逐项相加。
这样我们就能在 的时间复杂度得到 和 。
逆变换
既然 的子集是它本身, 的子集是 ,即可得到反演的递推式:
按位与
同理可得:
代码实现
正变换和逆变换可以合并为一个函数:正变换取 ,逆变换取 。
综上,可用分治写出 和 的递归版本:
void fmt_or(ll *a,int n,int type)
{
if(n==1)return;
int mid=n>>1;
ll *a0=a,*a1=a+mid;
fmt_or(a0,mid,type);
fmt_or(a1,mid,type);
for(int i=0;i<mid;i++)a1[i]=(a1[i]+a0[i]*type+mod)%mod;
}
void fmt_and(ll *a,int n,int type)
{
if(n==1)return;
int mid=n>>1;
ll *a0=a,*a1=a+mid;
fmt_and(a0,mid,type);
fmt_and(a1,mid,type);
for(int i=0;i<mid;i++)a0[i]=(a0[i]+a1[i]*type+mod)%mod;
}
倍增迭代
类似 FFT,可用倍增写出 和 的更高效(常数更小)的迭代版本:
void fmt_or(ll *a,int n,int type)
{
for(int k=1;k<n;k<<=1)
{
for(int i=0;i<n;i+=k<<1)
{
for(int j=0;j<k;j++)
{
a[i+j+k]=(a[i+j+k]+a[i+j]*type+mod)%mod;
}
}
}
}
void fmt_and(ll *a,int n,int type)
{
for(int k=1;k<n;k<<=1)
{
for(int i=0;i<n;i+=k<<1)
{
for(int j=0;j<k;j++)
{
a[i+j]=(a[i+j]+a[i+j+k]*type+mod)%mod;
}
}
}
}
高维前缀和
前缀和
一维序列 的 前缀和(Prefix Sum)序列 ,表示索引 不超过 它的元素之和。
高维前缀和
假设有 个维度,每个维度的索引范围为 。
扩展到 高维前缀和,就是各维度索引都 不超过 它的元素之和。
计算高维前缀和的常用方法有两种:容斥原理 与 逐维前缀和。
容斥原理
根据 容斥原理,可得:
但该方法的时间复杂度为 ,效率较低。
逐维前缀和
注意到, 维前缀和可视为 次逐维求和:
因此,每次只考虑一个维度,固定其余维度,求若干个一维前缀和。
对所有 个维度分别求和后,即得到 维前缀和。
该方法的时间复杂度为 。
正变换
当 ,每维只有 两种索引时,高维前缀和 就是 子集和。
同理,也可以通过 高维后缀和(K-Dimensional Suffix Sum)得到 超集和。
所以,我们可以通过 高维前缀和 求出 Zeta 变换。
时间复杂度为 。
逆变换
类似地,对所有 个维度分别做差分后,可通过 高维差分 得到莫比乌斯反演。
代码实现
我们可以写出 和 的高维前缀和版本:
void fmt_or(ll *a,int n,int type)
{
for(int i=1;i<n;i<<=1)
{
for(int j=0;j<n;j++)
{
if(i&j)a[j]=(a[j]+a[i^j]*type+mod)%mod;
}
}
}
void fmt_and(ll *a,int n,int type)
{
for(int i=1;i<n;i<<=1)
{
for(int j=0;j<n;j++)
{
if(!(i&j))a[j]=(a[j]+a[i^j]*type+mod)%mod;
}
}
}
子集卷积
FMT 还可以计算 子集卷积:
第一个限制 ,可以通过 或卷积 轻松处理。
第二个限制 ,即没有公共元素,等价于 。
我们可以多开一个维度,记录 集合大小。具体令:
则有:
答案为:
时间复杂度为 。
快速沃尔什变换(FWT)
原理
考虑构造异或运算的变换。
若令 表示 中 数量的奇偶性,即:
其中 表示 二进制中 的个数。
则有:
我们可以构造:
则有:
而以上式子就是 沃尔什变换。
分治递归
正变换
我们依然考虑分治。
设序列 的前一半为 ,后一半为 。不难发现, 最高位都为 , 的最高位都为 。
当索引 的最高位为 时,无论与最高位为 还是 的 进行 运算,因 ,奇偶性不变,因此这一部分为两者之和:
当索引 的最高位为 时,与最高位为 的 运算结果为 (奇偶性不变),与最高位为 的 运算结果为 (奇偶性翻转),因此这一部分为两者之差:
综上可得:
逆变换
逆变换易得:
按位同或
同理,将 定义中的 替换为 ,即可得到同或:
代码实现
正变换和逆变换可以合并为一个函数:正变换取 ,逆变换取 。
注意:模意义下的 指 的乘法逆元,即 。
综上,可用分治写出 和 的递归版本:
void fwt_xor(ll *a,int n,int type)
{
if(n==1)return;
int mid=n>>1;
ll *a0=a,*a1=a+mid;
fwt_xor(a0,mid,type);
fwt_xor(a1,mid,type);
for(int i=0;i<mid;i++)
{
ll x=a0[i],y=a1[i],t=type==1?1:mod+1>>1;
a0[i]=(x+y)*t%mod;
a1[i]=(x-y+mod)*t%mod;
}
}
void fwt_xnor(ll *a,int n,int type)
{
if(n==1)return;
int mid=n>>1;
ll *a0=a,*a1=a+mid;
fwt_xnor(a0,mid,type);
fwt_xnor(a1,mid,type);
for(int i=0;i<mid;i++)
{
ll x=a0[i],y=a1[i],t=type==1?1:mod+1>>1;
a0[i]=(y-x+mod)*t%mod;
a1[i]=(y+x)*t%mod;
}
}
倍增迭代
同理,可用倍增写出 和 更高效(常数更小)的迭代版本:
void fwt_xor(ll *a,int n,int type)
{
for(int k=1;k<n;k<<=1)
{
for(int i=0;i<n;i+=k<<1)
{
for(int j=0;j<k;j++)
{
ll x=a[i+j],y=a[i+j+k],t=type==1?1:mod+1>>1;
a[i+j]=(x+y)*t%mod;
a[i+j+k]=(x-y+mod)*t%mod;
}
}
}
}
void fwt_xnor(ll *a,int n,int type)
{
for(int k=1;k<n;k<<=1)
{
for(int i=0;i<n;i+=k<<1)
{
for(int j=0;j<k;j++)
{
ll x=a[i+j],y=a[i+j+k],t=type==1?1:mod+1>>1;
a[i+j]=(y-x+mod)*t%mod;
a[i+j+k]=(y+x)*t%mod;
}
}
}
}
性能分析
- 测试机器:CPU Apple M3 Max,RAM 36GB
- 测试环境:macOS Sequoia 15.6,clang 17.0.0
- 编译选项:
-std=c++17 -O2
- 计时方法:调用
std::chrono::high_resolution_clock
,单位为 - 数据生成:调用
std::mt19937_64
,生成 个随机整数 - 测试方法:每个算法连续运行 次,记录耗时并计算平均值
类型/算法 | 倍增迭代 | 分治递归 | 高维前缀和 |
---|---|---|---|
或卷积() | |||
与卷积() | |||
异或卷积() | |||
同或卷积() |
参考代码
#include <bits/stdc++.h>
using namespace std;
using ll=long long;
const int mod=998244353;
const int N=20;
void fmt_or(ll *a,int n,int type)
{
for(int k=1;k<n;k<<=1)
{
for(int i=0;i<n;i+=k<<1)
{
for(int j=0;j<k;j++)
{
a[i+j+k]=(a[i+j+k]+a[i+j]*type+mod)%mod;
}
}
}
}
void fmt_and(ll *a,int n,int type)
{
for(int k=1;k<n;k<<=1)
{
for(int i=0;i<n;i+=k<<1)
{
for(int j=0;j<k;j++)
{
a[i+j]=(a[i+j]+a[i+j+k]*type+mod)%mod;
}
}
}
}
void fwt_xor(ll *a,int n,int type)
{
for(int k=1;k<n;k<<=1)
{
for(int i=0;i<n;i+=k<<1)
{
for(int j=0;j<k;j++)
{
ll x=a[i+j],y=a[i+j+k],t=type==1?1:mod+1>>1;
a[i+j]=(x+y)*t%mod;
a[i+j+k]=(x-y+mod)*t%mod;
}
}
}
}
ll A[1<<N],B[1<<N],a[1<<N],b[1<<N];
template<typename F>
void calc(F f,int n)
{
for(int i=0;i<n;i++){a[i]=A[i];b[i]=B[i];}
f(a,n,1);
f(b,n,1);
for(int i=0;i<n;i++)a[i]=a[i]*b[i]%mod;
f(a,n,-1);
for(int i=0;i<n;i++)cout<<a[i]<<' ';
cout<<'\n';
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin>>n;
n=1<<n;
for(int i=0;i<n;i++)cin>>A[i];
for(int i=0;i<n;i++)cin>>B[i];
calc(fmt_or,n);
calc(fmt_and,n);
calc(fwt_xor,n);
return 0;
}
洛谷 P6097 【模板】子集卷积
给定两个长度为 的序列 和 ,你需要求出一个序列 ,其中 满足:
其中表示按位或,表示按位与。
答案对 取模。
代码(3)
- 倍增迭代
- 分治递归
- 高维前缀和
#include <bits/stdc++.h>
using namespace std;
using ll=long long;
const int mod=1e9+9;
const int N=20;
void fmt(ll *a,int n,int type)
{
for(int k=1;k<n;k<<=1)
{
for(int i=0;i<n;i+=k<<1)
{
for(int j=0;j<k;j++)
{
a[i+j+k]=(a[i+j+k]+a[i+j]*type+mod)%mod;
}
}
}
}
ll a[N+1][1<<N],b[N+1][1<<N],c[N+1][1<<N];
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin>>n;
int m=1<<n;
for(int i=0;i<m;i++)
{
cin>>a[__builtin_popcount(i)][i];
}
for(int i=0;i<m;i++)
{
cin>>b[__builtin_popcount(i)][i];
}
for(int i=0;i<=n;i++)
{
fmt(a[i],m,1);
fmt(b[i],m,1);
}
for(int i=0;i<=n;i++)
{
for(int j=0;j<=i;j++)
{
for(int k=0;k<m;k++)
{
c[i][k]=(c[i][k]+a[j][k]*b[i-j][k])%mod;
}
}
}
for(int i=0;i<=n;i++)
{
fmt(c[i],m,-1);
}
for(int i=0;i<m;i++)
{
cout<<c[__builtin_popcount(i)][i]<<' ';
}
return 0;
}
#include <bits/stdc++.h>
using namespace std;
using ll=long long;
const int mod=1e9+9;
const int N=20;
void fmt(ll *a,int n,int type)
{
if(n==1)return;
int mid=n>>1;
ll *a0=a,*a1=a+mid;
fmt(a0,mid,type);
fmt(a1,mid,type);
for(int i=0;i<mid;i++)a1[i]=(a1[i]+a0[i]*type+mod)%mod;
}
ll a[N+1][1<<N],b[N+1][1<<N],c[N+1][1<<N];
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin>>n;
int m=1<<n;
for(int i=0;i<m;i++)
{
cin>>a[__builtin_popcount(i)][i];
}
for(int i=0;i<m;i++)
{
cin>>b[__builtin_popcount(i)][i];
}
for(int i=0;i<=n;i++)
{
fmt(a[i],m,1);
fmt(b[i],m,1);
}
for(int i=0;i<=n;i++)
{
for(int j=0;j<=i;j++)
{
for(int k=0;k<m;k++)
{
c[i][k]=(c[i][k]+a[j][k]*b[i-j][k])%mod;
}
}
}
for(int i=0;i<=n;i++)
{
fmt(c[i],m,-1);
}
for(int i=0;i<m;i++)
{
cout<<c[__builtin_popcount(i)][i]<<' ';
}
return 0;
}
#include <bits/stdc++.h>
using namespace std;
using ll=long long;
const int mod=1e9+9;
const int N=20;
void fmt(ll *a,int n,int type)
{
for(int i=1;i<n;i<<=1)
{
for(int j=0;j<n;j++)
{
if(i&j)a[j]=(a[j]+a[i^j]*type+mod)%mod;
}
}
}
ll a[N+1][1<<N],b[N+1][1<<N],c[N+1][1<<N];
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin>>n;
int m=1<<n;
for(int i=0;i<m;i++)
{
cin>>a[__builtin_popcount(i)][i];
}
for(int i=0;i<m;i++)
{
cin>>b[__builtin_popcount(i)][i];
}
for(int i=0;i<=n;i++)
{
fmt(a[i],m,1);
fmt(b[i],m,1);
}
for(int i=0;i<=n;i++)
{
for(int j=0;j<=i;j++)
{
for(int k=0;k<m;k++)
{
c[i][k]=(c[i][k]+a[j][k]*b[i-j][k])%mod;
}
}
}
for(int i=0;i<=n;i++)
{
fmt(c[i],m,-1);
}
for(int i=0;i<m;i++)
{
cout<<c[__builtin_popcount(i)][i]<<' ';
}
return 0;
}