FWT 学习笔记
FWT 学习笔记
想尽量讲得本质一点。
首先有一个引出问题叫做 集合幂级数
\[c_i=\sum_{j \ opt\ k=i}a_jb_k \]其中,\(opt\) 是集合的并交补运算,而 \(i,j,k\) 也都是集合的意思
当我们把 \(i,j,k\) 看成二进制表示,那么集合中的每一个元素的选/不选对应二进制的 \(1/0\) , \(opt\) 变成了 \(or,and,xor\) 的一种
所以问题变成了这样:给定两个长度为 \(2^n\) 的序列 \(a_i,b_i\) (不够长用 \(0\) 补全),求出一个序列 \(c\) ,满足 \(\forall i,c_i=\sum_{j \ opt\ k=i}a_jb_k\)
这个时候我们可以使用,FWT ,\(\text{Fast Walsh-Hadamard Transform}\) ,快速沃尔什变换。
FFT
发现 \(fwt\) 和 \(fft\) 的英文很像,所以我们考虑,类似的思路
关于 \(fft\) ,我们有一个经典思路是:(图源:pyb 的 ppt)
其中,我们能做到 \(O(n\log n)\) 的 \(DFT\) 的原因在于我们有两个重要操作叫做 奇偶分段 蝴蝶变换 ,也就是说我们把原来的东西进行分治的操作后可以 \(O(1)\) 的合并回去,\(O(n\log n)\) 的 \(IDFT\) 在于一个 \(w\) 的性质的应用,我们不妨尝试把这样的思想套进 FWT
记住上图!
FWT
现在我们有这样的目标:找到一种变换 \(FWT\) ,使得
\[FWT(A)\times FWT(B)=FWT(C) \]其中,\(\times\) 表示对应位相乘,然后通过 \(FWT(C)\) 复原 \(C\)
显然,我们的 FWT 应该是对于原序列的一种线性组合,即某一项只是若干个原多项式中的若干项的若干倍的和
因为我们本来的 \(c_i\) 就是数个 \(a_jb_k\) 的和,而 FWT 是对应位相乘,如果其出现原多项式某几项的乘积,那么显然 GG
所以显然有的是 \(FWT(A+B)=FWT(A)+FWT(B)\)
这里不妨约定一些记号:
$ ?? = ??_?? ,?? = ??_?? , ?? = ??_?? $
$ ?? + ?? = ??_0 + ??_0, ??_1 + ??_1,\dots $
$ ?? ? ?? = ??_0 ? ??_0, ??_1 ? ??_1,\dots$
$ ??\oplus?? = \sum_{?????=0} ??_?? ? ??_?? , \sum_{?????=1} ??_?? ? ??_?? ,\dots$
默认所有的数列都有 \(2^n\) 项,即可以理解为集合元素个数有 \(??\) 个
$ ??_0$ 代表数列 \(??\) 的前 \(2^{???1}\) 项,即最高位(第一个)不选的所有集合,\(??_1\) 代表数列 \(??\) 中的后 \(2 ^{???1}\) ,即最高位(第一个)选的所有集合
FWTor
假设现在我们的 \(opt\) 就是 \(or\) ,下面简记 \(A|B=\sum_{j|k=i} a_jb_k\)
下面讲得可能有点小乱
FWT
总体想法是仿照 FFT ,考虑一个递归的形式构造出 FWT
显然,当 \(n=0\) 的时候,\(FWT(A)=A\)
下面讨论 \(n>0\) 的情况,我们首先考虑奇偶分段,假装我们已经知道了 \(FWT(A_0),FWT(A_1)\)
于是,一个粗略的 FWT 出现了: \(FWT(A)=Merge(FWT(A_0),FWT(A_1))\)
\(Merge\) 表示直接拼接,xjb 举一个例子:\(Merge(998,244)=998244\)
我们知道 \(FWT(C)\) 需要等于 \(FWT(A)\times FWT(B)\)
还知道 \(C_0=A_0B_0\) ,$ C_1=A_0B_1+A_1B_0+A_1 B_1$
发现上面那个显然不靠谱,我们对应位相乘后,只会得到 \(FWT(C_0)=FWT(A_0)FWT(B_0),FWT(C_1)=FWT(A_1)FWT(B_1)\) 这样一个错误。
也就是说后半部分还需要有 \(FWT(A_0)\) 的信息甩进去,所以:(这里不妨将 FWT 当成一个神秘的抽象函数)
\[FWT(A)= \begin{cases} Merge(FWT(A_0),FWT(A_0)+FWT(A_1))&,n>0\\ \quad \quad\quad\quad\quad\quad\quad\quad A&,n=0 \end{cases} \](回顾 \(+\) 的记号:$ ?? + ?? = ??_0 + ??_0, ??_1 + ??_1,\dots $)
可是这样还是有问题呀,这样对应位相乘,好像会多一个 \(FWT(A_0)FWT(B_0)\) 项的贡献出现在 \(FWT(C_1)\) 中
但值得注意的是, \(FWT(C)\) 并不一定需要就是 \(C\) 了,我们不妨将这个问题交给 \(IFWT\) 处理,
至少我们这里保证了信息是完全的,并且符合我们心目中奇偶分段和蝴蝶变换的要求
也就是说像这样给出来后,IFWT 有操作空间,并且 FWT 时间的确是 \(n\log n\)
当然,我们还需要保证 \(FWT(A)\times FWT(B)=FWT(C)\)
这里采用类似数学归纳法的方式证明,思路来自 pyb 的 ppt ,
即假设已知 \(FWT(A_0)\times FWT(B_0)=FWT(C_0),FWT(A_0)FWT(B_1)+FWT(A_1)FWT(B_0),FWT(A_1)FWT(B_1)=FWT(C_1)\) ,那么
\[\begin{align} FWT(A)\times FWT(B)&=Merge(FWT(A_0),FWT(A_0+A_1))\times Merge(FWT(B_0),FWT(B_0+B_1))\\ &=Merge(FWT(A_0)FWT(B_0),FWT(A_0)FWT(B_0)+FWT(A_0)FWT(B_1)+FWT(A_1)FWT(B_0),FWT(A_1)FWT(B_1))\\ &=Merge(FWT(A_0B_0),FWT(A_0B_0+A_0B_1+A_1B_0+A_1B_1))\\ &=FWT(Merge(A_0B_0,A_0B_1+A_1B_0+A_1B_1))\\ &=FWT(C) \end{align} \]小心 \(Merge\) 的括号打的位置
第一个等号:按 FWT 定义展开
第二个等号:按 \(\times\) 定义对应位相乘
第三个等号:利用归纳法得到的结论
第四个等号:把 \(A_0B_0\) 理解成 \(A^{\prime}_0\) , \(A_0B_1+A_1B_0+A_1B_1\) 理解成 \(A^{\prime}_1\)
第五个等号:前文在定义 Merge 的后面一点点所述:
还知道 \(C_0=A_0B_0\) ,$ C_1=A_0B_1+A_1B_0+A_1 B_1$
所以我们归纳证明了 \(FWT(A)\times FWT(B)=FWT(C)\)
IFWT
(小剧场)
FWT: 至高无上的 IFWT !吾交给汝一个使命:干掉 $FWT(A) \times FWT(B) $ 之后出现在 \(FWT(C_1)\) 中的 \(FWT(A_0B_0)\) 项!主不需要它!
IFWT:(心里mmp:彩笔 FWT)哈哈哈!看我容斥!
(突然发现自己好智障)
直接给出来:
\[IFWT(A)=Merge(IFWT(A_0),IFWT(A_1-A_0)) \]其中传入的 \(A\) 是经历了 \(FWT\) 的产物,小证一手:
\(FWT(A)\times FWT(B)=FWT(C_0)+FWT(C_0+C_1)\)
所以:
\(IFWT(FWT(C_0)+FWT(C_0+C_1))=Merge(IFWT(FWT(C_0)),IFWT(C_0+C_1-C_0))=Merge(IFWT(FWT(C_0)),IFWT(FWT(C_1)))=C\)
FWT 的本质
我们考虑 FWT 的代码怎么写,这里再嫖一张 pyb 的 ppt
发现 \(FWT\) 做的事情就是不断地让箭头的起点加到箭头的终点,按照红绿蓝的顺序一层一层地加,可以有代码:
void FWT(ll *A){
for(int i=2;i<=N;i<<=1) //i 线段长度
for(int p=i>>1,j=0;j
我们仔细观察这些箭头究竟都干了什么:
对于 \(111\) 而言,它嫖走了 \(110,101,011\) 的信息
对于 \(110\) 而言,它嫖走了 \(101,010\) 的信息
对于 \(101\) 而言,它嫖走了 \(100,001\) 的信息
对于 \(100\) 而言,它飘走了 \(000\) 的信息
你发现了吗?也就是说,每个位置获取的是它二进制少掉一个 \(1\) 后的位置的信息
而在获取这些少掉了 \(1\) 的位置的信息之前,这些少掉了 \(1\) 的位置也获取了它们所需要的信息,所以,我们刚刚不是说 \(FWT\) 可以看成一个“抽象函数”吗,它的“解析式”其实长这样:
\[FWT(A)_i=\sum_{j\subseteq i}a_j \]\(FWT(A)_i\) 表示 \(FWT(A)\) 的第 \(i\) 项,这里之所以用 \(\subseteq\) 是因为 FWT 本身其实就是在解决“集合幂级数”。
可以带入 \(FWT(A)\times FWT(B)\) 验证一下看是不是就是 \(FWT(C)\)
所以,我们也可以从另外一个角度去证明 FWT 的时间复杂度,即是 \(\sum_{0}^{1<
上面给出 \(FWT\) 的代码,其实很容易写出 \(IFWT\) 的代码:
void IFWT(ll *A){
for(int i=2;i<=N;i<<=1)
for(int p=i>>1,j=0;j
所以,我们容易把它整合起来:
void FWT(ll *A,int op){
for(int i=2;i<=N;i<<=1) //i 线段长度
for(int p=i>>1,j=0;j
FWTand
懒得写啦!所以直接摆式子
\[FWT(A)=\begin{cases} Merge(FWT(A_0+A_1),FWT(A_1))&,n>0\\ \quad\quad\quad\quad\quad\quad\quad A&,n=0 \end{cases}\\ IFWT(A)=(IFWT(A_0-A_1),IFWT(A_1))\\ FWT(A)_i=\sum_{i\subseteq j}a_j \]注意到从某种意义上来说,\(and\) 和 \(or\) 是类似的,以为
\(0|0=1,0|1=1,1|0=1,1|1=1\)
\(0\&0=0,0\&1=0,1\&0=0,1\&1=1\)
void FWT(ll *A,int opt){
for(int i=2;i<=N;i<<=1)
for(int p=i>>1,j=0;j
FWTxor
其实 FWTxor 的推导过程才和 FFT 是最像的,怎么说呢?
显然有 \(C_0=A_0B_0+A_1B_1\),\(C_1=A_0B_1+A_1B_0\)
FFT 的蝴蝶变换叫做:
\[F(\omega_n^k)=F_1(\omega_{\frac n2}^k)+\omega_n^kA_2(\omega_{\frac n2}^k)\\ F(\omega_n^{k+\frac n2})=F_1(\omega_{\frac n2}^k)-\omega_n^kA_2(\omega_{\frac n2}^k) \]FWTxor 直接套用:
\[FWT(A)=\begin{cases} Merge(FWT(A_0+A_1),FWT(A_0-A_1))&,n>0\\ \quad\quad\quad\quad\quad\quad\quad A&,n=0 \end{cases} \]然后你发现 \(FWT(A)\times FWT(B)=Merge(FWT(A_0B_0+A_1B_1+A_0B_1+A_1B_0),FWT(A_0B_0+A_1B_1-A_0B_1-A_1B_0))\)
然后你发现前半部分多了 \(A_0B_1+A_1B_0\) ,后面部分需要多了 \(A_0B_0+A_1B_1\) 而且还需要变个号
所以你灵光一现,如此构造出了 IFWT:
\(IFWT(A)=Merge(IFWT(\dfrac{A_0+A_1}2),IFWT(\dfrac{A_0-A_1}2))\)
然后你发现好像是解决了
void fwtxor(int *f,int op){
for(int i=2;i<=len;i<<=1)
for(int p=i>>1,j=0;j
(细心的读者可能发现这份代码和上面的数组不一样,而且取了模,但其实是一样的,只是从不同题里面 copy 出来的)
然后你开始探究 FWTxor 的本质(“解析式”)
然后你懵逼了
然后你上网看了一下:
\[FWT(A)_i=\sum_{j=0}^n(-1)^{|j\cup i|}a_j \]然后你尝试探究如何从 FWT 的递推式得到它,
然后你发现你想不明白,但是模拟一下它就是对的,于是你有上网一通乱找,你找到了
当你看完了巨佬的文章,你开始尝试自己推导:
\[c_i=\sum_{j\oplus k=i}a_jb_k=\sum_{j\subseteq i}\sum_{k\subseteq i}[i\oplus j\oplus k=0]a_jb_k \]这个时候你又知道这样一个式子:
\[\frac{1}{2^n}\sum_{T\subseteq U}(-1)^{|W\cup T|}=1\Leftrightarrow W=\empty \]其中 \(U\) 代表某一个大小为 \(2^n\) 的全集,\(W\) 是随便自己给定了一个集合
它的正确性是很显然的,当 \(W\) 不为空的时候,显然会出现一些 \(-1\)? ,从而使得这个 \(\sum\) 的值小于 \(2^n\)
当且仅当 \(W\) 为空集的时候,才会使得这个 \(\sum=2^n\)
然后,你利用这个式子,进行你的推导
\[\sum_{j\subseteq i}\sum_{k\subseteq i}[i\oplus j\oplus k=0]a_jb_k=\sum_{j\subseteq i}\sum_{k\subseteq i}\frac1{2^n}\sum_{T\subseteq U}(-1)^{|(i\oplus j\oplus k)\cup T|}a_jb_k \]你发现你的推导还需要一点东西,所以你来证明这个:
\[|T\cup\oplus_{i=1}^x s_i|\equiv \sum|T\cup s_i|\pmod2 \]其中 \(s_i\) 是某个集合,\(T\)? 是某个集合。
首先,我们来证明 \(x=2\)?? 的情况,即:\(|i\cup (j\oplus k)|\equiv|i\cup j|+|i\cup k|\pmod 2\)??
(注意,下面没有采用原博客所使用的方法)
我们直接把集合看成二进制(反正上面的文章也一直把集合和二进制在混用qwq) ,下面简记 \(popcount\to ppc\)
即证明 \(ppc(i\&(j\oplus k))\equiv ppc(i\&j)+ppc(i\&k)\pmod 2\)
(突然发现由于写了太多,导致上面出现了奇怪的人称变化)
设 \(z=i\&(j\oplus k),x=i\&j,y=i\&k\) ,接下来分类讨论,对于二进制每一位,都有:
- \(x\)?? 当前位为 \(0\)?? ,\(y\)?? 当前位为 \(0\)?? 。那么要么是 \(i\)? 没有当前位,要么是 \(j,k\)? 均没有当前位,所以 \(z\)?? 当前位也是 \(0\)??
- \(x\)??? 当前位为 \(0\)??? ,\(y\)??? 当前位为 \(1\)??? 。那么肯定是 \(j\) 没有当前位,\(i,k\) 均有当前位,所以 \(z\) 当前位也是 \(1\)
- \(x\) 当前位为 \(1\) ,\(y\) 当前位为 \(0\) 。那么肯定是 \(k\) 没有当前位,\(i,j\) 均有当前位,所以 \(z\) 当前位也是 \(1\)
- \(x\)??? 当前位为 \(1\)??? ,\(y\)??? 当前位为 \(1\)??? 。那么肯定是 \(i,j,k\) 均有当前位,\(z\) 当前位为 \(0\) ,在 \(mod\ 2\) 意义下依旧满足
所以现在我们已经证明了 \(x=2\) 的情况,容易发现 \(n>2\) 的情况可以看成 \(n=2\) 的情况,解决
所以我们现在继续我们的推式子大业:
\[\begin{align} \sum_{j\subseteq i}\sum_{k\subseteq i}\frac1{2^n}\sum_{T\subseteq U}(-1)^{|(i\oplus j\oplus k)\cup T|}a_jb_k&=\sum_{j\subseteq i}\sum_{k\subseteq i}\frac1{2^n}\sum_{T\subseteq U}(-1)^{|T\cup i|+|T\cup j|+|T\cup k|}a_jb_k\\ &=\frac1{2^n}\sum_{j\subseteq i}\sum_{k\subseteq i}\sum_{T\subseteq U}(-1)^{|T\cup i|}(-1)^{|T\cup j|}a_j(-1)^{|T\cup k|b}b_k \end{align} \]顺理成章的,我们知道了:
\[FWT(A)_i=\sum_{j=0}^n(-1)^{|j\cup i|}a_j \]!!!
「CF662C」 Binary Table
CF662C Binary Table - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
延续做一道黑题写一篇题解的传统,我们直接把题解写在这里面
Statement
有一个 \(n\) 行 \(m\) 列的表格,每个元素都是 \(0/1\) ,每次操作可以选择一行或一列,把 \(0/1\) 翻转,即把 \(0\) 换为 \(1\) ,把 \(1\) 换为 \(0\) 。请问经过若干次操作后,表格中最少有多少个 \(1\) 。
\(n\leq 20,m\leq 10^5\)
Solution
注意到 \(??\) 很小
容易得到暴力算法:暴力枚举第 \(??\) 行是否翻转,这样每一行的状态就已确定,对于每一次完整的行翻转后,取每一列 \(0/1\) 个数较小的贡献即可。(因为每一列也可以翻转)
时间复杂度是 \(??(?? ? 2^?? )\) ,我们考虑优化掉这个 \(m\)
显然行的操作我们可以状压为 \(??\),(二进制为 \(1\) 的位表示那一行要翻转)
每一列的的状态值也可以压缩:
- 用 \(??[??]\) 表示在原表列中, \(??\) 这个状态数量
- 用 \(??[??]\) 表示 \(??\) 这个状态最小 \(1\) 的个数(即 \(\min(popcount(i), n-ppc(i)\))
当确定了行的操作为 \(x\) ,那么所有状态为 \(i\) 的列的贡献为:\(A[i]B[i\oplus x]\)
令 \(j=i\oplus x\) ,那么 \(x=i\oplus j\)
所以 \(ans_x=\sum_{i\oplus j=x}A[i]B[j]\) ,直接上 FWT 即可
复杂度:\(O(\max(n2^m,nm))\)
Code
#include
#define int long long
using namespace std;
const int N = (1<<21);
const int M = 1e5+5;
char s[25][M];
int a[N],b[N];
int n,m,ans=1e18;
void fwtxor(int *f,int op){
for(int i=2;i<=(1<>1,j=0;j<(1<
子集卷积
写不动了!!
https://www.luogu.com.cn/problem/P6097
所谓子集卷积,听着很高级,其实也就那样(就最基本的而言),好像有叫做集合占位幂级数什么的
它用来处理求出这样一个 \(c\):
\[c_i=\sum_{\begin{align}j\ |\ k=i \\ j\ \&\ k=0\end{align}}a_jb_k \]我们很容易处理第一个限制 \(i \cup j=k\),直接 FWTand 即可。
对于第二个限制 \(i \cap j=\varnothing\Leftrightarrow |i|+|j|=|i\cup j|\),所以我们不妨再开一维记录集合中的元素个数
也就是说设 \(f_{i,j} = a_j[|j|=i],g_{i,j}=b_j[|j|=i]\),把他们卷起来,\(h_{i,j}=\sum_{k=0}^i \sum_{l|r=j}f_{k,l}g_{i-k,r}\)?
最后的答案即为: \(c_i=h_{|i|,i}\)? ,因为 \(popcount(i)+popcount(j)\ge popcount(i|j)\) ,所以相等的时候取到的就是正确的值?
#include
using namespace std;
const int N = 1<<21;
const int mod = 1e9+9;
int read(){
int s=0,w=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')w=-1;ch=getchar();}
while(isdigit(ch))s=s*10+ch-'0',ch=getchar();
return s*w;
}
int a[21][N],b[21][N],c[21][N];
int n,len;
void fwt(int *f,int op){
for(int i=2;i<=len;i<<=1)
for(int p=i>>1,j=0;j
扩展:https://www.luogu.com.cn/blog/user7035/zi-ji-juan-ji-ji-ji-gao-ji-yun-suan (我不会,等我长大后再学习)
[WC2018] 州区划分
WC2018 州区划分 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
延续做一道黑题写一篇题解的传统,我们直接把题解写在这里面
Statement
由于原题面已经概括得很好了,所以这里直接贴图:
\(n\leq 21,m\leq n^2/2,p\le 2 ,w_i\le 100\)
Solution
容易考虑状压 DP,设 \(f[i][j]\) 表示划分了 \(i\) 个州,选出的城市的集合为 \(j\) 的贡献,那么
\[f[i][j]=\sum_{k\subseteq j}f[i-1][k](\dfrac{sum[k]}{sum[j]})chk[k] \]其中,\(sum[s]\) 表示集合 \(s\) 的 \(w\) 的和,\(chk[s]\) 表示集合 \(s\) 是否可以独立成州
所谓独立成州的条件其实可以转化为 图不连通/存在奇度数点,我们容易在 \(O(2^nm)\) 左右的时间暴力预处理出 \(sum,chk\)
设 \(g[k]=sum[k]chk[k]\) ,那么:
\[\begin{align} f[i][j]&=\frac1{sum[j]}\sum_{k\subseteq j}f[i-1][k]g[k]\\ &=\frac1{sum[j]}\sum_{s\cup t=j \&\& s\cap t=\emptyset}f[i-1][s]g[t] \end{align} \]发现后面的那一坨其实就是子集卷积,所以复杂度 \(O(n^22^n)\) 解决
Code
#include
#define int long long
#define ppc(x) __builtin_popcount(x)
using namespace std;
const int mod = 998244353;
const int N = 22;
char buf[1<<23],*p1=buf,*p2=buf;
#define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++)
int read(){
int s=0,w=1; char ch=getchar();
while(!isdigit(ch)){if(ch=='-')w=-1;ch=getchar();}
while(isdigit(ch))s=s*10+(ch^48),ch=getchar();
return s*w;
}
int u[N*N],v[N*N],w[N],fa[N],deg[N],inv[(1<>=1;
}
return res;
}
void preparation(){
for(int i=0,sum,cnt,flag;sum=cnt=flag=0,i<(1<>1;j<(1<
完结撒花!!!!??ヽ(°▽°)ノ?