FWT(1)
概述
FWT 常用于解决以下式子的 \(O(n\log n)\) 求解:
FWT 的思路和 FFT 类似,先将多项式 \(A,B\) 转化为一些特殊的点值 \(FWT(A)_0^{n-1},FWT(B)_0^{n-1}\),再将对应点值相乘求出 \(FWT(C)_i=FWT(A)_iFWT(B)_i\),然后反着做,求出 \(C\)。
这些特殊的点值前人已经告诉我们求哪 \(n\) 个,我们只用知道怎么求出它们,即 \(FWT(A),FWT(B)\);同时还需要知道怎么做 IFWT,将点值转回去。
对于不同的按位运算符,FWT 的运算有一些不同,但思路类似;其中,| 和 & 的本质相近,它们的 FWT/IFWT 也很相近。
正文
|
FWT(A)
定义
其中 \(j\in i\) 表示 \(j\) 的二进制表示的 1 的位置集合含于 \(i\) 的二进制表示中 1 的位置集合。
递推
首先显然 \(FWT(A_i)=A_i\),即一个数构成的集合的 FWT 就是这个数。
公式
解释
符号解释:\(A_0,A_1\) 分别表示长度为 \(2^n\) 次多项式的前一半和后一半,其中 \(A_0\) 的下标我们从 \(0\) 到 \(2^{n-1}-1\),\(A_1\) 的下标我们从 \(2^{n-1}\) 到 \(2^n-1\);\((A,B)\) 表示 连接 \(A,B\) 两个多项式。
考虑定义,\(\forall i\in [0,2^{n-1}-1],\in i\) 的 \(j\) 一定 \(\in [0,2^{n-1}-1]\),所以 \(FWT(A)_0=FWT(A_0)\);\(\forall i\in [2^{n-1},2^n-1]\),\(\in i\) 的 \(j\) 在 \([0,2^{n-1}-1]\) 和 \([0,2^{n-1}-1]\) 分别出现在 \(p_1,p_2,...,p_u\) 和 \(q_1,q_2,...,q_u\),并且满足 \(q_i=p_i+2^{n-1}\),因此对于 \(p\) 的部分,\(FWT(A)_1\gets FWT(A_0)\),对于 \(q\) 的部分,\(FWT(A)_1\gets FWT(A_1)\)。
IFWT(A)
递推
首先显然 \(IFWT(A_i)=A_i\),即一个数构成的集合的 IFWT 就是这个数。
公式
(就是把 + 换成了 -)
解释
\(\because FWT(A)_0=FWT(A_0),FWT(A)_1=FWT(A_0)+FWT(A_1)\)
\(\therefore A_0=IFWT(FWT(A_0))=IFWT(FWT(A)_0),A_1=IFWT(FWT(A_1))=IFWT(FWT(A)_1-FWT(A_0))=IFWT(FWT(A)_1-FWT(A)_0)\)
注:你可能会说,\(IFWT(FWT(A)_1-FWT(A)_0)\) 为什么 = \(IFWT(FWT(A)_1)-IFWT(FWT(A)_0)\)?这个显然,因为 FWT 和 IFWT 其实本质上都是将同一个多项式在不同表示法中间变换而已。
实现
能够一直递推到长为 1 的区间需要满足 \(n\) 是 2 的幂,所以如果不是就要补成 \(2^m\)。按位 | 部分的代码如下:
void FWT(int *arr,int type)://type=1->FWT,type=0->IFWT
for(int w=1;w<(1<<m);w<<=1)
for(int j=0;j<(1<<m);j+=w<<1)
for(int i=j;i<j+w;i++)
(arr[i+w]+=(arr[i]*(type?1:-1)+mod)%mod)%=mod;
&
& 和 | 非常类似,所以不推了,如果你想重复一次,可以参照 这篇博客。
FWT
递推
公式
IFWT
递推
公式
实现
void FWT(int *arr,int type):
for(int w=1;w<(1<<m);w<<=1)
for(int j=0;j<(1<<m);j+=w<<1)
for(int i=j;i<j+w;i++)
(arr[i]+=(arr[i+w]*(type?1:-1)+mod)%mod)%=mod;
^
xor 是一种完全不同的运算。
FWT(A)
定义
其中记 \(\text{popcount}(x)=c(x)\)。
递推
首先显然 \(FWT(A_i)=A_i\),即一个数构成的集合的 FWT 就是这个数。
公式
注:这里的 \(A_0,A_1\) 的下标都是从 \(0\) 到 \(2^{n-1}-1\)。
解释
说一下第二排的第一个等号,是因为 \(\forall i\in [0,2^{n-1}-1]\),\(i\&j=i\&(j+2^{n-1})\),这是因为 \(i\) 的最高位是 \(0\)。
说一下第二排的第一个等号,是因为 \(\forall i\in [0,2^{n-1}-1]\),\(i\&j+2^{n-1}=i\&(j+2^{n-1})\),这是因为 \(i\) 的最高位是 \(1\),而 \(j\) 的最高位是 \(0\),因此 \({c(i\&j)}\) 和 \(c(i\&(j+2^{n-1}))\) 就相差了 \(1\)。
IFWT(A)
递推
首先显然 \(IFWT(A_i)=A_i\),即一个数构成的集合的 IFWT 就是这个数。
公式
(就是把 ×1 换成了 ×½)
解释
\(\because FWT(A)_0=FWT(A_0)+FWT(A_1),FWT(A)_1=FWT(A_0)-FWT(A_1)\)
(两式相加除以 2)\(\therefore A_0=IFWT(\frac{FWT(A)_0+FWT(A)_1}2),\)
(两式相减除以 2)\(A_1=IFWT(\frac{FWT(A)_0-FWT(A)_1}2)\)
实现
void FWT(int *arr,int type):
for(int w=1;w<(1<<m);w<<=1)
for(int j=0;j<(1<<m);j+=w<<1)
for(int i=j;i<j+w;i++){
int u=arr[i]+arr[i+w],v=arr[i]-arr[i+w];
arr[i]=1ll*u*(type?1:I2)%mod,arr[i+w]=1ll*(v+mod)*(type?1:I2)%mod;
}
定义的原因
你可能很疑惑,为什么要定义成那种奇形怪状的式子呢?原因在于它们相乘能够得到正确的 \(FWT(A*B)[i]\)。
|
&
^
记 \(P(x)\) 为 \(x\) 的奇偶性(即 mod 2)。
引理:\(P({c(i\&j)})\operatorname{xor}P(c(i\&k))=P(c(i\&(j\operatorname{xor}k)))\)。感性理解或查看 刚才的链接。
类 FWT
Eg.1
我们 for 循环的从小到大枚举 w 显然是将两个小块合并为一个大块的倍增过程,并且假如设 \(w=2^h\),那么我们正在处理下标的二进制第 \(h\) 位(从低到高为从 \(0\) 到 \(m-1\))。不信你可以看我们每次“递推”的过程,其实依据的是那次递推的最高位的运算性质,来将更低的位们的运算结果变成当前所有位的运算结果。
这个原理有什么用呢?这说明 FWT 也是“按位”的。也就是说,如果定义一种新运算 \(\otimes\),它对不同的二进制位采用不同的按位运算符,那我们也可以用“在不同的 w 的轮中使用不同的按位运算符”来求出一种“混合沃尔什变换”。形如:
void fwt(int *arr,int type){
for(int w=1,o=0;o<=m-1;w<<=1,o++){
if(b[o]==0)continue;
else if(b[o]==1){
for(int j=0;j<(1<<m);j+=w<<1)
for(int i=j;i<j+w;i++)
(arr[i+w]+=(arr[i]*(type?1:-1)+mod)%mod)%=mod;
}
else if(b[o]==2){
for(int j=0;j<(1<<m);j+=w<<1)
for(int i=j;i<j+w;i++)
(arr[i]+=(arr[i+w]*(type?1:-1)+mod)%mod)%=mod;
}
else {
for(int j=0;j<(1<<m);j+=w<<1)
for(int i=j;i<j+w;i++){
int u=arr[i]+arr[i+w],v=arr[i]-arr[i+w];
arr[i]=1ll*u*(type?1:I2)%mod,arr[i+w]=1ll*(v+mod)*(type?1:I2)%mod;
}
}
}
}
事实上,这道模拟赛题 的核心就是这。另外,这道题是将 dp 数组 \(f_i[0],f_i[1],...,f_i[m-1]\) 看成多项式 \(f_i\),并可以显然地得到 \(f_1=T\),\(f_i=f_{i-1}\otimes T(i\ge 2)\),FWT => 点值快速幂 => IFWT,就得到了最后答案。
Eg.2 特工
化简发现 \(b_i=\sum_{P(c(j))=P({c(i\&j)})}a_j\),递推得 \(FWT(A)_0=FWT(A_0)+sum(A_1)-FWT(A_1),FWT(A)_1=FWT(A_1)+FWT(A_0)\),考虑到 \(FWT(A)_1\) 基本就是个块内前缀和了,所以 \(sum(A_1)=FWT(A)[j+2w-1]-FWT(A)[j+w-1]\) 代入,那么 IFWT 的实现方式可以就 \(w\) 倒着来一次,把上面移项化成 \(FWT(A_0)=...,FWT(A_1)=...\) 就行了。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1.5e6+5;
int n,b[N];
void IFWT(int *arr){
for(int w=n>>1;w;w>>=1)
for(int j=0;j<n;j+=w<<1){
int sum=arr[j+(w<<1)-1]-arr[j+w-1];
for(int i=j;i<j+w;i++){
int x=arr[i],y=arr[i+w];
arr[i]=(x+y-sum)/2,arr[i+w]=(y-x+sum)/2;
}
}
}
signed main(){
scanf("%lld",&n);
for(int i=0;i<n;i++)scanf("%lld",&b[i]);
IFWT(b);
for(int i=0;i<n;i++)cout<<b[i]<<' ';
}
Eg.3 魔法小程序
首先看懂他在写什么,发现类似于一个“混合进制”,就把一个数拆成了很多位。很好想到可以把 \(a\) 里头的 1 都删掉,这样不影响,从而将每个数的位数变成 log。等价于是 \(j\) 每一位都 \(\le i\) 就把 \(b_j\) 加给 \(c_i\)。这个跟 FWT 其实就是进制的区别。FWT 怎么分的呢,是把一个 \(2w\) 大块变成了两个 \(w\) 小块;如法炮制,把一个 \(a_iw\) 大块划分成 \(a_i\) 个 \(w\) 小块。IFWT 还是采用好想的倒着来一遍方式。
#include <bits/stdc++.h>
#define int long long
using namespace std;
inline int read(){
register char ch=getchar();register int x=0,f=1;
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9')x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
return x*f;
}
void print(int x){
if(x<0){putchar('-'),print(-x);return;}
if(x/10)print(x/10);
putchar(x%10+48);
}
const int N=1e6+5;
int n,m,a[N],c[N];
signed main(){
n=read(),print(n),puts("");
for(int i=1;i<=n;i++)a[i]=read(),print(a[i]),putchar(' ');puts("");a[++n]=1e9;
m=read(),print(m),puts("");
for(int i=0;i<m;i++)c[i]=read();
int nn=0;a[0]=1e9;
for(int i=1;i<=n;i++)if(a[i]!=1)a[++nn]=a[i];
n=nn;n=min(n,17ll);
int w=1,ii=1;
while(w<m)w*=a[ii++];
w/=a[--ii];
for(int i=ii;w;i--,w/=a[i]){
for(int j=0;j<m;j+=w*a[i])
for(int k=min(j+w*a[i]-1,m-1);k>=j;k--)if(k-w>=j)c[k]-=c[k-w];
}
for(int i=0;i<m;i++)print(c[i]),putchar(' ');
}

浙公网安备 33010602011771号