FWT 学习笔记

FWT 学习笔记

想尽量讲得本质一点。

首先有一个引出问题叫做 集合幂级数

ci=j opt k=iajbk

其中,opt 是集合的并交补运算,而 i,j,k 也都是集合的意思

当我们把 i,j,k 看成二进制表示,那么集合中的每一个元素的选/不选对应二进制的 1/0opt 变成了 or,and,xor 的一种

所以问题变成了这样:给定两个长度为 2n 的序列 ai,bi (不够长用 0 补全),求出一个序列 c ,满足 i,ci=j opt k=iajbk

这个时候我们可以使用,FWT ,Fast Walsh-Hadamard Transform ,快速沃尔什变换。

FFT

发现 fwtfft 的英文很像,所以我们考虑,类似的思路

关于 fft ,我们有一个经典思路是:(图源:pyb 的 ppt)

其中,我们能做到 O(nlogn)DFT 的原因在于我们有两个重要操作叫做 奇偶分段 蝴蝶变换 ,也就是说我们把原来的东西进行分治的操作后可以 O(1) 的合并回去,O(nlogn)IDFT 在于一个 w 的性质的应用,我们不妨尝试把这样的思想套进 FWT

记住上图!

FWT

现在我们有这样的目标:找到一种变换 FWT ,使得

FWT(A)×FWT(B)=FWT(C)

其中,× 表示对应位相乘,然后通过 FWT(C) 复原 C

显然,我们的 FWT 应该是对于原序列的一种线性组合,即某一项只是若干个原多项式中的若干项的若干倍的和

因为我们本来的 ci 就是数个 ajbk 的和,而 FWT 是对应位相乘,如果其出现原多项式某几项的乘积,那么显然 GG

所以显然有的是 FWT(A+B)=FWT(A)+FWT(B)

这里不妨约定一些记号:

𝐴=𝑎𝑖,𝐵=𝑏𝑖,𝐶=𝑐𝑖

𝐴+𝐵=𝑎0+𝑏0,𝑎1+𝑏1,

𝐴𝐵=𝑎0𝑏0,𝑎1𝑏1,

𝐴𝐵=𝑖𝑗=0𝑎𝑖𝑏𝑗,𝑖𝑗=1𝑎𝑖𝑏𝑗,

默认所有的数列都有 2n 项,即可以理解为集合元素个数有 𝑛

𝐴0 代表数列 𝐴 的前 2𝑛1 项,即最高位(第一个)不选的所有集合,𝐴1 代表数列 𝐴 中的后 2𝑛1 ,即最高位(第一个)选的所有集合

FWTor

假设现在我们的 opt 就是 or ,下面简记 A|B=j|k=iajbk

下面讲得可能有点小乱

FWT

总体想法是仿照 FFT ,考虑一个递归的形式构造出 FWT

显然,当 n=0 的时候,FWT(A)=A

下面讨论 n>0 的情况,我们首先考虑奇偶分段,假装我们已经知道了 FWT(A0),FWT(A1)

于是,一个粗略的 FWT 出现了: FWT(A)=Merge(FWT(A0),FWT(A1))

Merge 表示直接拼接,xjb 举一个例子:Merge(998,244)=998244

我们知道 FWT(C) 需要等于 FWT(A)×FWT(B)

还知道 C0=A0B0 ,C1=A0B1+A1B0+A1B1

发现上面那个显然不靠谱,我们对应位相乘后,只会得到 FWT(C0)=FWT(A0)FWT(B0),FWT(C1)=FWT(A1)FWT(B1) 这样一个错误。

也就是说后半部分还需要有 FWT(A0) 的信息甩进去,所以:(这里不妨将 FWT 当成一个神秘的抽象函数)

FWT(A)={Merge(FWT(A0),FWT(A0)+FWT(A1)),n>0A,n=0

(回顾 + 的记号:𝐴+𝐵=𝑎0+𝑏0,𝑎1+𝑏1,

可是这样还是有问题呀,这样对应位相乘,好像会多一个 FWT(A0)FWT(B0) 项的贡献出现在 FWT(C1)

但值得注意的是, FWT(C) 并不一定需要就是 C 了,我们不妨将这个问题交给 IFWT 处理,

至少我们这里保证了信息是完全的,并且符合我们心目中奇偶分段和蝴蝶变换的要求

也就是说像这样给出来后,IFWT 有操作空间,并且 FWT 时间的确是 nlogn

当然,我们还需要保证 FWT(A)×FWT(B)=FWT(C)

这里采用类似数学归纳法的方式证明,思路来自 pyb 的 ppt ,

即假设已知 FWT(A0)×FWT(B0)=FWT(C0),FWT(A0)FWT(B1)+FWT(A1)FWT(B0),FWT(A1)FWT(B1)=FWT(C1) ,那么

(1)FWT(A)×FWT(B)=Merge(FWT(A0),FWT(A0+A1))×Merge(FWT(B0),FWT(B0+B1))(2)=Merge(FWT(A0)FWT(B0),FWT(A0)FWT(B0)+FWT(A0)FWT(B1)+FWT(A1)FWT(B0),FWT(A1)FWT(B1))(3)=Merge(FWT(A0B0),FWT(A0B0+A0B1+A1B0+A1B1))(4)=FWT(Merge(A0B0,A0B1+A1B0+A1B1))(5)=FWT(C)

小心 Merge 的括号打的位置

第一个等号:按 FWT 定义展开

第二个等号:按 × 定义对应位相乘

第三个等号:利用归纳法得到的结论

第四个等号:把 A0B0 理解成 A0 , A0B1+A1B0+A1B1 理解成 A1

第五个等号:前文在定义 Merge 的后面一点点所述:

还知道 C0=A0B0 ,C1=A0B1+A1B0+A1B1

所以我们归纳证明了 FWT(A)×FWT(B)=FWT(C)

IFWT

(小剧场)

FWT: 至高无上的 IFWT !吾交给汝一个使命:干掉 FWT(A)×FWT(B) 之后出现在 FWT(C1) 中的 FWT(A0B0) 项!主不需要它!

IFWT:(心里mmp:彩笔 FWT)哈哈哈!看我容斥!

(突然发现自己好智障)

直接给出来:

IFWT(A)=Merge(IFWT(A0),IFWT(A1A0))

其中传入的 A 是经历了 FWT 的产物,小证一手:

FWT(A)×FWT(B)=FWT(C0)+FWT(C0+C1)

所以:

IFWT(FWT(C0)+FWT(C0+C1))=Merge(IFWT(FWT(C0)),IFWT(C0+C1C0))=Merge(IFWT(FWT(C0)),IFWT(FWT(C1)))=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<N;j+=i)//j 哪一个部分
			for(int k=j;k<j+p;++k)  //k 嫖取信息
				A[k+p]+=A[k];
}

我们仔细观察这些箭头究竟都干了什么:

对于 111 而言,它嫖走了 110,101,011 的信息

对于 110 而言,它嫖走了 101,010 的信息

对于 101 而言,它嫖走了 100,001 的信息

对于 100 而言,它飘走了 000 的信息

你发现了吗?也就是说,每个位置获取的是它二进制少掉一个 1 后的位置的信息

而在获取这些少掉了 1 的位置的信息之前,这些少掉了 1 的位置也获取了它们所需要的信息,所以,我们刚刚不是说 FWT 可以看成一个“抽象函数”吗,它的“解析式”其实长这样:

FWT(A)i=jiaj

FWT(A)i 表示 FWT(A) 的第 i 项,这里之所以用 是因为 FWT 本身其实就是在解决“集合幂级数”。

可以带入 FWT(A)×FWT(B) 验证一下看是不是就是 FWT(C)

所以,我们也可以从另外一个角度去证明 FWT 的时间复杂度,即是 01<<npopcount(i)=n2n1

上面给出 FWT 的代码,其实很容易写出 IFWT 的代码:

void IFWT(ll *A){
	for(int i=2;i<=N;i<<=1)         
		for(int p=i>>1,j=0;j<N;j+=i)
			for(int k=j;k<j+p;++k)
				A[k+p]-=A[k];//唯一不同点
}

从代码逻辑的直观感受来看,如果我 FWT 的枚举顺序是 i=2...N,那 IFWT 的顺序是不是应该从 i=N...2 才对?

正确性可以这样理解,FWT 本质是线性变化,所以每一个二进制其实挺独立的,也就是满足交换律。(没有必要是从低位到高位做,也可以是任意顺序做,只是这样写比较方便)

我们甚至也可以是随便点定一个排列 p,然后按照排列 p 的顺序做。

更为直观的说明是,如果把每一个数字看成是一个 F2n 空间的向量,每一个二进制位代表某一个维度的坐标,那么上面实际上做的就是高维前缀和。而高维前缀和自然可以随便钦定枚举维度的顺序,然后每次 01

所以,我们容易把它整合起来:

void FWT(ll *A,int op){
	for(int i=2;i<=N;i<<=1)         //i 线段长度
		for(int p=i>>1,j=0;j<N;j+=i)//j 哪一个部分
			for(int k=j;k<j+p;++k)  //k 嫖取信息
				A[k+p]+=op*A[k];
}

上面从类似 FFT 的构造线性变化的角度出发,通过一点人类智慧,构造出了点值对应的形式,并证明了这样做是合法的。

但同时我们也发现 FWTor 其实就是高维前缀和的形式,而 IFWTor 就是高维差分,那么,我们能不能直接从高维前缀和/差分的角度来理解这样做 or 卷积的正确性呢?

我们考虑这个高维前缀和的形式:F(S)=TSf(T)

我们说,与之对应的高维差分:f(S)=TS(1)|ST|F(T) 其实本质就是容斥原理:

f(S)=F(S)(F(S)$S$)=F(S)(F(T),TSppc(T)=ppc(S)1)+T=

考虑我们的高维前缀和实际上解决的是这样的问题:对于两个不同的组合对象,从对象 1 中选出 S 的方案数为 f(S) ,从对象 2 中选出 S 的方案数是 g(S) ,我们想要计算的是 (fg)(S)=T1T2=Sf(T1)g(T2) 的值

发现实际上能产生贡献的 (T1,T2) 需要满足如下条件:

  • T1S,T2S
  • xS,s.t.xT1xT2

发现第一个条件是比较喜欢的,因为实际上它意味着所有在 T 中的元素都在 S 中,限制比较强烈

而第二个条件涉及到一个 or 的问题,比较麻烦,我们考虑把这个条件容斥掉

条件的否定是: xS,s.t.xT1xT2

考虑枚举 S 使得 xS 满足上述条件,考虑此时 T1,T2 满足的条件,有

(fg)(S)=SS(1)|SS|f(S)g(S)=TS(1)|ST|F(S)·G(S)

所以:fgFG 的高维差分,那么 并卷积的高维前缀和为 FG,即 FWTor(fg)=F·GIFWT(F·G)=fg,而 F=FWT(f),G=FWT(g)。于是,我们从组合容斥的角度,说明了 FWT 的正确性,并了解了其高维前缀和/差分的本质。


下面是题外话,我们可以同样以这样 高维前缀和/差分的思想 去理解莫比乌斯反演,去理解 μ 到底是咋搞出来的。

莫比乌斯反演:

g=1f ,构造函数 μ ,使得 1μ=∈ ,有 f=μg

写开:g(n)=d|nf(d)

考虑使用多元组来表示一个数,对于一个 n=piai ,我们使用 (a1,a2,,ar) 来表达

重定义 符号表示对于两个二元组 (ai),(bi) ,若 i,aibi ,那么 (ai)(bi)

那么,可以表示成: g(S)=TSf(T) ,其实就是高维前缀和

所以对应的反演形式,也就是 f(n)=d|nμ(nd)g(d) ,可以看做是高维差分 f(S)=TS(1)|ST|g(T) ,而对应的 μ(n/d)=(1)|S||T|

所以我们得以了解 μ 的构造思路

想象现在我们在做二维差分 ,a(i,j)=s(i,j)s(i1,j)s(i,j1)+s(i1,j1) ,系数都长成这样:

同理,考虑在刚刚所提到的表示法之下的系数,以 x=2i3j 为例子

a(x)=s(x)s(x/2)s(x/3)+s(x/6) ,对应的 μ(1)=1,μ(2)=1,μ(6)=1,μ()=0

由此:

(6)μ(N)={0,i[1,m],ci>11,m0(mod2),i[1,m],ci=11,m1(mod2),i[1,m],ci=1

FWTand

懒得写啦!所以直接摆式子

FWT(A)={Merge(FWT(A0+A1),FWT(A1)),n>0A,n=0IFWT(A)=(IFWT(A0A1),IFWT(A1))FWT(A)i=ijaj

注意到从某种意义上来说,andor 是类似的,以为

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<N;j+=i)
			for(int k=j;k<j+p;++k)
				A[k]+=A[k+p]*opt;
}

类似上方对于 FWTor 的组合阐述,通过 Jzzhu and Numbers 这道题,我们也可以对 FWTand 做组合阐述。

现在有若干组合对象,f1,n(s) 分别表示从这 n 个组合对象里面选出集合 s 的方案数,求有多少种选 s1n 的方案,使得 Andi=1nsi=0

方法是容斥,假如我求出了 g[i] 表示 Andi=1nppci 的方案数,那么 ans=g[0]g[1]+g[2],(其实就等价于 IFWTand,只不过把 ppc 相同的先加在一起了)

g[i] 的求法是容易的,只需要求出 h[s] 表示 sAnd 的方案数,等价于对于每一个组合对象,都有 ssi。那么这个其实也就是 FWTand 转成点值然后对应位置相乘。

那么就做完了。

使用一点更为多项式的阐述:

[x](1+xi)ci=[xvarnothing]IFWT(FWT(1+xi)ci)=s(1)|s|[xs](FWT(1+xi)ci)=s(1)|s|st2ct=s(1)|s|2stct

所以这道题的做法就是先在 ϕ(mod) 下用 FWT 把 2 的幂次做出来,然后带入上式。

FWTxor

其实 FWTxor 的推导过程才和 FFT 是最像的,怎么说呢?

显然有 C0=A0B0+A1B1,C1=A0B1+A1B0

FFT 的蝴蝶变换叫做:

F(ωnk)=F1(ωn2k)+ωnkA2(ωn2k)F(ωnk+n2)=F1(ωn2k)ωnkA2(ωn2k)

FWTxor 直接套用:

FWT(A)={Merge(FWT(A0+A1),FWT(A0A1)),n>0A,n=0

然后你发现 FWT(A)×FWT(B)=Merge(FWT(A0B0+A1B1+A0B1+A1B0),FWT(A0B0+A1B1A0B1A1B0))

然后你发现前半部分多了 A0B1+A1B0 ,后面部分需要多了 A0B0+A1B1 而且还需要变个号

所以你灵光一现,如此构造出了 IFWT:

IFWT(A)=Merge(IFWT(A0+A12),IFWT(A0A12))

然后你发现好像是解决了

void fwtxor(int *f,int op){
    for(int i=2;i<=len;i<<=1)
        for(int p=i>>1,j=0;j<len;j+=i)
            for(int k=j;k<j+p;++k){
                int x=f[k],y=f[k+p];
                f[k]=(x+y)%mod,f[k+p]=(x-y+mod)%mod;
                if(op==-1)(f[k]*=inv2)%=mod,(f[k+p]*=inv2)%=mod;
            }
}

然后你开始探究 FWTxor 的本质(“解析式”)

然后你懵逼了

然后你上网看了一下:

FWT(A)i=j=0n(1)|ji|aj

然后你尝试探究如何从 FWT 的递推式得到它,

然后你发现你想不明白,但是模拟一下它就是对的,于是你有上网一通乱找,你找到了 pyb‘s ppt 的参考文献

(其实一言以蔽之,考虑在 F2 下的乘法其实就是 ppc(i&j),所以可以从 FFT 为什么对理解 FWT 为什么对)

当你看完了巨佬的文章,你开始尝试自己推导:

ci=jk=iajbk=jiki[ijk=0]ajbk

这个时候你又知道这样一个式子:

12nTU(1)|WT|=1W=

其中 U 代表某一个大小为 2n 的全集,W 是随便自己给定了一个集合

它的正确性是很显然的,当 W 不为空的时候,显然会出现一些 1 ,从而使得这个 的值小于 2n

当且仅当 W 为空集的时候,才会使得这个 =2n

然后,你利用这个式子,进行你的推导

jiki[ijk=0]ajbk=jiki12nTU(1)|(ijk)T|ajbk

你发现你的推导还需要一点东西,所以你来证明这个:

|Ti=1xsi||Tsi|(mod2)

其中 si 是某个集合,T 是某个集合。

首先,我们来证明 x=2 的情况,即:|i(jk)||ij|+|ik|(mod2)

(注意,下面没有采用原博客所使用的方法)

我们直接把集合看成二进制(反正上面的文章也一直把集合和二进制在混用qwq) ,下面简记 popcountppc

即证明 ppc(i&(jk))ppc(i&j)+ppc(i&k)(mod2)

(突然发现由于写了太多,导致上面出现了奇怪的人称变化)

z=i&(jk),x=i&j,y=i&k ,接下来分类讨论,对于二进制每一位,都有:

  1. x 当前位为 0y 当前位为 0 。那么要么是 i 没有当前位,要么是 j,k 均没有当前位,所以 z 当前位也是 0
  2. x 当前位为 0y 当前位为 1 。那么肯定是 j 没有当前位,i,k 均有当前位,所以 z 当前位也是 1
  3. x 当前位为 1y 当前位为 0 。那么肯定是 k 没有当前位,i,j 均有当前位,所以 z 当前位也是 1
  4. x 当前位为 1y 当前位为 1 。那么肯定是 i,j,k 均有当前位,z 当前位为 0 ,在 mod 2 意义下依旧满足

所以现在我们已经证明了 x=2 的情况,容易发现 n>2 的情况可以看成 n=2 的情况,解决

所以我们现在继续我们的推式子大业:

(7)jiki12nTU(1)|(ijk)T|ajbk=jiki12nTU(1)|Ti|+|Tj|+|Tk|ajbk(8)=12njikiTU(1)|Ti|(1)|Tj|aj(1)|Tk|bbk

顺理成章的,我们知道了:

FWT(A)i=j=0n(1)|ji|aj

!!!

「CF662C」 Binary Table

CF662C Binary Table - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

延续做一道黑题写一篇题解的传统,我们直接把题解写在这里面

Statement

有一个 nm 列的表格,每个元素都是 0/1 ,每次操作可以选择一行或一列,把 0/1 翻转,即把 0 换为 1 ,把 1 换为 0 。请问经过若干次操作后,表格中最少有多少个 1

n20,m105

Solution

注意到 𝑛 很小

容易得到暴力算法:暴力枚举第 𝑖 是否翻转,这样每一行的状态就已确定,对于每一次完整的行翻转后,取每一列 0/1 个数较小的贡献即可。(因为每一列也可以翻转)

时间复杂度是 𝑂(𝑚2𝑛) ,我们考虑优化掉这个 m

显然行的操作我们可以状压为 𝑥,(二进制为 1 的位表示那一行要翻转)

每一列的的状态值也可以压缩:

  1. 𝐴[𝑖] 表示在原表中, 𝑖 这个状态数量
  2. 𝐵[𝑖] 表示 𝑖 这个状态最小 1 的个数(即 min(popcount(i),nppc(i))

当确定了行的操作为 x ,那么所有状态为 i 的列的贡献为:A[i]B[ix]

j=ix ,那么 x=ij

所以 ansx=ij=xA[i]B[j] ,直接上 FWT 即可

复杂度:O(max(n2m,nm))

Code

#include<bits/stdc++.h>
#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<<n);i<<=1)
        for(int p=i>>1,j=0;j<(1<<n);j+=i)
            for(int k=j;k<j+p;++k){
                int x=f[k],y=f[k+p];
                f[k]=x+y,f[k+p]=x-y;
                if(op==-1)f[k]/=2,f[k+p]/=2;
            }
}

signed main(){ 
    scanf("%lld %lld",&n,&m);
    for(int i=1;i<=n;++i)scanf("%s",s[i]+1);
    for(int i=1,k;k=0,i<=m;++i,a[k]++)
        for(int j=1;j<=n;++j)k=k<<1|(s[j][i]-'0');
    for(int i=0,t;i<(1<<n);++i)
        t=__builtin_popcount(i),b[i]=min(t,n-t);
    fwtxor(a,1),fwtxor(b,1);
    for(int i=0;i<(1<<n);++i)a[i]*=b[i];
    fwtxor(a,-1);
    for(int i=0;i<(1<<n);++i)ans=min(ans,a[i]);
    printf("%lld\n",ans);
    return 0;
}

子集卷积

写不动了!!

https://www.luogu.com.cn/problem/P6097

所谓子集卷积,听着很高级,其实也就那样(就最基本的而言),好像有叫做集合占位幂级数什么的

它用来处理求出这样一个 c

ci=(9)j | k=i(10)j & k=0ajbk

我们很容易处理第一个限制 ij=k,直接 FWTand 即可。

对于第二个限制 ij=|i|+|j|=|ij|,所以我们不妨再开一维记录集合中的元素个数

也就是说设 fi,j=aj[|j|=i],gi,j=bj[|j|=i],把他们卷起来,hi,j=k=0il|r=jfk,lgik,r

最后的答案即为: ci=h|i|,i ,因为 popcount(i)+popcount(j)popcount(i|j) ,所以相等的时候取到的就是正确的值

(即 |S|+|T|=|S|T|+|S&T| )

#include<bits/stdc++.h>
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<len;j+=i)
			for(int k=j;k<j+p;++k)
				(f[k+p]+=op*f[k])%=mod; 
} 

signed main(){
	n=read(),len=1<<n;
	for(int i=0;i<len;++i)a[__builtin_popcount(i)][i]=read();
	for(int i=0;i<len;++i)b[__builtin_popcount(i)][i]=read();
	for(int i=0;i<=n;++i)fwt(a[i],1),fwt(b[i],1);
	for(int i=0;i<=n;++i)
		for(int j=0;j<=i;++j)
			for(int k=0;k<len;++k)
				(c[i][k]+=(long long)a[j][k]*b[i-j][k]%mod)%=mod;
	for(int i=0;i<=n;++i)fwt(c[i],-1);
	for(int i=0;i<len;++i)printf("%d ",(c[__builtin_popcount(i)][i]+mod)%mod);
	return 0;
} 

所谓集合占位幂级数,是这样的形式:

fSgT[S&T=]hS|TSfSxSSfSz|S|xSfzaxS·gzbxTfg za+bxS|T

扩展:https://www.luogu.com.cn/blog/user7035/zi-ji-juan-ji-ji-ji-gao-ji-yun-suan (我不会,等我长大后再学习)

[WC2018] 州区划分

WC2018 州区划分 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

延续做一道黑题写一篇题解的传统,我们直接把题解写在这里面

Statement

由于原题面已经概括得很好了,所以这里直接贴图:

n21,mn2/2,p2,wi100

Solution

容易考虑状压 DP,设 f[i][j] 表示划分了 i 个州,选出的城市的集合为 j 的贡献,那么

f[i][j]=kjf[i1][k](sum[k]sum[j])chk[k]

其中,sum[s] 表示集合 sw 的和,chk[s] 表示集合 s 是否可以独立成州

所谓独立成州的条件其实可以转化为 图不连通/存在奇度数点,我们容易在 O(2nm) 左右的时间暴力预处理出 sum,chk

g[k]=sum[k]chk[k] ,那么:

(11)f[i][j]=1sum[j]kjf[i1][k]g[k](12)=1sum[j]st=j&&st=f[i1][s]g[t]

发现后面的那一坨其实就是子集卷积,所以复杂度 O(n22n) 解决

Code

#include<bits/stdc++.h>
#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<<N)+5];
int f[N][(1<<N)+5],g[N][(1<<N)+5];
int n,m,p;
bool vis[N];

void input(){
    n=read(),m=read(),p=read();
    for(int i=1;i<=m;++i)u[i]=read(),v[i]=read();
    for(int i=1;i<=n;++i)w[i]=read();
}
int find(int x){return x==fa[x]?x:fa[x]=find(fa[x]);}
bool merge(int u,int v){
    u=find(u),v=find(v);
    return u==v?0:fa[u]=v;//////////////
}
int ksm(int a,int b){
    int res=1;
    while(b){
        if(b&1)res=res*a%mod;
        a=a*a%mod,b>>=1;
    }
    return res;
}
void preparation(){
    for(int i=0,sum,cnt,flag;sum=cnt=flag=0,i<(1<<n);++i){
        for(int j=1;j<=n;++j)fa[j]=j,vis[j]=false,deg[j]=0;
        for(int j=0;j<n;++j)if(i&(1<<j))vis[j+1]=true,sum+=w[j+1],cnt++;
        for(int j=1;j<=m;++j)if(vis[u[j]]&&vis[v[j]])
            merge(u[j],v[j])&&(cnt--,1),deg[u[j]]++,deg[v[j]]++;
        flag|=cnt!=1,cnt=0;
        for(int j=1;j<=n;++j)cnt+=(deg[j]&1);
        flag|=cnt!=0,g[ppc(i)][i]=flag*ksm(sum,p);
        inv[i]=ksm(ksm(sum,mod-2),p);
    }
}
void fwtor(int *f,int op){
    for(int i=2;i<=(1<<n);i<<=1)
        for(int j=0,p=i>>1;j<(1<<n);j+=i)
            for(int k=j;k<j+p;++k)(f[k+p]+=(op*f[k]+mod)%mod)%=mod;
}
void work(){
    for(int i=0;i<=n;++i)fwtor(g[i],1);
    f[0][0]=1,fwtor(f[0],1);
    for(int i=1;i<=n;++i){
        for(int j=0;j<i;++j)//j!=i
            for(int k=0;k<(1<<n);++k)
                (f[i][k]+=f[j][k]*g[i-j][k]%mod)%=mod;
        fwtor(f[i],-1);
        for(int j=0;j<(1<<n);++j)
            f[i][j]=(ppc(j)==i)*(f[i][j]*inv[j]%mod);
        if(i^n)fwtor(f[i],1);
    }
}
void output(){
    printf("%lld\n",f[n][(1<<n)-1]);
}

signed main(){
    input();
    preparation();
    work();
    output();
    return 0;
}

「CF1034E」Little C Loves 3 III

Statement

给定 n 和长度为 2n 的数列 a0,a1...a2n1b0,b1...b2n1,保证每个元素的值属于[0,3]

生成序列 c,对于 ci,有:

ci=j|k=i,j&k=0aj×bk

c0,c1...c2n1,答案对 4 取模。

n21,时限 1s

Solution

显然,暴力 O(n22n) 的子集卷积不可过,考虑利用 mod4 的性质

普通的,子集卷积为了取到正确的值,采用的方法可以理解为 DP 多设一维状态 |S| ,使得我们加入了 z|S| 这一项

这里,我们让 aS=aS×4|S|,bT=bT×4|T| (鬼知道是怎么想到的!)

用 FWT 求出 c 后,x|S| 项系数对 4|S|+1 取模,再除以一个 4|S| 即可

解释:

集合占位幂级数是这样的形式: S(fSz|S|xS+i>|S|σi,SzixS) 其中 z 的取值是任意的, σ 一般都取 0σ 意义是在于可能会在 S 位置瞎 jb 转上一些不计入答案的贡献

而当我们把 z 取到 4mod4|S|+1 干掉了后面那个 ,除 4|S| 相当于还原

于是这样快乐 FWT 即可,O(n2n)

Code

#include<bits/stdc++.h>
#define int long long
using namespace std;

char s[1<<21|5],t[1<<21|5];
int f[1<<21|5],g[1<<21|5];
int n;

void fwt(int *f,int op){
    for(int i=2;i<=(1<<n);i<<=1)
        for(int p=i>>1,j=0;j<(1<<n);j+=i)
            for(int k=j;k<j+p;++k)f[k+p]+=op*f[k];
}

signed main(){
    scanf("%lld%s%s",&n,s,t);
    for(int i=0;i<(1<<n);++i)f[i]=(s[i]&15ll)<<(__builtin_popcount(i)<<1);fwt(f,1);
    for(int i=0;i<(1<<n);++i)g[i]=(t[i]&15ll)<<(__builtin_popcount(i)<<1);fwt(g,1);
    for(int i=0;i<(1<<n);++i)f[i]*=g[i]; fwt(f,-1);
    for(int i=0;i<(1<<n);++i)putchar(f[i]>>(__builtin_popcount(i)<<1)&3|48);
    return 0;
}

完结撒花!!!!✿✿ヽ(°▽°)ノ✿

posted @   _Famiglistimo  阅读(1640)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 2025年我用 Compose 写了一个 Todo App
· 张高兴的大模型开发实战:(一)使用 Selenium 进行网页爬虫
点击右上角即可分享
微信分享提示