FWT & FMT & 集合幂级数学习笔记
集合幂级数
集合幂级数听起来很难,实则不难理解。简单来说,集合幂级数就是把一个全集的每个子集都映射到一个值。
那么集合幂级数具体如何表示的呢?我们考虑用一个类多项式表示,在全集
所以,集合幂级数
加减法
集合幂级数的加减法很简单,只要将对应系数相加减即可,形式化来说:
乘法
这个比较麻烦了,我们希望集合幂级数的乘法对加法有分配律。形式化来说就是要满足:
其中
引入
FFT 大家应该都了解过了,它们都是对于加法卷积的快速变换。同理,对于位运算卷积,也有对应的快速变换,那就是 FWT 和 FMT。
先来看位运算卷积的形式:
其中
我们希望这个
下文可能会将集合直接看作二进制数考虑,将集合间的运算直接看作位运算,请读者自行转化。由于一般情况下
FWT
FWT 的应用场景更多,我认为更重要,所以先讲 FWT。
我们不妨设
既然要转化成点乘的形式,那么
由于 FFT 是一个线性变换,我们希望 FWT 也是一个线性变换,所以我们可以设
根据
根据
注意到原式等价于
由于位运算的独立性,我们可以拆位考虑。假设我们已经知道了
考虑构造
现在我们已经有了符合要求的
观察以下式子:
暴力求和显然是
考虑像 FFT 一样分治,设
接下来,分类讨论
所以,我们可以
以下我将分三种位运算具体讲解贡献系数
构造原理就是根据
集合并卷积
最基本的式子:
逐步推导:
-
令
j=k ,得c(i,j)^2=c(i,j) ,所以c(i,j)\in\{0,1\} 。 -
考虑
i=0 的情况:- 令
j=0,k=1 :c(0,0)c(0,1)=c(0,1) 。- 若
c(0,1)=0 ,则等式成立。 - 若
c(0,1)\neq0 ,则c(0,0)=1 。
- 若
- 令
-
考虑
i=1 的情况:- 令
j=0,k=1 :c(1,0)c(1,1)=c(1,1) 。- 若
c(1,1)=0 ,则等式成立。 - 若
c(1,1)\neq0 ,则c(1,0)=1 。
- 若
- 令
-
综合以上条件得到可行且可逆的矩阵:
-
-
\begin{bmatrix}1&1\\1&0\end{bmatrix}
-
注意到对于矩阵
那么,利用矩阵
对矩阵求逆,得到
:::info[代码]{open}
void FWTOR(ll*F){
for(int l=1;l<n;l<<=1){
for(int p=0;p<n;p+=l<<1){
for(int i=p;i<p+l;i++){
F[i+l]=(F[i]+F[i+l])%P;
}
}
}
}
void IFWTOR(ll*F){
for(int l=1;l<n;l<<=1){
for(int p=0;p<n;p+=l<<1){
for(int i=p;i<p+l;i++){
F[i+l]=(P-F[i]+F[i+l])%P;
}
}
}
}
:::
集合交卷积
最基本的式子:
逐步推导:
-
令
j=k ,得c(i,j)^2=c(i,j) ,所以c(i,j)\in\{0,1\} 。 -
考虑
i=0 的情况:- 令
j=0,k=1 :c(0,0)c(0,1)=c(0,0) 。- 若
c(0,0)=0 ,则等式成立。 - 若
c(0,0)\neq0 ,则c(0,1)=1 。
- 若
- 令
-
考虑
i=1 的情况:- 令
j=0,k=1 :c(1,0)c(1,1)=c(1,0) 。- 若
c(1,0)=0 ,则等式成立。 - 若
c(1,0)\neq0 ,则c(1,1)=1 。
- 若
- 令
-
综合以上条件得到可行且可逆矩阵:
-
-
\begin{bmatrix}0&1\\1&1\end{bmatrix}
-
注意到对于矩阵
那么,利用矩阵
对矩阵求逆,得到
:::info[代码]{open}
void FWTAND(ll*F){
for(int l=1;l<n;l<<=1){
for(int p=0;p<n;p+=l<<1){
for(int i=p;i<p+l;i++){
F[i]=(F[i]+F[i+l])%P;
}
}
}
}
void IFWTAND(ll*F){
for(int l=1;l<n;l<<=1){
for(int p=0;p<n;p+=l<<1){
for(int i=p;i<p+l;i++){
F[i]=(F[i]+P-F[i+l])%P;
}
}
}
}
:::
集合对称差卷积
最基本的式子:
逐步推导:
-
考虑
j=k=0 ,得c(i,0)^2=c(i,0) ,所以c(i,0)\in\{0,1\} 。- 如果
c(0,0)=0 ,取k=0 :c(0,j)c(0,0)=c(0,j) ,得0=c(0,j) ,所以第一行全零,矩阵不可逆。 - 因此必须
c(0,0)=1 。
- 如果
-
考虑
i=0 :- 取
j=1,k=1 :c(0,1)^2=c(0,0)=1 ,所以c(0,1)\in\{-1,1\} 。
- 取
-
考虑
i=1 :- 取
j=1,k=1 :c(1,1)^2=c(1,0) 。 - 取
j=0,k=1 :c(1,0)c(1,1)=c(1,1) 。- 如果
c(1,1)\neq0 ,则c(1,0)=1 。 - 如果
c(1,1)=0 ,则由第一个式子得c(1,0)=0 ,第二行全零,矩阵不可逆。
- 如果
所以
c(1,1)=\pm1 且c(1,0)=1 。 - 取
-
综合以上条件得到可行且可逆矩阵:
-
-
\begin{bmatrix}1&-1\\1&1\end{bmatrix}
-
注意到,在矩阵
那么,利用矩阵
对矩阵求逆,得到
:::info[代码]{open}
void FWTXOR(ll*F){
for(int l=1;l<n;l<<=1){
for(int p=0;p<n;p+=l<<1){
for(int i=p;i<p+l;i++){
ll x=F[i],y=F[i+l];
F[i]=(x+y)%P,F[i+l]=(x-y)%P;
}
}
}
}
void IFWTXOR(ll*F){
FWTXOR(F);
for(int i=0;i<n;i++)F[i]=F[i]*qpow(n,P-2);
}
:::
FMT
还是稍微提一下 FMT,FMT 是 FWT 在或卷积和与卷积下的一种特例,其变换矩阵是上/下三角的0/1矩阵,对应高维前缀/后缀和。个人认为通过构造的方式更容易理解 FMT,所以我将通过构造的角度讲解。
还是一样的设
显然我们要想办法构造满足
集合或卷积
注意到,如果
根据
简单代换得到:
所以,我们成功证明了
显然正变换就是高维前缀和,逆变换就是高维差分,实现利用 SOS DP 即可,则时间复杂度为
:::info[代码]{open}
void FMTOR(ll*F){
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
if(j&(1<<i))F[j]=(F[j]+F[j-(1<<i)])%P;
}
}
}
void IFMTOR(ll*F){
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
if(j&(1<<i))F[j]=(F[j]-F[j-(1<<i)])%P;
}
}
}
:::
集合交卷积
注意到,如果
根据
简单代换得到:
所以,我们成功证明了
显然正变换就是高维后缀和,逆变换就是高维差分,实现利用 SOS DP 即可,则时间复杂度为
:::info[代码]{open}
void FMTAND(ll*F){
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
if(j&(1<<i))F[j-(1<<i)]=(F[j-(1<<i)]+F[j])%P;
}
}
}
void IFMTAND(ll*F){
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
if(j&(1<<i))F[j-(1<<i)]=(F[j-(1<<i)]-F[j])%P;
}
}
}
:::
例题
注:为了方便,以下一般记
P4717
模板题,使用 FWT/FMT 即可,但是对称差卷积只能用 FWT。
:::info[代码]
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=1<<17,P=998244353,I=499122177,
Cor[2][2] ={{1,0},{1,1}},
Cand[2][2] ={{1,1},{0,1}},
Cxor[2][2] ={{1,1},{1,P-1}},
ICor[2][2] ={{1,0},{P-1,1}},
ICand[2][2]={{1,P-1},{0,1}},
ICxor[2][2]={{I,I},{I,P-I}};
int n,a[N],b[N];
ll F[N],G[N];
void FWT(ll*F,const int c[2][2]){
for(int l=1;l<n;l<<=1){
for(int p=0;p<n;p+=l<<1){
for(int i=p;i<p+l;i++){
ll t0=F[i],t1=F[i+l];
F[i]=(c[0][0]*t0+c[0][1]*t1)%P;
F[i+l]=(c[1][0]*t0+c[1][1]*t1)%P;
}
}
}
}
void mul(ll*F,ll*G,const int C[2][2],const int IC[2][2]){
FWT(F,C),FWT(G,C);
for (int i=0;i<n;i++)F[i]=F[i]*G[i]%P;
FWT(F,IC);
}
int main(){
cin.tie(0)->sync_with_stdio(0);
cin>>n;
n=1<<n;
for(int i=0;i<n;i++)cin>>a[i];
for(int i=0;i<n;i++)cin>>b[i];
for(int i=0;i<n;i++)F[i]=a[i],G[i]=b[i];
mul(F,G,Cor,ICor);
for(int i=0;i<n;i++){
cout<<F[i]<<' ';
F[i]=a[i],G[i]=b[i];
}
cout<<'\n';
mul(F,G,Cand,ICand);
for(int i=0;i<n;i++){
cout<<F[i]<<' ';
F[i]=a[i],G[i]=b[i];
}
cout<<'\n';
mul(F,G,Cxor,ICxor);
for(int i=0;i<n;i++)cout<<F[i]<<' ';
}
:::
P6097
子集卷积的模板题,挺重要的,需要使用一个套路。
注意到有两个限制,限制
:::info[代码]
#include<bits/stdc++.h>
using namespace std;
const int N=1<<20,P=1e9+9;
int n,m,a[21][N],b[21][N],c[21][N];
int gt(int x){return __builtin_popcount(x);}
void fwt(int*f,int op){
for(int l=1;l<m;l<<=1){
for(int p=0;p<m;p+=l<<1){
for(int i=p;i<p+l;i++){
f[i+l]=(f[i]*op+f[i+l])%P;
}
}
}
}
int main(){
cin.tie(0)->sync_with_stdio(0);
cin>>n;
m=1<<n;
for(int i=0;i<m;i++)cin>>a[gt(i)][i];
for(int i=0;i<m;i++)cin>>b[gt(i)][i];
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;i+j<=n;j++){
for(int k=0;k<m;k++){
c[i+j][k]=(c[i+j][k]+1ll*a[i][k]*b[j][k])%P;
}
}
}
for(int i=0;i<=n;i++)fwt(c[i],-1);
for(int i=0;i<m;i++)cout<<(c[gt(i)][i]+P)%P<<' ';
}
:::
AT_arc100_c
直接算
考虑把答案转化为前缀和的形式,先算
但是该式如何计算?发现就是高维前缀和的最大值和次大值,在模板基础上多维护一个值即可。
:::info[代码]
#include<bits/stdc++.h>
using namespace std;
#define pi pair<int,int>
const int N=1<<18;
int n,s;
pi f[N];
void mx(pi&x,pi y){
if(x.first<y.first)x={y.first,max(x.first,y.second)};
else x={x.first,max(x.second,y.first)};
}
int main(){
cin.tie(0)->sync_with_stdio(0);
cin>>n;
n=1<<n;
for(int i=0;i<n;i++)cin>>f[i].first;
for(int l=1;l<n;l<<=1)for(int p=0;p<n;p+=l<<1)for(int i=p;i<p+l;i++)mx(f[i+l],f[i]);
s=f[0].first;
for(int i=1;i<n;i++)cout<<(s=max(s,f[i].first+f[i].second))<<'\n';
}
:::
CF449D
直接算按位与为
但是如何计算
:::info[代码]
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=1<<20,P=1e9+7;
int n,m=1<<20,x;
ll f[N];
ll qpow(ll a,int b){
ll s=1;
while(b){
if(b%2)s=s*a%P;
a=a*a%P,b/=2;
}
return s;
}
inline void fwt(ll*f,ll op){
for(int l=1;l<m;l<<=1){
for(int p=0;p<m;p+=l<<1){
for(int i=p;i<p+l;i++){
f[i]=(f[i]+f[i+l]*op)%P;
}
}
}
}
int main(){
cin.tie(0)->sync_with_stdio(0);
cin>>n;
for(int i=1;i<=n;i++){
cin>>x;
f[x]++;
}
fwt(f,1);
for(int i=0;i<m;i++)f[i]=qpow(2,f[i]);
fwt(f,-1);
cout<<(f[0]+P)%P;
}
:::
P12232
集合幂级数求逆的模板。
由于
显然,
直接做子集卷积即可,详见代码。
:::info[代码]
#include<bits/stdc++.h>
using namespace std;
#define gt __builtin_popcount
#define ll long long
const int N=1<<20,P=998244353;
int n,m,t,a[N],F[21][N],G[21][N];
ll qpow(ll a,int b){
ll s=1;
while(b){
if(b&1)s=s*a%P;
a=a*a%P,b/=2;
}
return s;
}
inline void fwt(int*f,int op){
for(int l=1;l<m;l<<=1){
for(int p=0;p<m;p+=l<<1){
for(int i=p;i<p+l;i++){
f[i+l]=(f[i]*op+f[i+l])%P;
}
}
}
}
int main(){
cin.tie(0)->sync_with_stdio(0);
cin>>n;
m=1<<n;
for(int i=0;i<m;i++){
cin>>a[i];
F[gt(i)][i]=a[i];
}
for(int i=0;i<=n;i++)fwt(F[i],1);
G[0][0]=t=qpow(a[0],P-2);
fwt(G[0],1);
for(int i=1;i<=n;i++){
for(int j=0;j<i;j++){
for(int s=0;s<m;s++){
G[i][s]=(G[i][s]-1ll*F[i-j][s]*G[j][s])%P;
}
}
for(int s=0;s<m;s++)G[i][s]=1ll*G[i][s]*t%P;
}
for(int i=0;i<=n;i++)fwt(G[i],-1);
for(int i=0;i<m;i++)cout<<(G[gt(i)][i]+P)%P<<' ';
}
:::
P13843
集合幂级数 exp 的模板,素数版本可直接套用非素数模数版本。
考虑泰勒展开得到
考虑组合意义,
尝试 DP,记
实现的时候保证
:::info[代码]{open}
m=1<<n;
for(int k=0;k<=n;k++){
for(int i=0;i<n;i++){
for(int s=0;s<m;s++){
if((s&1<<i)&&(s&-s)!=(1<<i))F[k][s]+=F[k][s^1<<i];
}
}
}
:::
以上代码可看作将
以下是完整代码:
:::info[代码]
#include<bits/stdc++.h>
using namespace std;
#define ll unsigned long long
#define gt __builtin_popcount
const int N=1<<20;
int n,m;
ll f[N],g[N],F[21][N],G[N],H[21][N];
int main(){
cin.tie(0)->sync_with_stdio(0);
cin>>n;
m=1<<n;
for(int i=0;i<m;i++){
cin>>f[i];
F[gt(i)][i]=f[i];
}
for(int k=0;k<=n;k++){
for(int i=0;i<n;i++){
for(int s=0;s<m;s++){
if((s&1<<i)&&(s&-s)!=(1<<i))F[k][s]+=F[k][s^1<<i];
}
}
}
for(int k=1;k<=n;k++){
for(int i=0;i<n;i++){
for(int s=0;s<m;s++){
if((s&1<<i)&&(s&-s)!=(1<<i))H[k][s]-=H[k][s^1<<i];
}
}
for(int s=0;s<m;s++){
G[s]=0;
if(gt(s)==k)G[s]=g[s]=f[s]+H[k][s];
}
for(int i=0;i<n;i++){
for(int s=0;s<m;s++){
if(s&1<<i)G[s]+=G[s^1<<i];
}
}
for(int j=0;j+k<=n;j++)for(int s=0;s<m;s++)H[j+k][s]+=G[s]*F[j][s];
}
g[0]=1;
for(int i=0;i<m;i++)cout<<g[i]<<' ';
}
:::
P13844
集合幂级数 ln 的模板,素数版本可直接套用非素数模数版本。
ln 运算是 exp 运算的逆运算,可以记
实现是类似的,以下是完整代码:
:::info[代码]
#include<bits/stdc++.h>
using namespace std;
#define ll unsigned long long
#define gt __builtin_popcount
const int N=1<<20;
int n,m;
ll f[N],g[N],h[N],F[21][N],G[N],H[21][N];
int main(){
cin.tie(0)->sync_with_stdio(0);
cin>>n;
m=1<<n;
for(int i=0;i<m;i++){
cin>>f[i];
F[gt(i)][i]=f[i];
}
for(int k=0;k<=n;k++){
for(int i=0;i<n;i++){
for(int s=0;s<m;s++){
if(s&1<<i)F[k][s]+=F[k][s^1<<i];
}
}
}
for(int k=1;k<=n;k++){
for(int s=0;s<m;s++)h[s]=H[k][s];
for(int i=0;i<n;i++){
for(int s=0;s<m;s++){
if((s&1<<i)&&(s&-s)!=(1<<i))h[s]-=h[s^1<<i];
}
}
for(int s=0;s<m;s++){
G[s]=0;
if(gt(s)==k)G[s]=g[s]=f[s]-h[s];
}
for(int i=0;i<n;i++){
for(int s=0;s<m;s++){
if((s&1<<i)&&(s&-s)!=(1<<i))G[s]+=G[s^1<<i];
}
}
for(int j=0;j+k<=n;j++)for(int s=0;s<m;s++)H[j+k][s]+=G[s]*F[j][s];
}
for(int i=0;i<m;i++)cout<<g[i]<<' ';
}
:::
P11734
比较模板的图计数,做图计数之前需要先知道一个基本式子:
其中
而本题中
:::info[代码]
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define gt __builtin_popcount
const int N=1<<20,P=998244353;
int n,m,x,y,a[N],b[N],F[21][N],G[21][N],H[N],t;
int qpow(ll a,int b){
ll s=1;
while(b){
if(b%2)s=s*a%P;
a=a*a%P,b/=2;
}
return s;
}
void fmt0(int*f,int op){for(int i=0;i<n;i++)for(int s=0;s<m;s++)if(s&1<<i)(f[s]+=op*f[s^1<<i])%=P;}
void fmt1(int*f,int op){for(int i=0;i<n;i++)for(int s=0;s<m;s++)if((s&1<<i)&&(s&-s)!=(1<<i))(f[s]+=op*f[s^1<<i])%=P;}
int main(){
cin.tie(0)->sync_with_stdio(0);
cin>>n>>m;
for(int i=1;i<=m;i++){
cin>>x>>y;
a[(1<<x-1)|(1<<y-1)]++;
}
m=1<<n;
fmt0(a,1);
for(int s=0;s<m;s++)a[s]=F[gt(s)][s]=qpow(2,a[s]);
for(int k=0;k<=n;k++)fmt0(F[k],1);
for(int k=1;k<=n;k++){
fmt1(G[k],-1);
for(int s=0;s<m;s++){
H[s]=0;
if(gt(s)==k)H[s]=b[s]=(a[s]-G[k][s])%P;
}
fmt1(H,1);
for(int j=0;j+k<=n;j++)for(int s=0;s<m;s++)G[j+k][s]=(G[j+k][s]+(ll)H[s]*F[j][s])%P;
}
memset(F,0,sizeof F),memset(G,0,sizeof G);
b[0]--;
for(int s=0;s<m;s++)F[gt(s)][s]=b[s]=-b[s];
for(int k=0;k<=n;k++)fmt0(F[k],1);
t=qpow(b[0],P-2);
for(int s=0;s<m;s++)G[0][s]=t;
for(int k=1;k<=n;k++){
for(int i=0;i<k;i++)for(int s=0;s<m;s++)G[k][s]=(G[k][s]-(ll)F[k-i][s]*G[i][s])%P;
for(int s=0;s<m;s++)G[k][s]=(ll)G[k][s]*t%P;
}
fmt0(G[n],-1);
cout<<(G[n][m-1]+P)%P;
}
:::