容斥原理
一、简介
我们先看一个小问题:
已知站桐亚的有(a)人,站桐乃的有(b)人,两个都站的有(c)人,问至少站桐亚或者桐乃其中一个的有多少个人?
答案是显然的:(a+b-c),我们可以通过(Venn)图清晰地看出答案:
设站桐亚的集合为(S_1),站桐乃的集合为(S_2),于是我们有:
那如果不止两个集合呢?
定理1:对于集合(S_1,S_2,S_3...S_n),它们的并集的元素个数是:
证明:
我们可以考虑一个属于(igcup^{n}_{i=1})的元素(x)。
令(x)所属的(m)个集合为(T_1,T_2, ...T_m),其中(T_i)是集合(S_1,S_2,...S_n)中的任意一个
我们可以通过上列等式右边的式子得到(x)的出现次数(cnt)为:
上试结合二项式定理不难理解
于是每个元素在右式中都计算且只计算了1次,所以公式得证
(容斥原理也可以同样用数学归纳法严格证明,这里不再赘述)
根据定理1,我们就可以把求并集元素个数转化成求交集元素个数
我们令(ar{S_i})表示(S_i)关于全集(U)的补集,则我们有:
定理2:
这个定理结合(Venn)图很容易理解,此处不再证明。
根据定理2,我们就可以把求交集元素个数转化成求并集元素个数
二、容斥原理在一些数学问题上的应用
1.已知不定方程(x_1+x_2+x_3+...+x_n=m)和(n)个限制条件(x_i leq b_i),求该不定方程的非负数解的数目
我们先不考虑限制条件。那么方程解的数目即为(C_{n+m-1}^{m-1}) (插板法)
在应用容斥原理前,我们先确定全集(U)以及(U)中每个元素的性质(P_i)。
于是我们得到:
1.全集(U)为满足该方程组所有非负整数解
2.对于每一个(x_i),都有个性质(P_i),即(x_ileq b_i)
设(S_i)为满足性质(P_i)的集合,那我们的最终答案就是(|igcap _{i=1}^nS_i |)。
由定理2得:(|igcap _{i=1} ^nS_i |= |U|-|igcup _{i=1} ^ n ar{S_i}|)
显然,(|U|)即为(C_{n+m-1}^{m-1}) ,我们只要计算后半部分即可,而后半部分回归了容斥原理得一般形式,即后半部分可以用定理1展开!
观察定理1等式的右半边,我们只需要考虑以下问题:
给出(1leq i_1<i_2<i_3 <...<i_t leq n),求(igcap_{k=1}^t ar{S_{i_k}}) 的值
现在我们来考虑(ar{S_{i_k}})的含义。
集合(ar{S_{i_k}})表示所有满足(x_{i_k}ge b_{i_k}+1)的解,这说明,有部分变量是有下界限制的
我们现在尽可能的去掉这个限制
于是我们可以将(m)减去(sum_{k=1}^{t} b_{i_k}+1)
显然,新的方程的解与我们要求的解是一一对应的。此时,新方程每个变量都没有上下界限制
于是,我们可以对集合(ar{S_1},ar{S_2},ar{S_3}...ar{S_n})按照定理1进行容斥原理的计算
2.(错排问题)求对于序列(a=left{1,2,3...n ight})的所有排列中,满足(a_i ot= i)的排列的个数
像上一道题目一样,我们仍然考虑全集(U)以及性质(P)
1.全集(U)为(a)的所有排列
2.性质(P_i)为(a_i ot= i)
设(S_i)为满足性质(P_i)的集合,那我们的最终答案就是(|igcap _{i=1}^nS_i |)。
由定理2得:(|igcap _{i=1} ^nS_i |= |U|-|igcup _{i=1} ^ n ar{S_i}|)
(和上道题一模一样)
显然,(|U|=A_n^n =n!)
后半部分一样可以用定理1展开。
同样,我们考虑(igcap_{i=1}^{t} ar{S_{i_k}})。
对于每一个(ar{S_{i_k}}),实际上都有一个位置被确定了,而剩下的位置我们可以随便乱排
于是(igcap_{i=1}^{t} ar{S_{i_k}}=(n-t)!)
对于每一个满足(1leq tleq n)的(t),我们所枚举的(ar{S_{i_1}},ar{S_{i_2}},ar{S_{i_3}},...ar{S_{i_t}})对答案的贡献是一样的。
于是答案即为:
当然,错排问题还有递推的解法,这里不再赘述
3.欧拉函数与莫比乌斯函数
欧拉函数(varphi(n))表示(1)到(n)之间与(n)互质数的个数
两个数互质即代表它们的最大公因数为(1)
进一步说,任何一个数(N)可以被分解成:(唯一分解定理)
其中(p_1,p_2,p_3,...,p_n)为质数
也就是说,与(n)互质的数,必然没有(n)经过唯一分解之后的质因数
这是,容斥原理出场了:
全集(U)表示(1)到 (n)之间的正整数,性质(P_i)表示该数不含有质因数(p_i)
设(S_i)为满足性质(P_i)的集合,那我们的最终答案就是(|igcap _{i=1}^nS_i |)。
根据定理2,我们可以得到:(其中t表示唯一分解之后n的质因数个数)
在我们上述求欧拉函数的过程中,我们讨论的是一个关于质数的集合。
当我们取遍这个集合的子集的时候,得到的质数的乘积把它唯一分解之后,每个质因数的次数都是1
(由于(n)可以唯一分解为(p_1^{a1} imes p_2^{a_2} imes p_3^{a_3} imes ... imes p_t^{a_t}),所以事实上(n)除以一些质数的成绩也是另外一些质数的积)
我们称这样的数为无平方因子数。
观察上式,我们发现仅有(1)和无平方因子数对答案有贡献。
而且,对于一个无平方因子数,它对答案的贡献取决于它的质因子的个数
我们定义函数(mu(n))为该数对答案的贡献,于是我们可以得到:
事实上,这就是著名的莫比乌斯函数
有了莫比乌斯函数,上述欧拉函数的计算公式就可以写成:
有了莫比乌斯函数,我们就可以在某些数论的计数题中,通过观察约数对答案的贡献,利用莫比乌斯函数进行容斥
关于莫比乌斯函数,以下还有几点想说的:
1.莫比乌斯函数是积性函数,可以用线性筛求解
2.提到莫比乌斯函数,就不得不提莫比乌斯反演:
若有
则有
莫比乌斯反演不是我们讨论的重点,有兴趣的可以自行了解更多。
4.概率论
对于概率空间内的事件(A_1,A_2,A_3,...,A_n),我们有:
若事件的概率只与事件的数量有关,设(i)个事件交集的概率为(a_i),则:
三、容斥原理在一些信息学竞赛的题目里的应用
1.[HAOI2008]硬币购物
现在有四种面值的硬币(c_1,c_2,c_3,c_4),每一种硬币有(d_i)个, 现在有(n)次询问,每次询问能用多少种方法来付(s)元?
数据范围:(nleq10^3, sleq10^5)
这道题看似是一个背包,但单次查询最好情况下是(O(4s))的,无法接受多组询问
观察到题目里只有四种面值,我们于是很容易想到一个不定方程:
(c_1x_1+c_2x_2+c_3x_3+c_4x_4=s)
我们一样的考虑全集(U)和性质(P):
1.全集(U)为不定方程的所有非负整数解
2.(P_i)为(x_ileq d_i)
设(S_i)为满足性质(P_i)的集合,那我们的最终答案就是(|igcap _{i=1}^nS_i |)。
由定理2得:(|igcap _{i=1} ^nS_i |= |U|-|igcup _{i=1} ^ n ar{S_i}|)
(这两句话又出现了)
显然,(ar{S_i}) 为满足(x_ige d_i+1)的所有不定方程的解
我们仍然可以减去下界和,于是这道题就变成了一个没有限制的无限背包问题,我们可以进行预处理
设最大的(s)为(m),则总的时间复杂度为(O(4m+n imes2^4))
Code:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=1e5+5;
const int max_state=1<<4;
int c[5];
int d[5];
ll dp[maxn];
int q,s;
int main(){
for(int i=1;i<=4;i++) scanf("%d",&c[i]);
scanf("%d",&q);
dp[0]=1;
for(int i=1;i<=4;i++)//完全背包预处理
for(int j=c[i];j<=100000;j++)
dp[j]+=dp[j-c[i]];
while(q--){
for(int i=1;i<=4;i++) scanf("%d",&d[i]);
scanf("%d",&s);
ll ans=dp[s];//初始值|U|
for(int i=1;i<max_state;i++){//枚举每一个集合
int tmp=i;
int res=s;
bool flag=true;
int cnt=0;
while(tmp){
cnt++;
if(tmp&1){
flag^=1;
res-=(d[cnt]+1)*c[cnt];//减掉下界
}
tmp>>=1;
}
if(res>=0) ans=flag?ans+dp[res]:ans-dp[res];//容斥
}
printf("%lld
",ans);
}
return 0;
}
2.P5339 [TJOI2019]唱、跳、rap和篮球
现在有四类人:一部分最喜欢唱、一部分最喜欢跳、一部分最喜欢rap,还有一部分最喜欢篮球。如果队列中(k),(k + 1),(k + 2),(k + 3)位置上的同学依次,最喜欢唱、最喜欢跳、最喜欢rap、最喜欢篮球,那么他们就会聚在一起讨论蔡徐坤。现在我们不希望它们讨论蔡徐坤,问一共有多少种方案?
本题有人数限制,我们先把它放一边
很明显,这样我们可以做容斥。
我们可以计算至少有0组讨论cxk,至少有一组讨论cxk......
那么根据定理2,最后的答案就是:
(num[0组讨论]-num[1组讨论]+num[2组讨论]+...+(-1)^nnum[全部讨论])
然后我们把讨论cxk的插入进其他人中间。
设有(t)组讨论cxk,还剩(r)个人,那么方案数就是(C_{r+t}^t)
现在我们考虑人数限制。
最粗暴的方法就是大力枚举有多少个人分别唱,跳,rap,打篮球,再加上容斥,(O(n^4)),超时
因为只有4种,我们可以巧一点。
我们可以枚举有(m)个唱或跳,那么答案就是:
我们惊讶的发现,(sum_{i=m-b}^{a}C_{m}^{i}) 和(sum_{i=n-m-d}^{c}C_{n-m}^{i}) 可以用前缀和计算
于是我们只需要枚举(m)了
Code:
#include<bits/stdc++.h>
#define MOD 998244353
using namespace std;
typedef long long ll;
const int maxn=1e3+5;
int n,a,b,c,d;
ll C[maxn][maxn];
ll sum[maxn][maxn];
ll ans;
ll res;
inline ll query(int t,int l,int r){
if(l>r) return 0;
if(l<=0) return sum[t][r];
ll tmp=sum[t][r]-sum[t][l-1];
if(tmp<0) res+=MOD;
return tmp;
}
int main(){
scanf("%d%d%d%d%d",&n,&a,&b,&c,&d);
C[0][0]=1; C[1][0]=C[1][1]=1;
for(int i=2;i<=n;i++){
C[i][0]=1;
for(int j=1;j<=i;j++)
C[i][j]=(C[i-1][j]+C[i-1][j-1])%MOD;
}
sum[0][0]=1;
for(int i=0;i<=n;i++){
C[i][0]=1;
for(int j=1;j<=i;j++){
C[i][j]=C[i-1][j-1]+C[i-1][j];
if(C[i][j]>=MOD) C[i][j]-=MOD;
}
}
for(int i=0;i<=n;i++){
sum[i][0]=1;
for(int j=1;j<=n;j++){
sum[i][j]=sum[i][j-1]+C[i][j];
if(sum[i][j]>=MOD) sum[i][j]-=MOD;
}
}
a=min(a,n);
b=min(b,n);
c=min(c,n);
d=min(d,n);
int cnt=0;
while(n>=0&&a>=0&&b>=0&&c>=0&&d>=0){
res=0;
for(int i=0;i<=n;i++){
(res+=C[n][i]*query(i,i-b,a)%MOD*query(n-i,n-i-d,c)%MOD)%=MOD;
// cout<<C[n][i]<<' '<<query(i,i-b,a)<<' '<<query(n-i,n-i-d,c)<<endl;
}
(res*=C[n+cnt][cnt])%=MOD;
if(cnt&1) (ans-=res)%=MOD;
else (ans+=res)%=MOD;
n-=4; a--; b--; c--; d--; cnt++;
if(ans<0) ans+=MOD,ans%=MOD;
//cout<<ans<<endl;
}
printf("%lld
",ans);
return 0;
}
3.[ZJOI2016]小星星
现在给你一颗树和一个图,将树上的点重新编号,使得树上的一条边((u,v))在图上也有一条相应的边((idx_u,idx_v))((idx_i)表示树上一点(i)重新编号后的结果),问有多少种编号的方法?
((nleq 17 , mleq n(n-1)/2))
显然,本题是一道动态规划题,而且是一道树形dp。
按照套路,一般树形dp的第一位肯定这个节点的编号
我们慢慢来分析这个问题:
影响答案的首先是对树上一个点的重新编号,很容易想到枚举一个点重新编排后的编号
第二,树上的一条边能否对应图上的一条边是能否对答案产生贡献的必要条件
所以对于树上一点,我们可以枚举与他相邻的节点的重新编排后的编号
最后,重新编排的编号不能有重复
于是不难想到一种dp:
令(dp[i][j][S]) 表示树上一点(i),重新编号后对应的是(j),(i)的子树集合为(S)。
因为我们要枚举一个数子树的子集,所以复杂度肯定是特别高的。
等一下,为什么我们好端端的枚举起集合来了?
别忘了,我们要避免重复,即我们希望得到一个1-n的排列
那如果我们不考虑这个条件呢?
这样dp只有二维,但是有可能会重复
重复了怎么办?容斥原理
我们强制树上每个点重新编排后的编号为(S=left{1,2,3,...,n ight})的一个子集,每次我们做一个(O(n^3))的dp,然后枚举它的子集。
那么答案即为:
(num[|S|=n]-num[|S|=n-1]+num[|S|=n-2]+...+(-1)^nnum[|S|=1])
其中(num)表示中括号内值为真时的方案数
Code:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=1005;
int n,m;
int g[maxn][maxn];
struct Node{
int to;
int next;
}edge[maxn<<1];
int head[maxn],cnt;
int maxstate;
int tot;
int idx[maxn];
ll dp[maxn][maxn];
ll ans;
inline void add(int x,int y){
edge[++cnt].next=head[x];
edge[cnt].to=y;
head[x]=cnt;
}
inline void dfs(int x,int fa){
//cout<<x<<' '<<fa<<endl;
for(int i=head[x];i!=0;i=edge[i].next){
int k=edge[i].to;
if(k==fa) continue;
dfs(k,x);
}
for(int i=1;i<=tot;i++){
dp[x][idx[i]]=1;
for(int j=head[x];j!=0;j=edge[j].next){
int k=edge[j].to;
if(k==fa) continue;
ll sum=0;
for(int l=1;l<=tot;l++)
if(g[idx[i]][idx[l]]) sum+=dp[k][idx[l]];
//cout<<sum<<endl;
dp[x][idx[i]]*=sum;
}
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
int x,y;
scanf("%d%d",&x,&y);
g[x][y]=g[y][x]=1;
}
for(int i=1;i<n;i++){
int x,y;
scanf("%d%d",&x,&y);
add(x,y);
add(y,x);
}
maxstate=1<<n;
for(int i=0;i<maxstate;i++){
int tmp=i;
int num=0;
tot=0;
while(tmp){
num++;
if(tmp&1) idx[++tot]=num;
tmp>>=1;
}
dfs(1,0);
for(int i=1;i<=tot;i++)
if((n-tot)&1) ans-=dp[1][idx[i]];
else ans+=dp[1][idx[i]];
}
printf("%lld
",ans);
return 0;
}
参考文献:
1.2013国家集训队论文 王迪《浅谈容斥原理》
2.李煜东《算法竞赛进阶指南》
3.《奥数教程》高中第三分册
如有不足敬请指正,谢谢!