快速沃尔什变换 FWT
给定两个数组 $A,B$,求出 $A,B$ 经过下列变换之后得到的 $C$ 数组,快速计算。
$$
C_i = \sum_{j \oplus k=i} A_jB_k
$$
容易发现,快速沃尔什变换就是对下标进行卷积,当 $\oplus$ 为加法的时候就是 FFT/NTT。
其中 $\oplus$ 为 $\operatorname{or},\operatorname{and},\operatorname{xor}$ 中的任意一个,它们分别被称作:或卷积,与卷积,异或卷积。
或卷积
求解或卷积,即为求解:
$$
C_i = \sum_{j | k=i} A_jB_k
$$
首先如果我们设 $A0_i = \sum_{j \in i} A_j,B0_i=\sum_{j \in i} B_j$,那么我们有:
$$
\begin{aligned}
C0i &= \sum_{j \in i} C_j \\
&= \sum_{k|l \in i} A_kB_l \\
&= \sum_{k\in i,l \in i} A_kB_l \\
&= \sum_{k\in i} A_k \sum_{l \in i} B_l \\
&= A0_i B0_i \\
\end{aligned}
$$
而 $A \to A0,B\to B0,C0\to C$ 都很好算,高维前缀和即可,所以时间复杂度就被优化成了 $O(n2^n)$。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| inline void gor(ll *a,ll *b,ll *c,ll n){ for(ll i=0;i<n;i++) fa[i]=a[i],fb[i]=b[i]; for(ll i=1;i<n;i*=2){ for(ll j=0;j<n;j+=2*i){ for(ll k=j;k<j+i;k++) (((fa[k+i]+=fa[k])>=mod)&&(fa[k+i]-=mod)),(((fb[k+i]+=fb[k])>=mod)&&(fb[k+i]-=mod)); } } for(ll i=0;i<n;i++) c[i]=fa[i]*fb[i]%mod; for(ll i=1;i<n;i*=2){ for(ll j=0;j<n;j+=2*i){ for(ll k=j;k<j+i;k++) ((c[i+k]-=c[k])<0&&(c[i+k]+=mod)); } } for(ll i=0;i<n;i++) c[i]=(c[i]%mod+mod)%mod; }
|
与卷积
求解与卷积,即为求解:
$$
C_i = \sum_{j \And k=i} A_jB_k
$$
与或卷积一样的,只不过全部 $j \in i$ 换成了 $i \in j$,公式如下:
首先如果我们设 $ A0_i = \sum_{i \in j} A_j , B0_i=\sum_{i \in j} B_j $,那么我们有:
$$
\begin{aligned}
C0i &= \sum_{i \in j} C_j \\
&= \sum_{i \in (k \And l)} A_kB_l \\
&= \sum_{i\in k,i \in l} A_kB_l \\
&= \sum_{i\in k} A_k \sum_{i \in l} B_l \\
&= A0_i B0_i \
\end{aligned}
$$
求解 C 是一样的,所以就是高维后缀和,时间复杂度 $O(n2^n)$。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| inline void gand(ll *a,ll *b,ll *c,ll n){ for(ll i=0;i<n;i++) fa[i]=a[i],fb[i]=b[i]; for(ll i=1;i<n;i*=2){ for(ll j=0;j<n;j+=2*i){ for(ll k=j;k<j+i;k++) (((fa[k]+=fa[k+i])>=mod)&&(fa[k]-=mod)),(((fb[k]+=fb[k+i])>=mod)&&(fb[k]-=mod)); } } for(ll i=0;i<n;i++) c[i]=fa[i]*fb[i]%mod; for(ll i=1;i<n;i*=2){ for(ll j=0;j<n;j+=2*i){ for(ll k=j;k<j+i;k++) ((c[k]-=c[i+k])<0&&(c[k]+=mod)); } } for(ll i=0;i<n;i++) c[i]=(c[i]%mod+mod)%mod; }
|
异或卷积
求解异或卷积,即为求解:
$$
C_i = \sum_{j \operatorname{xor} k=i} A_jB_k
$$
这个推导有点复杂,所以我们考虑设代码中的 $f_k=a,f_{k+i}=b$,那么或卷积是 $b \gets a+b$,与卷积是 $a \gets a+b$,异或卷积是 $a \gets a+b,b \gets a-b$。
然后最后我们要逆运算回去,异或与与和或不同,因为我们两者加起来发现多了一倍,而不是用 $a \gets a-b$ 或者 $b \gets b-a$,因此每次用 $a \gets \frac{a+b}2,b \gets \frac{a-b}{2}$ 即可。
具体的推导过程可以用当 $n=2^1$ 的时候特殊情况推导,这个留作复习的时候思考吧。
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| inline void gxor(ll *a,ll *b,ll *c,ll n){ for(ll i=0;i<n;i++) fa[i]=a[i],fb[i]=b[i]; for(ll i=1;i<n;i*=2){ for(ll j=0;j<n;j+=2*i){ for(ll k=j;k<j+i;k++){ fa[k] += fa[k+i],fa[k+i] = fa[k]-fa[k+i]-fa[k+i]; fa[k] %= mod,fa[k+i] %= mod; fb[k] += fb[k+i],fb[k+i] = fb[k]-fb[k+i]-fb[k+i]; fb[k] %= mod,fb[k+i] %= mod; } } } for(ll i=0;i<n;i++) c[i]=fa[i]*fb[i]%mod; for(ll i=1;i<n;i*=2){ for(ll j=0;j<n;j+=2*i){ for(ll k=j;k<j+i;k++){ c[k] += c[k+i],c[k+i] = c[k]-c[k+i]-c[k+i]; c[k] = c[k]*inv2%mod,c[k+i] = c[k+i]*inv2%mod; } } } for(ll i=0;i<n;i++) c[i]=(c[i]%mod+mod)%mod; }
|
FWT 的一些性质
上面的三个运算统称为位运算卷积,其实只有或卷积是 FWT,其他的有的叫做莫比乌斯变换等等。
并且由 $A \to A0$ 的过程叫做 FWT 变换,从 $A0\to A$ 的过程叫做 FWT 逆变换(IFWT),其实跟 FFT/NTT 很像。
运算律
我们可以发现,每次从 $ A $ 数组变化到 $ A0 $ 数组都可以看作是一个矩阵乘法线性变换的过程,比如 $ A0_{i}=\sum_{j \in i} A_j $ 可以写作 $ p_{j,i}=[j \in i] $(转移矩阵),因此 FWT 具有结合律和分配律而不具有交换律。
所以如果我们在某个算法中会用到 FWT 变换的话,大部分情况都可以在程序最开始先令 $ A \to A0 $,然后用 $A0$ 进行变换,最后用 $ A0 $ 还原 $ A $ 即可,但是要注意,算法里面必须进行的是线性变换才可以这么处理,例题可以参见 P5406 THUPC2019 找树。
或者我们如果遇到了要求 $ A_i = \sum_{n_0}\sum_{n_1}\sum_{n_2}\dots\sum_{n_k}[n_0|n_1|n_2|\dots|n_k=i]B_{n_0}B_{n_1}\dots B_{n_k} $ 这类题目的话,可以对 $B$ 直接进行 FWT 变换得到 $ B0 $,然后令 $ A0=B0^{k+1} $,最后通过 $ A0 \to A $ 即可,这样的话时间复杂度通常会少一个 $ \log $,是一个很大的优化。(证明可以通过上面的分配律或者把式子写出来展开即可)
分位考虑
如果我们要求对于二进制下的第 $pos$ 位采用了规定的 或/与/异或 的运算法则,那 FWT 还适用吗?
答案是肯定的,我们只需要在代码的第一层枚举中记录位数,然后位数是什么就运用哪种变换就可以了,比如下面的 FWT 和 IFWT 就是这样的。
顺带解释一下第一层 for
循环是枚举高维前缀和处理的位数,第二层是枚举没有处理的位数,第三层是枚举处理过的位数,可以想象一下 $ \text{second_num}+1\text{(第一层枚举的位)}+\text{third_num} $。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| inline poly fwt(poly a){ poly fa = a; for(ll i=1,pos=0;i<(1<<w);i*=2,pos++){ for(ll j=0;j<(1<<w);j+=2*i){ for(ll k=j;k<j+i;k++){ if(s[pos]=='|'){ ((fa.a[k+i]+=fa.a[k])>=mod)&&(fa.a[k+i]-=mod); } if(s[pos]=='&'){ ((fa.a[k]+=fa.a[k+i])>=mod)&&(fa.a[k]-=mod); } if(s[pos]=='^'){ ((fa.a[k]+=fa.a[k+i])>=mod)&&(fa.a[k]-=mod),fa.a[k+i] = fa.a[k]-fa.a[k+i]*2; } } } } for(ll i=0;i<(1<<w);i++) fa.a[i]=(fa.a[i]%mod+mod)%mod; return fa; }
inline poly ifwt(poly a){ poly c = a; for(ll i=1,pos=0;i<(1<<w);i*=2,pos++){ for(ll j=0;j<(1<<w);j+=2*i){ for(ll k=j;k<j+i;k++){ if(s[pos]=='|'){ ((c.a[k+i]-=c.a[k])<0)&&(c.a[k+i]+=mod); } if(s[pos]=='&'){ ((c.a[k]-=c.a[k+i])<0)&&(c.a[k]+=mod); } if(s[pos]=='^'){ c.a[k] += c.a[k+i],c.a[k+i] = c.a[k]-c.a[k+i]*2; c.a[k] = c.a[k]*inv2,c.a[k+i] = c.a[k+i]*inv2; } } } } for(ll i=0;i<(1<<w);i++) c.a[i]=(c.a[i]%mod+mod)%mod; return c; }
|
例题依旧是上面那道 THUPC 的题目。
子集卷积及其相关操作
子集卷积
给出数组 $A,B$ 求数组 $C$,其中:
$$
C_i = \sum_{j|k=i,j \And k=0} A_jB_k
$$
首先,我们可以发现如果没有 $j \And k=0$ 这个限制就是 FWT 或卷积,但是有一个限制就是 $j \And k=0$,考虑转化这个限制变成 $\operatorname{popcount}(j)+\operatorname{popcount}(k)=\operatorname{popcount}(i)$,于是如果我们给 $A$ 扩展一维,给 $B$ 扩展一维,并令 $A_{i}=A0_{\operatorname{popcount(i)},i}$,$B$ 同理,那么我们得到:
$$
C0_{\operatorname{popcount}(i),i} = \sum_{j|k=i} A0_{\operatorname{popcount(j)},j}B0_{\operatorname{popcount(k)},k}
$$
那么 $j$ 和 $k$ 的 $\And$ 的限制就没有了,又因为 FWT 对加法具有分配律,所以我们可以先对 $A0$ 和 $B0$ 的 $pos$ 个($pos$ 是二进制位数)维度做 FWT 变换,然后令:
$$
C1_{b,i} = \sum_{b=0}^a A1_{a,i}B1_{b-a,i}
$$
最后对 $C1$ 做 IFWT 就得到了 $C0$ 的值,进而可以得到 $C$ 的值。(其实应该是枚举了一个 $b$ 就算一次累加答案,但是因为 FWT 的分配律,所以可以枚举所有 $b$ 把结果加在一起之后再 IFWT)这个时候我们发现有些值虽然它二进制下 $1$ 的个数不等于当前维度,但是它的位置上依然有值,这是正常的(这个值没用),如果这个值对后面的计算有影响,清零即可。
总的时间复杂度是 $O(2^nn^2)$,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| inline void gor1(ll *a,ll n){ for(ll i=1;i<n;i*=2){ for(ll j=0;j<n;j+=2*i){ for(ll k=j;k<j+i;k++) (((a[k+i]+=a[k])>=mod)&&(a[k+i]-=mod)); } } }
inline void gor2(ll *a,ll n){ for(ll i=1;i<n;i*=2){ for(ll j=0;j<n;j+=2*i){ for(ll k=j;k<j+i;k++) ((a[i+k]-=a[k])<0&&(a[i+k]+=mod)); } } for(ll i=0;i<n;i++) a[i]=(a[i]%mod+mod)%mod; }
inline void times(ll *a,ll *b,ll *c,ll n){ ll limit = 0; for(ll i=0;i<n;i++) f0[__builtin_popcount(i)][i]=a[i],f1[__builtin_popcount(i)][i]=b[i],limit=max(limit,(ll)__builtin_popcount(i)); for(ll i=0;i<=limit;i++) gor1(f0[i],n),gor1(f1[i],n); for(ll i=0;i<=limit;i++) for(ll j=0;i+j<=limit;j++) for(ll k=0;k<=n;k++) f2[i+j][k]=(f2[i+j][k]+f0[i][k]*f1[j][k])%mod; for(ll i=0;i<=limit;i++) gor2(f2[i],n); for(ll i=0;i<n;i++) c[i]=f2[__builtin_popcount(i)][i]; for(ll i=0;i<=limit;i++) for(ll j=0;j<=n;j++) f1[i][j]=f2[i][j]=f0[i][j]=0; }
|
子集卷积的逆运算
如题,这一个小节主要探讨的是子集卷积除法的一些问题。
给出数组 $A,C$,求数组 $B$,这三个数组满足下面的条件:
$$
C_i = \sum_{j|k=i,j \And k=0} A_jB_k
$$
根据上面子集卷积的定义,我们得到了 $A1,B1$ 之后,通过:
$$
C1_{b,i} = \sum_{b=0}^a A1_{a,i}B1_{b-a,i}
$$
得到了 $C1$,现在同理,我们得到了 $A1,C1$,也可以通过这个式子从 $0 \sim n$ 递推得到 $B1$ 的值,很简单,这里有一道例题,大概就是把题目的 dp 转化为知道上面式子的 $C$ 和 $A$ 求 $B$ 的问题进而求解,时间复杂度依然是 $O(n^22^n)$。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| inline void div(ll *a,ll *b,ll *c,ll n){ ll limit = 0; for(ll i=0;i<n;i++) f0[__builtin_popcount(i)][i]=a[i],f1[__builtin_popcount(i)][i]=b[i],limit=max(limit,(ll)__builtin_popcount(i)); for(ll i=0;i<=limit;i++) gor1(f0[i],n),gor1(f1[i],n); for(ll i=0;i<n;i++){ ll inv = qmi(f1[0][i],mod-2,mod); f2[0][i]=f0[0][i]*inv%mod; for(ll j=1;j<=limit;j++){ f2[j][i]=f0[j][i]; for(ll k=0;k<j;k++) f2[j][i]=(f2[j][i]-f2[k][i]*f1[j-k][i]%mod+mod)%mod; f2[j][i]=f2[j][i]*inv%mod; } } for(ll i=0;i<=limit;i++) gor2(f2[i],n); for(ll i=0;i<n;i++) c[i]=f2[__builtin_popcount(i)][i]; for(ll i=0;i<=limit;i++) for(ll j=0;j<=n;j++) f1[i][j]=f2[i][j]=f0[i][j]=0; }
|
子集卷积的其他运算
同理,还有开方等等其他操作,这里简单介绍一下开方,而 $\ln,\exp$ 的问题目前不需要掌握并且需要较高的数学知识因此此处不展开。
开方,也就是:
$$
C_i = \sum_{j|k=i,j \And k=0} A_jA_k
$$
知道 $C$ 求 $A$,还是先把 $C1$ 算出来,然后得到了:
$$
C1_{b,i} = \sum_{b=0}^a A1_{a,i}A1_{b-a,i}
$$
然后我们从 $0$ 开始,$0$ 需要通过二次剩余求出来,然后其他项就可以递推了,也很简单,时间复杂度依然是简单的 $O(n^22^n)$,二次剩余基本不占时间。
动态子集卷积
这个通常用于 dp 优化里面,也就是我们知道了 $G,Q$ 两个数组,要想通过下面的方式得到 $F$ 数组:
$$
F_S Q_S = \sum_{T\in S,T \ne 0} G_TF_{S/T}
$$
$T$ 一般不可能是空集,如果强制要求 $T$ 包含某一个元素的话依然可以做。
首先我们发现 $\operatorname{popcount}(S/T)<\operatorname{popcount}(S)$,于是我们可以第 $i$ 次处理出所有 $F_S$ 满足 $\operatorname{popcount}(S)=i$,因为 $G$ 和 $Q$ 是确定的,于是我们就可以通过所有 $\operatorname{popcount}(S)<i$ 的 $F_S$ 得到这一层的 $F_S$,同样的,我们把 $\operatorname{popcount}$ 放到维度里面,假如我们得到了 $\operatorname{popcount}(S)<i$ 的 $F_S$,我们考虑如何求出 $\operatorname{popcount}(T)=i$ 的 $F_T$(像普通子集卷积那样给 $F$ 增加一维):
$$
F_{\operatorname{popcount}(T),T}Q_T=\sum_{i=1}^{\operatorname{popcount}(T)} \sum_{j|k=T} G_{i,j}F_{\operatorname{popcount}(T)-i,k}
$$
这样的话,求出来的答案一定是正确的,因为不合法的状态都会累加到不合法的 $F$ 上,完成整个过程之后把 $F_{i,j}(\operatorname{popcount}(j) \ne i)$ 置为 $0$ 即可。
容易发现,每次后面两个和式中的值都是确定的,因此我们只需要预处理出 $G$ 每一层 FWT 后的结果 $G0$ 和 $F$ 每一层 FWT 后的结果 $F0$ 即可。
然后就可以得到:
$$
F0_{\operatorname{popcount}(T),T}Q_T=\sum_{i=1}^{\operatorname{popcount}(T)} G0_{i,T}F0_{\operatorname{popcount}(T)-i,T}
$$
于是再 IFWT 回去即可,时间复杂度为 $O(n^22^n)$,例题是 P4221 WC2018 州区划分,参考代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
| #include<bits/stdc++.h> #define ll int #define mod 998244353 #define N 22
namespace IO{ inline char nc(){ static char buf[1000000],*p=buf,*q=buf; return p==q&&(q=(p=buf)+fread(buf,1,1000000,stdin),p==q)?EOF:*p++; } inline ll read(){ ll res = 0,w = 1; char c = nc(); while(c<'0'||c>'9')w=(c=='-'?-1:w),c=nc(); while(c<='9'&&c>='0')res=res*10+c-'0',c=nc(); return res*w; } char obuf[1<<21],*p34=obuf; inline void pc(char c){ p34-obuf<=(1<<20)?(*p34++=c):(fwrite(obuf,p34-obuf,1,stdout),p34=obuf,*p34++=c); } inline void write(ll x){ if(x>9) write(x/10); pc(x%10+'0'); } } using namespace IO; using namespace std;
ll n,m,p,i,j,k,x,y,et,la[N],to[N<<5],ne[N<<5],w[N],du[N],fath[N],pos,f[N][1<<N-1],g[N][1<<N-1],cntt[1<<N-1],inv[1<<N-1],sum[1<<N-1]; inline ll gf(ll x){return x==fath[x]?x:fath[x]=gf(fath[x]);}
inline ll qmi(ll a,ll b,ll p){ ll res = 1%p,t = a; while(b){ if(b&1) res=1ll*res*t%p; t=1ll*t*t%p; b>>=1; } return res; }
inline void fwt(ll *a,ll n){ for(ll i=1;i<n;i*=2){ for(ll j=0;j<n;j+=2*i){ for(ll k=j;k<j+i;k++){ (a[k+i]+=a[k])>=mod&&(a[k+i]-=mod); } } } }
inline void ifwt(ll *a,ll n){ for(ll i=1;i<n;i*=2){ for(ll j=0;j<n;j+=2*i){ for(ll k=j;k<j+i;k++){ (a[k+i]-=a[k])<0&&(a[k+i]+=mod); } } } }
int main(){ n=read(),m=read(),p=read(); for(i=1;i<=m;i++){ x=read(),y=read(); et++,ne[et]=la[x],la[x]=et,to[et]=y; et++,ne[et]=la[y],la[y]=et,to[et]=x; } for(i=1;i<=n;i++) w[i]=read(); for(i=1;i<(1<<n);i++){ ll cnt=1; for(j=1;j<=n;j++) fath[j]=j,du[j]=0; for(j=0;j<n;j++){ if((i>>j)&1){ pos=j,cntt[i]=cntt[i-(1<<j)]+1,sum[i]=sum[i-(1<<j)]+w[j+1]; for(k=la[j+1];k;k=ne[k]){ if((i>>(to[k]-1))&1) fath[gf(j+1)]=gf(to[k]),du[j+1]++; } } } for(j=0;j<n;j++) if(du[j+1]%2==1||(((i>>j)&1)&&gf(pos+1)!=gf(j+1))) cnt=0; sum[i] %= mod,inv[i] = qmi(qmi(sum[i],p,mod),mod-2,mod); if(!cnt) g[cntt[i]][i] = (g[cntt[i]][i]+qmi(sum[i],p,mod))%mod; } for(i=0;i<=n;i++) fwt(g[i],1<<n); f[0][0]=1,fwt(f[0],1<<n); for(i=1;i<=n;i++){ for(j=1;j<=i;j++) for(k=0;k<(1<<n);k++) f[i][k]=(f[i][k]+1ll*g[j][k]*f[i-j][k])%mod; ifwt(f[i],1<<n); for(j=0;j<(1<<n);j++){ if(cntt[j]==i) f[i][j]=1ll*f[i][j]*inv[j]%mod; else f[i][j]=0; } if(i<n) fwt(f[i],1<<n); } cout<<f[n][(1<<n)-1]<<endl; return 0; }
|