快速沃尔什变换
1.适用问题类型
定义元素含义:
> 对于有序集合a[n],b[n],记录其中每个元素出现次数,得到频率数组A[n],B[n]。
>
> A[i],B[i]分别表示数字i分别在 集合a 与 集合b 出现的次数。
>
> ⊕是一种位运算,分别可以是 or,xor,and运算,运算结果不会使二进制位数增大。
问题:
step1:从集合a与集合b中各选一个元素x与y,使得x⊕y=i。
step2:问结果为i的数字会有多少种不同的选法,有多少种i可能会出现,出现的i中最大值为多少。
解答:
对所有满足i==j⊕k有序对<j,k>,集合a中j出现次数与集合b中k出现次数之积为当前有序对的结果贡献。
因此我们只对每个i求出需要求出segma{A[j]*B[k] | j⊕k==i},即为i最终出现的次数,记次数为C[i]。
得到公式:
[C_{i}=sum_{j oplus k=i} A_{j} B_{k}
]
这个公式如果换成 i=j+k ,那么是不是很容易联想到FFT解决卷积问题(时域的卷积等于频域的乘积)。
既然有解决i=j+k的变换,那也有解决位运算的变化,性质类似,也叫做卷积,变换后也可以卷积变为乘积。
上面由异或推导出的公式可以称作异或卷积,而针对几种卷积形式的变换,叫做沃尔什变换。
2.定理
公式推导略不会,以下结论参考了快速沃尔什变换如果有兴趣可以看
下为沃尔什变换在不同运算符下的递归定义。
(1)或运算
[FWT(A)=
left{egin{matrix}
FWT(A_{0}),FWT(A_{0}+A_{1}) &(n
eq 1)\
A & (n= 1)
end{matrix}
ight.
]
[IFWT(A)=
left{egin{matrix}
IFWT(A_{0}),IFWT(A_{1}-A_{0}) &(n
eq 1)\
A & (n= 1)
end{matrix}
ight.
]
(2)与运算
[FWT(A)=
left{egin{matrix}
FWT(A_{0}+A_{1}),FWT(A_{1}) &(n
eq 1)\
A & (n= 1)
end{matrix}
ight.
]
[IFWT(A)=
left{egin{matrix}
IFWT(A_{0}-A_{1}),IFWT(A_{1}) &(n
eq 1)\
A & (n= 1)
end{matrix}
ight.
]
(3)或运算
[FWT(A)=
left{egin{matrix}
FWT(A_{0}+A_{1}),FWT(A_{0}-A_{1}) &(n
eq 1)\
A & (n= 1)
end{matrix}
ight.
]
[IFWT(A)=
left{egin{matrix}
frac{IFWT(A_{0})+IFWT(A_{1})}{2},
frac{IFWT(A_{0})-IFWT(A_{1})}{2} &(n
eq 1)\
A & (n= 1)
end{matrix}
ight.
]
3.代码
于是乎,到了喜闻乐见的模板时间
细节写在注释中了
/************************
以下stu都表示正变换或逆变换.
stu为1时为正变换,为0是为逆
不同的卷积对应不同的最内层代码就ok变换
注意取模,对于异或卷积的除2操作需要逆元
注意a[i]和b[i]都为频次
*************************/
void get() {
//卷积转换为乘积当然要乘一下啦
//注意A和B都要从原数组经历一次FWT
//结果存在A中,B如果需要可以重复利用
for (int i = 0; i < n; i++) a[i] *= b[i];
}
void FWT(int f[], bool stu = 1) {
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++){
{//或卷积
if(!stu)f[i+j+k] += f[i+j] ;
else f[i+j+k] -= f[i+j] ;
}
{//与卷积
if(!stu) f[i+j] += f[i+j+k] ;
else f[i+j] -= f[i+j+k] ;
}
{//异或卷积
int u=f[i+j],v=f[i+j+k];
f[i+j]=u+v;
f[i-j]=u-v;
if(!stu){
f[i+j]>>=1;
f[i+j+k]>>=1;
}
}
}
}
}
4.例题
题意:从序列A中取出k个数字,求取出数字的最大异或和。输出k从1到n的所有结果。
做法:把原数组转换成频次数组a,然后对原始频次数组不断进行卷积,卷了几次就是对应几次的数值出现频次。可以先复制一份到数组b,对b进行fwt,然后对a进行fwt,将两者fwt结果相乘,在对a进行ifwt变回频次数组,的到选两个的结果,再对a进行fwt,再相乘,在ifwt,类推得到19次结果。因为单个数字范围总共为1<<18,所以进行19次就可以,后面不会有更大的结果了。
代码
#include<iostream>
#include<cstdio>
using namespace std;
typedef long long ll;
const int maxn = 2e5+50;
int n;
int a[1<<18],b[1<<18];
int ans[maxn];
//stu==1 fwt stu==0 ifwt
int MaxNum;
//O(wlogw) stu=1 fwt stu=2 ifwt
void fwt(int *arr,bool stu){//分治求fwt
for(int q=2;q<=MaxNum;q<<=1){
int k=q>>1;
for(int i=0;i<MaxNum;i+=q){
for(int j=0;j<k;j++){
int u=arr[i+j],v=arr[i+j+k];
//看情况取模
arr[i+j]=u+v;
arr[i+j+k]=u-v;
if(!stu){
arr[i+j]>>=1;//
arr[i+j+k]>>=1;
}
}
}
}
}
int main(){
int n;
MaxNum=1<<18;
scanf("%d",&n);
int tp;
for(int i=1;i<=n;i++){
scanf("%d",&tp);
a[tp]=b[tp]=1;
ans[1]=max(ans[1],tp);
}
fwt(b,true);//fwt(b),不用变回来
for(int i=2;i<=20;i++){
fwt(a,true);//fwt(a)
for(int j=0;j<MaxNum;j++){//原函数卷积等于fwt后乘积
a[j]*=b[j];
}
fwt(a,false);//ifwt() 倒回原函数
for(int j=0;j<MaxNum;j++){
if(a[j]){//只关注是否出现,所以变为1.
a[j]=1;ans[i]=max(ans[i],j);
}
}
}
for(int i=20;i<=n;i++){
ans[i]=ans[i-2];
}
for(int i=1;i<=n;i++){
printf("%d ",ans[i]);
}
return 0;
}
如果文章有错误或存在疑问,欢迎在评论区留言。