容斥与二项式反演
感谢 hotpotcondiment 对此页面的贡献
容斥原理
有 个集合 (可能集合有交),则至少在一个集合的元素为
应用
考虑一个由多个条件组成的集合 ,且满足这个由多个条件组成的集合 的元素个数为 ,(特别地,记 为元素的全集 的大小 ,感谢 black_trees 的指出)则
满足 中某些条件之一的元素个数为
同时不满足 中任意条件的元素个数为
例题1:CF997C Sky Full Of Stars
题意
有一个 的矩阵,用三种颜色染色,求至少有多少种方案使得至少有一行或一列是同一种颜色,对 取模。。
解法
考虑容斥。可以把题目中“至少有一行或一列是同一种颜色”的条件看成“满足 个条件之一:有一行或有一列为同一种颜色”。此时设至少 行和 列为同色的方案数为 ,则答案为:
显然有 ,。注意 的情况,由于一行的颜色相同决定 列的颜色必须相同,一列的颜色相同决定了 行的颜色必须相同;故而最后 行和 列的颜色必须相同。故而 。
故而答案可以表示成:
前面两项可以 算,关键是优化 的计算。考虑把与 无关的项移到 后,化简则有
其中最后两步用到了二项式定理,即 。
代码
点此查看代码
#include <bits/stdc++.h> using namespace std; #define ll long long const int maxn=1000010; const int md=998244353; int fac[maxn],inv[maxn]; int n,i,b=1; inline ll Pow(ll d,ll z){ ll ret=1; do{ if(z&1) ret=(ret*d)%md; d=(d*d)%md; }while(z>>=1); return ret; } inline ll C(int x,int y){return ((1LL*fac[y]*inv[x])%md)*inv[y-x]%md;} ll ans,tmp,sum; int main(){ fac[0]=inv[0]=1; for(i=1;i<maxn;++i) fac[i]=(1LL*fac[i-1]*i)%md; inv[maxn-1]=Pow(fac[maxn-1],md-2); for(i=maxn-2;i;--i) inv[i]=(1LL*inv[i+1]*(i+1))%md; scanf("%d",&n); for(i=1;i<=n;++i,b=-b){ sum+=b*((C(i,n)*Pow(Pow(3,1LL*n*i),md-2))%md* (Pow(1-Pow(Pow(3,n-i),md-2),n)-1))%md; ans+=(2*b*C(i,n)*Pow(3,1LL*n*(n-i)+i))%md; } ans=(ans%md+md)%md; sum=((sum%md+md)%md)*Pow(3,1LL*n*n+1); printf("%lld",(ans+sum)%md); return 0; }
例题2:ARC101E Ribbons on Tree
题意
有一棵大小为 的树。现在要对树上的点两两配对。然后对于每一对点 ,将 到 的简单路径上的所有边染色,定义一组合法的配对方案为能够把树上的所有边染色。求合法的配对方案数,对 取模。 且保证 为偶数。
解法
考虑容斥。设整棵树的边集为 ,且令对应的染色的所有边均在 边集内的方案数为 ,则可得总合法方案数为 。(可以把每一条边未被染色视为一个条件,然后需要所有边均被染色)
考虑 满足怎样的条件。显然它们构成了 个连通块,然后每对点均必须在同一个连通块中。
考虑每个连通块内有多少种配对方法。若该连通块大小为 (显然 为偶数),则第 个点有 个点进行配对,第 个未配对的点有 个点进行配对,以此类推可得该连通块内的总配对方案为 (记为 。特别地, 为奇数时,定义 为 )。同时若某个选边方案对应的所有连通块的集合为 ,则最后这种选边方案对应的配对方案为 。
考虑使用 dp 计算所有的方案数。设 表示在 的子树内,选定了 个连通块 (不包括 所在的连通块,因为最后还没有统计好 所在连通块的大小),且 目前所在的连通块大小为 的对应部分的统计完的连通块的对应方案数。从 的某个儿子 转移时,若选择保留 边,则 和 所在连通块相连接,有 。(此时不需要乘上有关 的系数,因为这个连通块还有和其他连通块合并且扩大的机会)否则,由于 所在的连通块只能通过 边继续扩大,故而此时 所在的连通块不会再次扩大,已经统计完成,可以乘上 所在的连通块大小对应的 值,故而有 。统计完成后,需要将 累加到 上,答案即为 。注意:在具体转移 dp 值时,需要把转移的 dp 值赋在另一个数组上,避免之前的 dp 值(现在已经非法,由于我们每次多扫描到 的一棵子树就要强制将其影响体现在 上)造成影响;在转移结束后再赋回来。
这样对于 子树内的 dp,设目前将 dp 值转移到 子树的所有子树大小之和为 ,且即将合并到 子树上的子树大小为 ,则此次统计会对总时间复杂度造成 的贡献。考虑组合意义,可得总时间复杂度等效于在树上选取任意两对不同的点(但是一对点可以是两个相同的点)的数量(它们会在它们的 lca 处对总时间复杂度有 的贡献),时间复杂度即为 。空间复杂度显然为 。
考虑优化。发现其实最后 dp 值对答案的贡献的容斥系数只与连通块的数量的奇偶性有关,所以可以把 数组的第二维变成选定的连通块的奇偶性(第二维的大小就只会有 ),同时把第二维涉及的加法转为异或,空间复杂度即可降为 。
考虑时间复杂度的计算。对于 子树内的 dp,设目前将 dp 值转移到 子树的所有子树大小之和为 ,且即将合并到 子树上的子树大小为 ,则此次统计会对总时间复杂度造成 的贡献。考虑组合意义,可得总时间复杂度等效于在树上选取任意一对不同的点的数量,时间复杂度即为 。
代码
点此查看代码
#include <bits/stdc++.h> using namespace std; const int maxn=5010; const int md=1000000007; int n,i,j,u,v,tot; int head[maxn],ver[maxn<<1],nxt[maxn<<1]; int h[maxn],dp[maxn][2][maxn],tdp[2][maxn]; int dfs(const int p,const int f){ int lp,to,siz=1,ssi; long long tmp; dp[p][0][1]=1; for(lp=head[p];lp;lp=nxt[lp]){ to=ver[lp]; if(to==f) continue; ssi=dfs(to,p); for(i=siz;i;--i){ for(j=1;j<=ssi;++j){ tdp[0][i+j]=(1LL*dp[p][0][i]*dp[to][0][j] +1LL*dp[p][1][i]*dp[to][1][j]+tdp[0][i+j])%md; tdp[1][i+j]=(1LL*dp[p][0][i]*dp[to][1][j] +1LL*dp[p][1][i]*dp[to][0][j]+tdp[1][i+j])%md; tdp[0][i]=((1LL*dp[p][0][i]*dp[to][1][j] +1LL*dp[p][1][i]*dp[to][0][j])%md*h[j]+tdp[0][i])%md; tdp[1][i]=((1LL*dp[p][0][i]*dp[to][0][j] +1LL*dp[p][1][i]*dp[to][1][j])%md*h[j]+tdp[1][i])%md; } } siz+=ssi; for(i=1;i<=siz;++i){ dp[p][0][i]=tdp[0][i]; dp[p][1][i]=tdp[1][i]; tdp[0][i]=tdp[1][i]=0; } } return siz; } int main(){ h[2]=1; for(i=4;i<maxn;i+=2) h[i]=(1LL*h[i-2]*(i-1))%md; scanf("%d",&n); for(i=1;i<n;++i){ scanf("%d%d",&u,&v); ver[++tot]=v; nxt[tot]=head[u]; head[u]=tot; ver[++tot]=u; nxt[tot]=head[v]; head[v]=tot; } dfs(1,0); u=dp[1][1][0]; v=dp[1][0][0]; for(i=1;i<=n;++i){ u=(u+1LL*h[i]*dp[1][0][i])%md; v=(v+1LL*h[i]*dp[1][1][i])%md; } printf("%d",(u-v+md)%md); return 0; }
引入:矩阵相关知识
单位矩阵
定义
满足 的 的矩阵为 阶单位矩阵。
性质
。
逆矩阵
定义
设 为 的矩阵,若存在一个 的矩阵 满足 ,则 为 的逆矩阵,记为 (有些矩阵不存在逆矩阵,例如全 矩阵)
高斯-若尔当法求逆矩阵
设 为 的矩阵,可以构造矩阵如下:
将矩阵高斯消元,得到下面的矩阵:
此时 即为 的逆矩阵。
转置矩阵
定义
设 为 的矩阵,若存在一个 的矩阵 满足 ,则 为 的转置矩阵,记为 。
性质
。
关系矩阵
定义
已知 和 为定义在 上的函数,构造 矩阵 和 如下:
若存在一个 的矩阵 满足 ,则称 为 到 的关系矩阵。
引入:反演原理
到 的关系矩阵与 到 的关系矩阵互为逆矩阵。
证明:
设 为 到 的关系矩阵, 为 到 的关系矩阵,则 ,,从而有 ,。由矩阵乘法满足结合律即有 。
二项式反演
内容
其中 可以为任何的大于 的常数,甚至可以是正无穷。
证明
令 ,。
:显然 到 和 到 的关系矩阵均为 (其中 )。现在只需要证明 即可。推式子可得如下:
上式中第 步可以用组合意义直接得出。
: 到 的关系矩阵 满足 , 到 的关系矩阵 满足 。推式子可得如下:
:因为 到 和 到 的关系矩阵均为 (其中 ),考虑把 转置得 。然后推式子基本与 的证明中的内容相似,只不过第六步为 。同样可以证明 ,所以 。
:因为 到 的关系矩阵 满足 , 到 的关系矩阵 满足 ;所以 ,。由 的证明过程可得 ,所以 。
用法
某些题目可能需要求 恰好 满足若干条件的元素数量,但是 钦定 满足若干条件的元素数量更为好求,则可以通过二项式反演求得 恰好 满足若干条件的元素数量。
例如:设恰好满足 个条件的元素个数为 ,钦定 满足 个条件的元素个数为 ,(条件总共有 个)则一般有:
使用二项式反演简化计算即可。
注意:钦定 满足若干条件的元素中可能有重复的情况,和 至少 不同。
同时由于 式中左式不带有形如 的系数,所以较 式更为常用。
例题1:错排问题
题意
求长为 的 的排列 的数量,需要满足 。
解法
设 为恰好有 个数满足 值 = 下标 的排列数, 为钦定有 个数满足 值 = 下标 的排列数;则显然有 且 。由二项式反演可得 。故而 。这样同时变相地证明了容斥原理的一个公式。
例题2:CF1342E Placing Rooks
题意
在 的国际象棋棋盘上放 个车,要求满足两个条件:
- 所有的空格子都能被至少一个车攻击到。
- 恰好有 对车可以互相攻击到。
答案对 取模。。
解法
显然必须要每一行或每一列需要有车放置,否则一定存在有格子不会被任何车攻击到。同时若存在 行不放车(),由于车的总数为 ,故而必须在每一列放恰好一个车,同时一定有 对车能相互攻击;存在 列不放车同理。下面只讨论存在不放车的行的情况。
设 为存在恰好 行不放车的方案数, 为钦定 行不放车的方案数;则 ,同时由于可以任选 行且 个车均有 种选择(可能存在有不放车的行),则 。故而 。
代码
点此查看代码
#include <bits/stdc++.h> using namespace std; #define ll long long const int maxn=200010; const int md=998244353; int n,i,b=1,t; int fac[maxn],inv[maxn]; ll k,ans; inline int Pow(int d,int z){ int ret=1; do{ if(z&1) ret=(1LL*ret*d)%md; d=(1LL*d*d)%md; }while(z>>=1); return ret; } inline int C(int x,int y){return ((1LL*fac[y]*inv[x])%md*inv[y-x])%md;} int main(){ fac[0]=1; for(i=1;i<maxn;++i) fac[i]=(1LL*fac[i-1]*i)%md; inv[maxn-1]=Pow(fac[maxn-1],md-2); for(i=maxn-1;i;--i) inv[i-1]=(1LL*inv[i]*i)%md; scanf("%d%lld",&n,&k); if(k>=n){ putchar('0'); return 0; } for(i=k;i<n;++i,b=-b) ans+=b*(((1LL*C(k,i)*C(i,n))%md)*Pow(n-i,n)%md); printf("%lld",((ans+ans*(!!k))%md+md)%md); return 0; }
例题3:P6478 [NOI Online #2 提高组] 游戏
题意
给定一棵 个节点的树,其中有 个白点, 个黑点。现在要将黑白两点两两配对,对于 ,求恰好有 对点存在祖先后代关系的方案数,对 取模。。
解法
设 为恰好有 对黑白点配对的方案数, 为钦定 对黑白点配对的方案数(也就是说能够找出 对黑白点配对的方案数)。显然 。
考虑 的求法,可以使用树形 dp。设 表示在 的子树中配对了 对有祖先后代关系的黑白点的方案数,则在求 时,可以顺次把 的每个儿子的 dp 值加入 ,转移有 。同时可以处理出 子树内(不包括 的)的黑点个数 ,白点个数 ;最后考虑 和哪些点配对时,若 为白点,则 。 为黑点同理。注意:最后有 ,未配对的点任意配对的方案需要算上。 时间复杂度显然为 。
代码
点此查看代码
#include <bits/stdc++.h> using namespace std; const int maxn=5010; const int md=998244353; int n,i,j,b,u,v,tot; int C[maxn][maxn],fac[maxn]; int h[maxn],ver[maxn<<1],nxt[maxn<<1]; int dp[maxn][maxn],cnt[maxn][2],tmp[maxn]; long long ans; bool col[maxn]; int dfs(const int p,const int f){ int lp,to,siz=1,ssi; dp[p][0]=1; for(lp=h[p];lp;lp=nxt[lp]){ to=ver[lp]; if(to==f) continue; ssi=dfs(to,p); for(i=0;i<=(siz>>1);++i) for(j=0;j<=(ssi>>1);++j) tmp[i+j]=(1LL*dp[p][i]*dp[to][j]+tmp[i+j])%md; siz+=ssi; cnt[p][0]+=cnt[to][0]; cnt[p][1]+=cnt[to][1]; for(i=0;i<=(siz>>1);++i){ dp[p][i]=tmp[i]%md; tmp[i]=0; } } ++cnt[p][col[p]];i=cnt[p][!col[p]];j=1; for(;i;--i) dp[p][i]=(1LL*dp[p][i-1]*(j++)+dp[p][i])%md; return siz; } int main(){ C[0][0]=fac[0]=1; for(i=1;i<maxn;++i){ C[0][i]=1; fac[i]=(1LL*fac[i-1]*i)%md; for(j=1;j<=i;++j){ C[j][i]=C[j][i-1]+C[j-1][i-1]; if(C[j][i]>=md) C[j][i]-=md; } } scanf("%d",&n);getchar(); for(i=1;i<=n;++i) col[i]=getchar()-'0'; for(i=1;i<n;++i){ scanf("%d%d",&u,&v); ver[++tot]=v;nxt[tot]=h[u];h[u]=tot; ver[++tot]=u;nxt[tot]=h[v];h[v]=tot; } dfs(1,0);n>>=1; for(i=0;i<=n;++i) dp[1][i]=(1LL*dp[1][i]*fac[n-i])%md; for(i=0;i<=n;++i){ b=1; for(j=i;j<=n;++j,b=-b) ans+=(1LL*b*dp[1][j]*C[i][j])%md; printf("%lld\n",(ans%md+md)%md); ans=0; } return 0; }
例题4
题意
给定一棵 个点的有标号无根树,你可以进行至多 次操作,每次操作是删除一条边再加上一条边,使得这个图还是一棵树。问能得到多少种本质不同的有标号无根树,答案对 取模。。
解法
听说 TopCoder13369 TreeDistance 和 CF917D Stranger Trees 这个题很像。
啊这个东西需要 矩阵树定理 吗
设 为删去 条边,重新连接 条边 且不能选择删去的边进行连接 的能得到的不同树的数量; 为 钦定 删去 条边再连接 条边能得到的不同树的数量。(此处的 钦定 为不考虑去重,例如删去任意 条边再连上同样的 条边的同种方案会被考虑 次,故而最终答案是 )此时 。
考虑删去 条边会形成 个连通块,连接 条边会将这 个连通块变成 个;故而如果定好了删边和连边的方案,则具体到操作上可以先删除一条边,再把连接这条边的两个端点最后所在的连通块的边连上;保证了每次操作合法,即可以把删边和连边的操作分开进行。
考虑把这 个连通块连接成的树有多少种。
引入:Prüfer 序列
定义
对于一棵大小为 的有标号无根树(),可以进行若干次操作,每次操作可以把标号最小的叶子节点删除,同时在一个序列末端加入 这个点的父亲,直到最后树上只剩两个点。我们称形成的这个长为 的序列为这棵树的 Prüfer 序列。
性质
显然最后整个序列值域为 且一棵树对应着唯一一个 Prüfer 序列。
Cayley 公式: 个点的有标号无根树总共有 种。( 个点的有标号完全图有 棵生成树)
证明:考虑如何从 Prüfer 序列反推出这棵树。
摘自 OI-wiki:据 Prüfer 序列的性质,我们可以得到原树上每个点的度数。然后你也可以得到度数最小的叶结点编号,而这个结点一定与 Prüfer 序列的第一个数连接。然后我们同时删掉这两个结点的度数。每次我们选择一个度数为 的最小的结点编号,与当前枚举到的 Prüfer 序列的点连接,然后同时减掉两个点的度。到最后我们剩下两个度数为 的点,其中一个是结点 。把它们连接起来,则可以构造这棵唯一的树。
故而任意一个 Prüfer 序列均有对应的唯一的树, 个点的有标号无根树总共有 种。
另外一个结论:有 棵有标号无根树,第 棵树大小为 ,现在要把这 棵树用 条边连接起来,得到的本质不同的有标号无根树数量为 。
证明:模拟 Prüfer 序列的生成过程。我们可以把每个连通块的“权值”看成是其中最小的点的编号;然后在最后生成的树中每次删除“权值”最小的只与另外一个连通块连接的连通块 ,把与其连接的连通块的连向 的点加入一个序列中。可以发现序列的值域为 ,同时由 Prüfer 序列的构造树的方法可以构造一棵唯一的树;并且由于它没有维护每个连通块在成为叶子节点时向外连边的点(我们称其为连通块的代表元素),故而本质不同的有标号无根树数量为 。
故而最后 即为把树划分的所有连通块对应的 的和。考虑如何维护 。
可以使用树形 dp。设 表示在 子树内划分了 个连通块(不包括 所在的未统计完的连通块),且 所在的连通块的大小为 的对应 。转移统计方式可以参考容斥原理部分的 ARC101E Ribbons on Tree 部分题解。同时时间复杂度还是 的。
考虑把 数组的一维进行压缩。由于最后计算 需要树上的连通块个数,所以只能压缩 所在一维。有一种不太容易想到的 dp 方式:设 为在 子树上,划分了 个连通块(包括 所在的未统计完的连通块),且 表示是否在 所在的连通块中选择了上述的代表元素;则转移为 (合并 和 所在连通块,注意两者所在的连通块不能同时有代表元素。可以用组合意义理解,也就是 等效于在每个连通块中选一个点的方案数),(不合并 和 所在的连通块,注意 所在的连通块必须要统计完成)。初值有 。时间复杂度显然为 。
(11/23:终于把上面的一些错漏之处改正了,参考了 这篇题解)
代码
点此查看代码
#include <bits/stdc++.h> using namespace std; const int maxn=5010; const int md=1000000007; int n,i,j,u,v,t; int h[maxn],g[maxn],pw[maxn],nxt[maxn]; int f[maxn][2],dp[maxn][maxn][2],C[maxn][maxn]; inline void Add(int &x,int y){x-=((x+=y)>=md)*md;} int dfs(int p){ dp[p][1][0]=dp[p][1][1]=1; int to,sz,rt=1; for(to=h[p];to;to=nxt[to]){ sz=dfs(to); for(i=1;i<=rt;++i){ for(j=1;j<=sz;++j){ Add(f[i+j][0],(1LL*dp[p][i][0]*dp[to][j][1])%md); Add(f[i+j][1],(1LL*dp[p][i][1]*dp[to][j][1])%md); Add(f[i+j-1][0],(1LL*dp[p][i][0]*dp[to][j][0])%md); Add(f[i+j-1][1],(1LL*dp[p][i][0]*dp[to][j][1]+ 1LL*dp[p][i][1]*dp[to][j][0])%md); } } rt+=sz; memcpy(dp[p],f,sizeof(f)); memset(f,0,sizeof(f)); } return rt; } class TreeDistance{ public: int countTrees(vector<int> p,int k){ n=p.size()+1; k=n-1-min(k,n-1); for(i=2;i<=n;++i){ u=p[i-2]+1; nxt[i]=h[u]; h[u]=i; } pw[0]=1; for(i=0;i<=n;++i) C[0][i]=1; for(i=1;i<=n;++i){ C[0][i]=1; pw[i]=(1LL*pw[i-1]*n)%md; for(j=i;j<=n;++j) Add(C[i][j],C[i][j-1]+C[i-1][j-1]); } dfs(1); for(i=0;i<n-1;++i) g[i]=(1LL*dp[1][n-i][1]*pw[n-i-2])%md; g[n-1]=1; u=0; for(i=n-1;i>=k;--i){ for(j=i,v=0;j<n;++j){ t=(1LL*C[i][j]*g[j])%md; if(v) t=md-t; Add(u,t); v=!v; } } return u; } };
练习题:
DarkBzoj 2839 集合计数
考虑把交集之外的元素进行考虑,可以看作交集之外的 个元素的分配方法分配给 个集合(),方法显然为 (分配方法包括不分配任何元素)。二项式反演即可。
注意: 部分需要对 取模()。(这个部分卡了我很久)
P4859 已经没有什么好害怕的了
升序排序,模拟匹配过程。发现可以用双指针 + dp 得出钦定有 个糖果的能量大于药片的方案数,二项式反演即可。
P5505 分特产
考虑容斥。在至少有 个同学分不到特产的情况下,分开讨论每种特产的分配方案(即把无标号球放进有标号盒子且能有空盒的方案数)。
CF1228E Another Filling the Grid
讨论如何放 的情况比较困难(至少我只知道 的方法),可以讨论如何放 的元素。然后容斥即可,做法可参考 CF997C。
本文来自博客园,作者:Fran-Cen,转载请注明原文链接:https://www.cnblogs.com/Fran-CENSORED-Cwoi/p/16753845.html
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!