集合幂级数

位运算卷积#

对于长度为2n的数列(下标[0,2n1])A,B,定义 Ci=jk=iAj×Bk,其中 是位运算,该操作被称作位运算卷积,记作 C=AB.

因为 FFT 的思想是将 A,B 转化为点值表示法,相乘之后再转成多项式,所以我们可以类似地,寻找一个变换 FWT(A),使得FWT(C)=FWT(A)FWT(B),这里是点乘,同时我们希望 FWT(A)是一个线性变换,也就是说,FWT(A+B)=FWT(A)+FWT(B),kFWT(A)=FWT(kA)

不妨设 FWT(A)i=j=02n1f(i,j)Aj,也就是第 j 位对第 i 位的贡献系数为 f(i,j)

那也就是说,

(jf(i,j)Aj)×(kf(i,k)Bk)=(pf(i,p)Cp)

jkf(i,j)f(i,k)AjBk=pf(i,p)Cp

同时,因为 AB=C

Cp=jk=iAjBk

pf(i,p)Cp=pf(i,p)jk=iAjBk

jkf(i,j)f(i,k)AjBk=pf(i,p)jk=iAjBk

jkf(i,j)f(i,k)AjBk=jkf(i,jk)AjBk

对比系数,可得 f(i,j)f(i,k)=f(i,jk)

同时,可以发现我们上述的推导没有用到任何位运算的性质,也就是说,普通的多项式卷积也符合上述条件,即 xjxk=xj+k

不过就算我们现在有了 f 依然不能快速计算 FWT(A)

我们不妨给 f(i,j) 加上更强的限制,让 两个数的 f 等于他们每个二进制位上两个数的 f 的乘积。

那我们考虑分治计算 FWT(A) ,.

FWT(A)i=j=02n11f(i,j)Aj+j=2n12n1f(i,j)Aj

FWT(A)i=f(c,0)j=02n11f(i,j)Aj+f(c,1)j=2n12n1f(i,j)Aj

其中 ci 的最高位,i,ji,j去掉最高位之后的数。

容易发现按位去做就可以变成一个子问题了(看看代码就会了)。

那么我们求出 FWT(C) 之后,只需要逆用 FWT(C),也就是被称为 IFWT(C) 就好了,和上述过程几乎一模一样,只是 f 这个矩阵变成了原本的逆矩阵就好了,这也就是说,位运算卷积适用的情况就是 f(i,j)f(i,k)=f(i,jk),且矩阵 f 有逆矩阵。

基础位运算卷积#

或卷积#

C=A|B

注意到 ji 的子集且 ki 的子集等价于 j|ki 的子集。

也就是说,构造 f(a,b)=[a|b=a],也就是说矩阵为:

[1011]

我们可以直接得出 f 的逆矩阵:

[1011]

与卷积#

C=A&B

仿照上面构造 f(a,b)=[a|b=b]

矩阵为:

[1101]

逆矩阵:

[1101]

异或卷积#

需要一点点观察技巧,构造 f(a,b)=(1)|a&b|,这是因为 (1)|i&j|(1)|i&k|=(1)|i&j|+|i&k|,注意到如果某一位[i&j]=[i&k],没有贡献,也就是说,否则只要 i 这一位 是 1 就有贡献,因此 等价于 1|i&(jk)|

矩阵为:

[1111]

逆矩阵:

[0.50.50.50.5]

K 进制卷积#

FWT(A)的部分是类似的,重点还是构造 f.

高维 max 卷积#

也就是每一位取 max

类似地,设 f(a,b)=[max(a,b)=a],显然是符合要求的。

K=4 为例:
矩阵:

[1000110011101111]

也就是下三角矩阵。

逆矩阵:

[1000110001100011]

高维 min 卷积#

把上面的矩阵改成上三角就好了。

高维异或卷积#

需要用点科技,先放放吧。

Examples#

CF1119H Triple#

solution#

对于第 i 个数组,设集合幂级数 Fi(x)=X×xai+Y×xbi+Z×xci
那么Ans=i=1nFi
其中乘法为异或卷积。
考虑把每个 FiFWT 然后点乘之后再 IFWT
依次考虑每一位 i,算出 j=1nFWT(Fj)i=j=1n(1)|i&aj|+(1)|i&bj|+(1)|i&cj|

这样看似乎不可做,但是考虑后面只有 23中不同的情况,我们算出每种情况的个数就可以直接计算了。

但是 8 种还是有点多,考虑令 ai,bi,ci 都异或 ai,最后再异或 i=1nai

那么现在 |i&aj|=0,贡献确定了,就只剩下 4 种情况了。

X+Y+ZX+YZXY+Z,XYZ 的个数分别是 A,B,C,D,考虑找一些等量关系。

首先由:

  • A+B+C+D=n

然后,我们考虑带入一些 X,Y,Z的特值。

  • 只考虑xbi

那么 i=1nFWT(Fi)=A+BCD

因为 FWT(F+G)=FWT(F)+FWT(G),所以 i=1nFWT(Fi)=FWT(i=1nFi)

  • 只考虑 xci

那么 i=1nFWT(Fi)=AB+CD

  • 只考虑 xbici

那么 i=1nFWT(Fi)=ABC+D

这样就可以解出来 A,B,C,D 了。

Bonus#

考虑拓展到 任意的 k 元组。

仿照上面的思路,我们枚举一个子集 S[1,2k1],计算只考虑 xuSwu,那么 FWT(Fj)i=uS(1)|i&wj,u|

那么 jFWT(Fj)i=u=02n1Cufu,S,其中 cuu 这种情况的出现次数, fu,S 是贡献系数。

容易发现 fu,S=(1)|u&S|,因此我们对于每个i,求出一个长度为2k的数组H,那么 H=FWT(c),因此只需要IFWT(H)就可以啦

CF582E Boolean Function#

solution#

注意到一共有 24 种不同的 A,B,C,D的取值,也就是说一共有 216种不同的结果,所以建出表达式树,那么每个非叶节点的dp为儿子的dp值的位运算卷积。
复杂度O(16n216)

#include<bits/stdc++.h>
using namespace std;
const int N = 520;
const int mod = 1e9+7;
void FWTor(int *fwt,int n,int opt)
{
	for(int len=2,l=1;len<=n;len<<=1,l<<=1)
	for(int i=0;i<n;i+=len)
	for(int j=i;j<i+l;j++)
	fwt[j+l]=(fwt[j+l]+1ll*fwt[j]*opt%mod+mod)%mod;
}
void FWTand(int *fwt,int n,int opt)
{
	for(int len=2,l=1;len<=n;len<<=1,l<<=1)
	for(int i=0;i<n;i+=len)
	for(int j=i;j<i+l;j++)
	fwt[j]=(fwt[j]+1ll*fwt[j+l]*opt%mod+mod)%mod;
}
int dp[N][(1<<16)+7];
char s[N];
int get(char c)
{
	int res=0,state=0;
	for(int S=0;S<(1<<4);S++)
	{
		if('A'<=c&&c<='D')res=((S>>(c-'A'))&1);
		if('a'<=c&&c<='d')res=1-((S>>(c-'a'))&1);
		state=state|(res<<S);
	}
	return state;
}
int match[N],L[(1<<16)],R[1<<16];
void And(int *A,int *B,int *C)
{
	for(int i=0;i<(1<<16);i++)L[i]=B[i],R[i]=C[i];
	FWTand(L,(1<<16),1);FWTand(R,(1<<16),1);
	for(int i=0;i<(1<<16);i++)L[i]=1ll*L[i]*R[i]%mod;
	FWTand(L,(1<<16),-1);
	for(int i=0;i<(1<<16);i++)A[i]=(A[i]+L[i])%mod;
}
void Or(int *A,int *B,int *C)
{
	for(int i=0;i<(1<<16);i++)L[i]=B[i],R[i]=C[i];
	FWTor(L,(1<<16),1);FWTor(R,(1<<16),1);
	for(int i=0;i<(1<<16);i++)L[i]=1ll*L[i]*R[i]%mod;
	FWTor(L,(1<<16),-1);
	for(int i=0;i<(1<<16);i++)A[i]=(A[i]+L[i])%mod;
}
void solve(int l,int r)
{
	if(l==r)
	{
		if(s[l]=='?')
		{
			for(int c='A';c<='D';c++)
			{
				int state=get(c);
				dp[l][state]=(dp[l][state]+1)%mod;
			}
			for(int c='a';c<='d';c++)
			{
				int state=get(c);
				dp[l][state]=(dp[l][state]+1)%mod;
			}
		}
		else 
		{
			int state=get(s[l]);
			dp[l][state]=(dp[l][state]+1)%mod;
		}
		return;
	}
	solve(l+1,match[l]-1);
	solve(match[r]+1,r-1);
	int x=match[l]+1;
	if(s[x]!='|')And(dp[l],dp[l+1],dp[match[r]+1]);
	if(s[x]!='&') Or(dp[l],dp[l+1],dp[match[r]+1]);
}
int n,m;
int stk[N],top=0;
int A[N],B[N],C[N],D[N],E[N];
int main()
{
	scanf("%s",s+1);
	n=strlen(s+1);
	for(int i=1;i<=n;i++)
	{
		if(s[i]!='('&&s[i]!=')')continue;
		if(s[i]=='(')stk[++top]=i;
		else 
		{
			int x=stk[top--];
			match[x]=i;
			match[i]=x;
		}
	}
	cin>>m;
	for(int i=1;i<=m;i++)
	cin>>A[i]>>B[i]>>C[i]>>D[i]>>E[i];
	solve(1,n);
	int ans=0;
	for(int S=0;S<(1<<16);S++)
	if(dp[1][S])
	{
		bool flag=1;
		for(int i=1;i<=m;i++)
		{
			int res=0;
			res+=(A[i]<<0);
			res+=(B[i]<<1);
			res+=(C[i]<<2);
			res+=(D[i]<<3);
			if((S>>res)%2!=E[i])flag=0;
		}
		if(flag)ans=(ans+dp[1][S])%mod;
	}
	cout<<ans;
	return 0;
}

CF908H New Year and Boolean Bridges#

solution#

对于用"A"相连的边,他们一定构成了强连通分量,我们先把他们连起来,对于 "X" 的边,他们必须不能再同一个强连通分量。

对于一个强连通分量,最少的边数显然是一个环。

现在我们就是要合并一些连通分量使得缩点之后剩下一条链。

对于两个连通分量,我们可以将他们变成一个强连通分量,或者连成一条链。

如果环的大小分别是A,B,前者代价是 A+B,后者是 A+B+1,因此我们肯定要贪心的把两个小的合并成一个大的。

因为大小为 1 的连通分量不影响,所以设最终大小>1的强连通分量个数为 C,代价为 n+C1

如果我们设把S这个点集联通的最小代价是 dp[S],那么转移就是min子集卷积,显然不可做。

但是注意到代价小于等于 n,因此不妨改成 dp[E][S]表示能否把S合并成E个连通分量,转移是子集卷积,但是注意到如果一个集合合法,他的子集都合法,因此可以直接用或卷积,最终第一次合法的E就是答案

// LUOGU_RID: 107709248
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 48;
const int M = (1<<23)+7;
char s[N][N];
int popcnt[M],lg[M];
int f(int x,int y){return ((x|y)==x)*((popcnt[x^y]&1)?-1:1);}
void FWT(int *fwt,int n)
{
    for(int len=2,l=1;len<=n;len<<=1,l<<=1)
    for(int i=0;i<n;i+=len)
    for(int j=i;j<i+l;j++)
    fwt[j+l]=fwt[j+l]+fwt[j];
}
int fa[N],siz[N];
int mask[N];
int n,tot;
int col[N];
int dem[M];
int calc[M],dp[M];
int Find(int x)
{
    if(x==fa[x])return x;
    return fa[x]=Find(fa[x]);
}
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)fa[i]=i;
    for(int i=1;i<=n;i++)
    {
        scanf("%s",s[i]+1);
        for(int j=1;j<i;j++)
        if(s[i][j]=='A')fa[Find(i)]=Find(j);
    }
    for(int i=1;i<=n;i++)siz[Find(i)]++;
    for(int i=1;i<=n;i++)
    if(i==Find(i))
    {
        if(siz[i]==1)continue;
        col[i]=++tot;
    }
    if(tot==0)
    {
        printf("%d\n",n-1);
        return 0;
    }
    for(int i=1;i<=n;i++)
    for(int j=1;j<i;j++)
    if(s[i][j]=='X')
    {
        int x=Find(i),y=Find(j);
        if(x==y)
        {
            cout<<"-1";
            return 0;
        }
        if(siz[x]==1||siz[y]==1)continue;
        mask[col[x]]|=(1<<(col[y]-1));
        mask[col[y]]|=(1<<(col[x]-1));
    }
    lg[0]=-1;
    calc[0]=1;
    for(int i=1;i<(1<<tot);i++)
    {
        lg[i]=lg[i>>1]+1;
        popcnt[i]=popcnt[i>>1]+(i&1);
        int t=lg[i&-i]+1;
        if((mask[t]&(i-(i&-i)))==0)calc[i]=calc[i-(i&-i)];
    }
    for(int i=0;i<(1<<tot);i++)dem[i]=f((1<<tot)-1,i),dp[i]=1;
    FWT(calc,(1<<tot));
    for(int E=0;;E++)
    {
        for(int i=0;i<(1<<tot);i++)dp[i]=dp[i]*calc[i];
        int res=0;
        for(int i=0;i<(1<<tot);i++)res=res+dp[i]*dem[i];
        if(res)
        {
            printf("%d\n",n+E);
            return 0;
        }
    }
    return 0;
}

CF838C Future Failure#

solution#

对于一个字符串,如果i出现了ai次,那么不同的排列方式个数为C=n!ai!

结论1:如果2|C,先手必胜

证明:

若先手可以通过删字符达到一个必败态,显然先手必胜。

否则先手只能选择重排,重排之后如果对手选择删字符,根据上面,必败。

因此后手也会重排,但是因为C是偶数,最终必定是先手赢。

C 是奇数,那么先手必须选择删字符。

先手肯定尽量不想让下一步的局面变成 2|C,因此必须保持奇数。

不妨删除一个2的次数最小的ai然后-1,显然这个次数一定不超过n里面2的次数,因此删掉之后方案数乘上an,显然2的个数不会增加,因此必定还是奇数。

这也就是说,谁先删完谁赢,即如果n是偶数就输,奇数就赢。

现在考虑计数。

如果n是奇数,显然任意的都合法,方案数kn

根据勒让德定理,(a+ba)p 的次数为p进制下a+b的进位次数,显然也可以拓展到多个数。

因此排列个数是奇数的充要条件是a1|a2|ak=a1+a2ak

也就是子集卷积,然后就完了。

#include<bits/stdc++.h>
using namespace std;
const int N = (1<<18)+1;
int n,k,mod;
typedef long long LL;
const int B = 19;
inline int plu(int x,int y){return (x+y>=mod?x+y-mod:x+y);}
inline int dec(int x,int y){return (x-y<0?x-y+mod:x-y);}
int Pow(int a,int b)
{
	int res=1;
	while(b)
	{
		if(b&1)res=1ll*res*a%mod;
		a=1ll*a*a%mod;
		b>>=1;
	}
	return res;
}
int limit;
int D;
void FWT(int *f)
{
	for(int len=2,l=1;len<=limit;len<<=1,l<<=1)
	for(int i=0;i<limit;i+=len)
	for(int j=i;j<i+l;j++)
	f[j+l]=plu(f[j+l],f[j]); 
}
void IFWT(int *f)
{
	for(int len=2,l=1;len<=limit;len<<=1,l<<=1)
	for(int i=0;i<limit;i+=len)
	for(int j=i;j<i+l;j++)
	f[j+l]=dec(f[j+l],f[j]); 
} 
int dp[B][(1<<B)],f[B][(1<<B)];
int popcnt[N];
int ifac[N],fac[N];
void put(int *f)
{
	for(int i=0;i<limit;i++)
	cout<<f[i]<<' ';
	cout<<endl;
}
int main()
{
	cin>>n>>k>>mod;
	if(n%2==1)
	{
		cout<<Pow(k,n);
		return 0;
	}
	ifac[1]=1;
	fac[0]=1;
	limit=(1<<18);
	for(int i=1;i<limit;i++)
	popcnt[i]=popcnt[i>>1]+(i&1);
	int D=popcnt[n];
	for(int i=2;i<limit;i++)
	ifac[i]=1ll*ifac[mod%i]*(mod-mod/i)%mod;
	ifac[0]=1;
	for(int i=1;i<limit;i++)
	ifac[i]=1ll*ifac[i-1]*ifac[i]%mod;
	for(int i=1;i<limit;i++)
	fac[i]=1ll*fac[i-1]*i%mod;
	for(int i=0;i<limit;i++)
	f[popcnt[i]][i]=ifac[i],dp[0][i]=1;
	for(int i=0;i<=D;i++)
	FWT(f[i]);
	int u=k;
	while(k)
	{
		if(k&1) 
		{

			for(int i=D;i>=0;i--)
			for(int j=0;j<i;j++)
			for(int k=0;k<limit;k++)
			dp[i][k]=plu(dp[i][k],1ll*dp[j][k]*f[i-j][k]%mod);			
		}
		for(int i=D;i>=0;i--)
		for(int j=0;j<i;j++)
		for(int k=0;k<limit;k++)
		f[i][k]=plu(f[i][k],1ll*f[j][k]*f[i-j][k]%mod);
		k>>=1;
	}
	IFWT(dp[popcnt[n]]);
	int ans=dec(Pow(u,n),1ll*dp[popcnt[n]][n]*fac[n]%mod);
	cout<<ans;
	return 0;
}
 

CF1326F2 Wise Men (Hard Version)#

solution#

因为同时要求有边和无边,比较难处理,所以不妨我们先不处理无边的限制,也就是说对于1,我们还是限制两点有边,对于0,我们不做限制。

可以发现这时候的0既可以当做0又可以当做1,因此现在求的答案是原答案的高维后缀和,差分回去就好了。

那么现在序列可以被划分成若干连续段,每段都是1。

对应到原图上,就是原图被划分成若干条链,我们把单点也算作链。

首先求出gS表示选出一条覆盖的点集为S的链的方案数。

我们把它存到G|S|,S中,就类似于子集卷积中的占位集合幂级数。

因为考虑到最终01段的划分是无序的,所以实际上的状态数是18的分拆数是很小的。

我们直接爆搜分拆数就好了。

对于方案数,我们只需要求出最终点集的并集为S的方案数fS就好了。

为什么?乍一看我们这样做无法保证点集之间互不相交,但是事实上所有幂级数的大小之和就是n,那我们既然覆盖的点集也是n,自然就不交了。

因此我们只需要做或卷积就行了。

先对每一个Gi做FWTor,然后dfs的时候直接点值相乘。

最后我们应该要再做IFWTor,但是我们只需要求2n1项的值,所以根据IFWT的本质是高维差分,直接容斥计算就好了。

#include<bits/stdc++.h>
using namespace std;
const int N = 19;
const int M = (1<<18)+7;
typedef long long LL;
int n;
void FWTor(LL *fwt,int lim)
{
	for(int len=2,l=1;len<=lim;len<<=1,l<<=1)
	for(int i=0;i<lim;i+=len)
	for(int j=i;j<i+l;j++)
	fwt[j+l]+=fwt[j];
} 
void IFWT(LL *fwt,int lim)
{
	for(int len=2,l=1;len<=lim;len<<=1,l<<=1)
	for(int i=0;i<lim;i+=len)
	for(int j=i;j<i+l;j++)
	fwt[j]-=fwt[j+l];
}
LL dp[N][M];
LL g[N][M];
char mp[N][N];
inline int bit(int x){return 1ll<<(x-1);} 
inline int get(int x,int v){return (x>>(v-1))&1;}
int popcnt[M];
int U;
LL f[N][M];
map<vector<int> ,LL> DP;
vector<int> perm;
void GenerSet(int x,int a,int k)
{
	if(x==n+1)
	{
		LL ans=0;
		for(int S=0;S<=U;S++)
		{
			int c=popcnt[U^S];
			if(c&1) ans-=f[k][S];
			else ans+=f[k][S];
		}
		DP[perm]=ans;
		return;
	}
	for(int i=a;i+x<=n+1;i++)
	{
		perm.push_back(i);
		for(int S=0;S<=U;S++)
		f[k+1][S]=g[i][S]*f[k][S]; 
		GenerSet(x+i,i,k+1);
		perm.pop_back();
	}
}
LL ret[M];
int main()
{
	cin>>n;
	U=(1<<n)-1;
	for(int i=1;i<=n;i++)
	scanf("%s",mp[i]+1);
	for(int i=1;i<=n;i++)
	dp[i][bit(i)]=1;
	for(int i=1;i<=U;i++)popcnt[i]=popcnt[i>>1]+(i&1);
	for(int S=1;S<=U;S++)
	for(int i=1;i<=n;i++)
	if(dp[i][S])
	{
		for(int j=1;j<=n;j++)
		if(get(S,j)==0&&mp[i][j]=='1')
		dp[j][S|bit(j)]+=dp[i][S];
		g[popcnt[S]][S]+=dp[i][S];
	}
	for(int i=1;i<=n;i++)FWTor(g[i],U+1);
	for(int i=0;i<=U;i++)f[0][i]=1;
	GenerSet(1,1,0); 
	for(int S=0;S<(1<<(n-1));S++)
	{
		perm.clear();
		int pre=-1;
		for(int i=0;i<n-1;i++)
		if(((S>>i)&1)==0) 
		{
			perm.push_back(i-pre);
			pre=i;
		}
		perm.push_back(n-1-pre);
		sort(perm.begin(),perm.end());
		ret[S]=DP[perm];
	}
	IFWT(ret,(1<<(n-1)));
	for(int i=0;i<(1<<(n-1));i++)
	printf("%lld ",ret[i]);
	return 0;
}
posted @   Larunatrecy  阅读(12)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示
主题色彩