FWT
引入
已知 \(A,B\),用于解决 \(C_i=\sum_{j\star k=i}A_jB_k\)。其中 \(\star\) 是一种二进制运算。
我们可以构造 \(fwt\) 序列,
使得 \(A\to fwt_A\),\(B\to fwt_B\),\(fwt_C\) 为 \(fwt_A,fwt_B\) 对应位相乘,再 \(fwt_C\to C\)。
OR
显然有 \(j|i=i,k|i=i\),则 \((j|k)|i=i\)。
构造 \(fwt_A[i]=\sum_{j|i=i}A_j\)。
那么 \(fwt_A[i]\times fwt_B[i]=(\sum_{j|i=i}A_j)\times (\sum_{k|i=i} B_k)\)
\(=\sum_{(j|k)|i=i} A_jB_k=fwt_C[i]\).
\(A\to fwt_A\),设 \(merge\) 表示两个序列从左到右拼起来。
初始地,\(fwt_A[i]=A[i]\),考虑分治,设左边的 \(fwt_0\),右边的是 \(fwt_1\)。
那么 \(fwt=merge(fwt_0,fwt_0+fwt_1)\)。
\(fwt_A\to A\)。
\(A=merge(A_0,A_1-A_0)\)。可以看做只是翻转符号。
AND
同理。
XOR
定义 \(x\star y=popcount(x\& y)\bmod 2\)。那么,\((x\star y)\text{xor}(x\star z)=x\star(y\space\text{xor}\space z)\)。
构造 \(fwt_A[i]=\sum_{i\star j=0} A_j-\sum_{i\star j=1}A_j\)。
那么 \(fwt_A[i]\times fwt_B[i]=(\sum_{i\star j=0} A_j-\sum_{i\star j=1}A_j)\times (\sum_{i\star k=0} B_k-\sum_{i\star k=1}B_k)\)
\(=\sum_{i\star (j\space\text{xor}\space k)=0} A_jB_k-\sum_{i\star (j\space\text{xor}\space k)=1} A_jB_k=fwt_C[i]\)。
所以,\(fwt=merge(fwt_0+fwt_1,fwt_0-fwt_1)\)。
\(a=merge(\dfrac{a_0+a_1}{2},\dfrac{a_0-a_1}{2})\)。
事实上,我们分治的过程中,也就是,枚举二进制位。
然后对于 \(fwt_0,fwt_1\) 都考虑新填这一位的答案,并合并 \(fwt_0,fwt_1\)。
code
#include<algorithm>
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
using namespace std;
const int N=(1<<17)+10,mod=998244353;
int add(int x,int y) {
x+=y; if(x>=mod) x-=mod;
return x;
}
int n,a[N],b[N],A[N],B[N];
void in() {
for(int i=0; i<n; i++) A[i]=a[i],B[i]=b[i];
}
void out() {
for(int i=0; i<n; i++) printf("%d%c",A[i],i==n-1?'\n':' ');
}
void get() {
for(int i=0; i<n; i++) A[i]=1ll*A[i]*B[i]%mod;
}
void OR(int *f,int x) {
for(int len=2; len<=n; len<<=1) {
for(int i=0; i<n; i+=len)
for(int j=0; j<(len>>1); j++)
f[i+j+(len>>1)]=add(f[i+j+(len>>1)],1ll*f[i+j]*x%mod);
}
}
void AND(int *f,int x) {
for(int len=2; len<=n; len<<=1) {
for(int i=0; i<n; i+=len)
for(int j=0; j<(len>>1); j++)
f[i+j]=add(f[i+j],1ll*f[i+j+(len>>1)]*x%mod);
}
}
void XOR(int *f,int x) {
for(int len=2; len<=n; len<<=1) {
for(int i=0; i<n; i+=len)
for(int j=0; j<(len>>1); j++) {
int tmp=f[i+j];
f[i+j]=add(f[i+j],f[i+j+(len>>1)]);
f[i+j+(len>>1)]=add(tmp,mod-f[i+j+(len>>1)]);
f[i+j]=1ll*f[i+j]*x%mod;
f[i+j+(len>>1)]=1ll*f[i+j+(len>>1)]*x%mod;
}
}
}
int main() {
scanf("%d",&n); n=1<<n;
for(int i=0; i<n; i++) scanf("%d",&a[i]);
for(int i=0; i<n; i++) scanf("%d",&b[i]);
in(),OR(A,1),OR(B,1),get(),OR(A,mod-1),out();
in(),AND(A,1),AND(B,1),get(),AND(A,mod-1),out();
in(),XOR(A,1),XOR(B,1),get(),XOR(A,mod-mod/2),out();
return 0;
}
子集卷积
对于 fwt 的过程,只需要再加上一维 \(i\),\(A_i\) 序列存储所有 \(popcount\) 为 \(i\) 的 \(a_i\),其他为 \(0\)。
如果我们 \(|x\cup y|=|x|+|y|\),那么 \(x,y\) 是符合条件的。
即对于 \(C_i\),由所有 \(A_j\) 和 \(B_{i-j}\) 卷积再求和而来。
CF662C Binary Table
考虑枚举翻转了那些状态的行,每列分别计算可以做到 \(O(2^nm)\)。
不难发现,我们可以把每种状态的列都等价,取出来计算出现次数。
对于列,考虑初始状态 \(s\),以及最后状态 \(s'\),那么贡献是 \(s'\) 中 01 出现少的有多少个。
\(s\space \text{xor}\space s'= 行的状态\),变成卷积,即求 \(C_{i}=\sum_{s\space \text{xor}\space s'=i}cnt_s\times val_{s'}\),再取最小值即可。
P3175 [HAOI2015] 按位或
考虑拆位,将手里的数转化为 \(n\) 个只有 \(0,1\) 的数。
考虑 \(\min-\max\) 容斥,\(E(\max(U))=\sum (-1)^{|T|-1}\min(T)\)。
而 \(\min(T)\) 使用暴力公式 \(\sum_{t=0}^\infty t\times p(\min(T)=t)\),其中 \(p(t)\) 表示在 \(t\) 秒恰好完成的概率。
对于 \(\min(T)\),只要有一个数满足条件即可,即 \(T\) 集合里任何数被选都可以。
设选不到 \(T\) 集合中的数为 \(q\),\(q\) 为超集和。
\(\min(T)=(1-q)\times (1+2q+3q^2+...)\)
\(=(1-q)\times((1+q+q^2+q^3...)+(q+q^2+q^3+...)+(q^2+q^3+q^4...))\)。
等比数列求和,由于公差相同,\(=(1-q)\times \dfrac{1}{1-q}(1+q+q^2+...)=\dfrac{1}{1-q}\)。
子集和,套用 FWT 即可。
P4221 [WC2018] 州区划分
先考虑一个州合法的条件,即存在欧拉回路,那么所有点 \(deg\) 都为偶数。
我们可以预处理求出所有的合法集合。
之后考虑 \(dp\),设 \(dp_s\) 表示当前填到 \(s\) 的贡献的和。
\(dp_s=\sum_{t \subset s} dp_t\times (\frac{sum(s-t)}{sum(s)})^p\)。
这个 \(dp\) 具有阶段性,也就是 \(dp\) 数组会更新,更新的又要继续更新后面的,不能一次处理出。
考虑像子集卷积那样,新加一维状态,从 \(popcount\) 小的做到大的。
CF1119H Triple
暴力的话,做 \(O(n)\) 次卷积,复杂度炸裂。
考虑巧妙的计算 \(fwt\) 数组,对于位置 \(q\),\(fwt_q=\)
所有 \((-1)^{popcount(s\&x)}\cdot x+(-1)^{popcount(s\&y)}\cdot y+(-1)^{popcount(s\&z)}\cdot z\) 的积。
总共只有 \(8\) 种情况,求出每种情况的方案数,再乘起来即可。问题是求每种情况的个数。
先简化问题,对于 \((a,b,c)\),令 \(b=a\otimes b,c=a\otimes c\),最后再把所有 \(a\) 异或回去即可。
现在只有四种情况,\(x+y+z,x+y-z,x-y+z,x-y-z\),设方案数为 \(c_1,c_2,c_3,c_4\)。
我们考虑弄出 \(4\) 个方程把 \(c\) 解出来。
考虑把只把所有 \(y\),或只把所有 \(z\),或只把所有 \(y\otimes z\) 做一遍 fwt,
对于 \(q\) 这个点的各项系数,可以求出:
\(n=c_1+c_2+c_3+c_4\)
\(fwt(y)_q=c_1+c_2-c_3-c_4\)
\(fwt(z)_q=c_1-c_2+c_3-c_4\)
\(fwt(y\otimes z)_q=c_1-c_2-c_3+c_4\)
解这个方程,最后 ifwt 回去即可。
P5387 [Cnoi2019] 人形演舞
我们可以把题目拆成 \(|V|\) 个独立游戏。
其次,我们发现,若 \(x\) 最高位是 \(k\),那么 \(SG(x)=x-2^k+1\).
所以,题目变成了选 \(|V|\) 个数使得 \(SG\) 异或起来非零。
考虑异或卷积,我们就是卷 \(|V|\) 次,考虑把 \(fwt\) 数组翻 \(|V|\) 次幂即可。
P5406 [THUPC2019] 找树
考虑矩阵树定理,里面填一个集合幂级数 \(f(x)=\sum cnt_ix^i\),最后判断方案数是否为 \(0\)。
矩阵树定理成立的原因:集合幂级数构成了环,加法就是对应相加,乘法就是卷积。
由于行列式是线性变换,我们先把矩阵每个元素做好 fwt,
由于每个位置都是独立的,对于 fwt 每个位置求行列式,最后再 ifwt 回去即可。