集合幂级数
位运算卷积#
对于长度为的数列(下标),定义 ,其中 是位运算,该操作被称作位运算卷积,记作 .
因为 的思想是将 转化为点值表示法,相乘之后再转成多项式,所以我们可以类似地,寻找一个变换 ,使得,这里是点乘,同时我们希望 是一个线性变换,也就是说,。
不妨设 ,也就是第 位对第 位的贡献系数为 。
那也就是说,
同时,因为 ,
即
对比系数,可得
同时,可以发现我们上述的推导没有用到任何位运算的性质,也就是说,普通的多项式卷积也符合上述条件,即 。
不过就算我们现在有了 依然不能快速计算 。
我们不妨给 加上更强的限制,让 两个数的 等于他们每个二进制位上两个数的 的乘积。
那我们考虑分治计算 ,.
其中 是 的最高位,是 去掉最高位之后的数。
容易发现按位去做就可以变成一个子问题了(看看代码就会了)。
那么我们求出 之后,只需要逆用 ,也就是被称为 就好了,和上述过程几乎一模一样,只是 这个矩阵变成了原本的逆矩阵就好了,这也就是说,位运算卷积适用的情况就是 ,且矩阵 有逆矩阵。
基础位运算卷积#
或卷积#
注意到 是 的子集且 是 的子集等价于 是 的子集。
也就是说,构造 ,也就是说矩阵为:
我们可以直接得出 的逆矩阵:
与卷积#
仿照上面构造
矩阵为:
逆矩阵:
异或卷积#
需要一点点观察技巧,构造 ,这是因为 ,注意到如果某一位,没有贡献,也就是说,否则只要 这一位 是 1 就有贡献,因此 等价于 。
矩阵为:
逆矩阵:
进制卷积#
求 的部分是类似的,重点还是构造 .
高维 max 卷积#
也就是每一位取 。
类似地,设 ,显然是符合要求的。
以 为例:
矩阵:
也就是下三角矩阵。
逆矩阵:
高维 min 卷积#
把上面的矩阵改成上三角就好了。
高维异或卷积#
需要用点科技,先放放吧。
Examples#
CF1119H Triple#
solution#
对于第 个数组,设集合幂级数 。
那么。
其中乘法为异或卷积。
考虑把每个 先 然后点乘之后再 。
依次考虑每一位 ,算出
这样看似乎不可做,但是考虑后面只有 中不同的情况,我们算出每种情况的个数就可以直接计算了。
但是 种还是有点多,考虑令 都异或 ,最后再异或 。
那么现在 ,贡献确定了,就只剩下 种情况了。
设 的个数分别是 ,考虑找一些等量关系。
首先由:
然后,我们考虑带入一些 的特值。
- 只考虑
那么
因为 ,所以
- 只考虑
那么
- 只考虑
那么
这样就可以解出来 了。
Bonus#
考虑拓展到 任意的 元组。
仿照上面的思路,我们枚举一个子集 ,计算只考虑 ,那么 。
那么 ,其中 是 这种情况的出现次数, 是贡献系数。
容易发现 ,因此我们对于每个,求出一个长度为的数组,那么 ,因此只需要就可以啦
CF582E Boolean Function#
solution#
注意到一共有 种不同的 的取值,也就是说一共有 种不同的结果,所以建出表达式树,那么每个非叶节点的为儿子的值的位运算卷积。
复杂度
#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" 的边,他们必须不能再同一个强连通分量。
对于一个强连通分量,最少的边数显然是一个环。
现在我们就是要合并一些连通分量使得缩点之后剩下一条链。
对于两个连通分量,我们可以将他们变成一个强连通分量,或者连成一条链。
如果环的大小分别是,前者代价是 ,后者是 ,因此我们肯定要贪心的把两个小的合并成一个大的。
因为大小为 的连通分量不影响,所以设最终大小的强连通分量个数为 ,代价为 。
如果我们设把这个点集联通的最小代价是 ,那么转移就是子集卷积,显然不可做。
但是注意到代价小于等于 ,因此不妨改成 表示能否把合并成个连通分量,转移是子集卷积,但是注意到如果一个集合合法,他的子集都合法,因此可以直接用或卷积,最终第一次合法的就是答案
// 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#
对于一个字符串,如果出现了次,那么不同的排列方式个数为
结论1:如果,先手必胜
证明:
若先手可以通过删字符达到一个必败态,显然先手必胜。
否则先手只能选择重排,重排之后如果对手选择删字符,根据上面,必败。
因此后手也会重排,但是因为是偶数,最终必定是先手赢。
若 是奇数,那么先手必须选择删字符。
先手肯定尽量不想让下一步的局面变成 ,因此必须保持奇数。
不妨删除一个的次数最小的然后-1,显然这个次数一定不超过里面的次数,因此删掉之后方案数乘上,显然的个数不会增加,因此必定还是奇数。
这也就是说,谁先删完谁赢,即如果是偶数就输,奇数就赢。
现在考虑计数。
如果是奇数,显然任意的都合法,方案数。
根据勒让德定理, 中 的次数为进制下的进位次数,显然也可以拓展到多个数。
因此排列个数是奇数的充要条件是。
也就是子集卷积,然后就完了。
#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。
对应到原图上,就是原图被划分成若干条链,我们把单点也算作链。
首先求出表示选出一条覆盖的点集为的链的方案数。
我们把它存到中,就类似于子集卷积中的占位集合幂级数。
因为考虑到最终01段的划分是无序的,所以实际上的状态数是18的分拆数是很小的。
我们直接爆搜分拆数就好了。
对于方案数,我们只需要求出最终点集的并集为的方案数就好了。
为什么?乍一看我们这样做无法保证点集之间互不相交,但是事实上所有幂级数的大小之和就是,那我们既然覆盖的点集也是,自然就不交了。
因此我们只需要做或卷积就行了。
先对每一个做FWTor,然后的时候直接点值相乘。
最后我们应该要再做IFWTor,但是我们只需要求项的值,所以根据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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现