容斥原理学习笔记
定义
一般指将统计问题分解为几个子问题,然后将几个问题的结果分别乘以一个容斥系数求和
注意容斥系数不一定是维恩图的$(-1)^k$之类的,可能包含组合数
经典容斥
给定或是可以求出:分别满足一个属性的数量,分别满足两个属性的数量,分别满足两个属性的数量......
要求恰好元素的总数之类的
这种比较简单,直接$O(2^n)$小学容斥公式解决即可。
但有些题的属性不明显,需要经过适当转化
例题:【BZOJ3589】动态树
组合容斥
共有n个元素
给定或是可以求出:f(k)=【钦定有k个元素满足,其他元素随意】的数量。
要求出g(k)=【恰好有k个元素满足】的数量
请注意g(k)并不是f()的后缀和。
设该问题的容斥系数为h(x),则
$g(k)=\sum_{i=0}^n h(i)*f(i)$
考虑列方程求解h(x)
我们希望恰好有题目给定的a个元素满足的情况被统计的次数恰好为1,即我们要让$T(x)=[x=a]$
考虑$g(k)=\sum_{i=0}^n h(i)*f(i)$中,
【恰好有k个元素满足的情况】统计次数为$T(k)=\sum_{i=0}^k h(i)*(^k_i)$
意思是在钦定i个元素满足,但实际恰好有k个元素满足的情况中,钦定的元素可能是这k中的任意一个,所以要先选出k个中是哪i个被钦定了,并且不管钦定的是谁,方案数都不会变
题外话:请注意恰好一个的方案数不等于至少一个减去至少两个的方案数(至少k个相当于先在n中选k个,钦定这k个满足,然后其他乱选)
假设有一个方案满足1,2,那么它在至少一个的方案数中会被统计两次(分别是钦定1和钦定2满足),然而在2中只会被统计一次,如果要恰好满足更多个情况就更复杂了。所以要组合容斥解决。
现在接这个方程:$T(k)=\sum_{i=0}^k h(i)*(^k_i)$
二项式反演:$h(k)=\sum_{i=0}^k(-1)^{k-i}*(^k_i)*T(i)$
即:$h(k)=\sum_{i=0}^k(-1)^{k-i}*(^k_i)*[i=a]$
即若k>=a,则$h(k)=(-1)^{k-a}*(^k_a)$,否则等于0
那么$g(k)=\sum_{i=k}^n (-1)^{i-k}*(^i_k)*f(i)$
先处理出f(x),然后处理g(x)即可,$O(n)$
其实这本质就是设未知函数,解函数方程的思想
有些题目比较奇怪,可能不会像$T(x)=[x=a]$这么简单,可能是要求【恰好有a的倍数个元素满足】的数量之类的毒瘤玩意。这样就把$T(x)=[x=a]$改为$T(x)=[a|x]$,然后用单位根反演即可,比如【loj前夕】
例题代码
#include<bits/stdc++.h> using namespace std; #define mod 998244353 #define int long long #define N 10000010 int fac[N],ifac[N],g[N]; int c(int n,int m) { return fac[n]*ifac[m]%mod*ifac[n-m]%mod; } int qpow(int aa,int bb,int pp) { int res=1; aa%=pp; while(bb) { if(bb&1) res*=aa,res%=pp; aa*=aa,aa%=pp; bb>>=1ll; } return res; } signed main() { int n; cin>>n; int w1=qpow(3,(mod-1)/4,mod),inv4=qpow(4,mod-2,mod); fac[0]=ifac[0]=1; for(int i=1;i<=n;i++) fac[i]=fac[i-1]*i%mod; ifac[n]=qpow(fac[n],mod-2,mod); for(int i=n-1;i;i--) ifac[i]=ifac[i+1]*(i+1)%mod; int w[4]={0,w1-1,(w1*w1-1)%mod,(w1*w1%mod*w1-1)%mod},wn[4]={1,1,1,1}; int ans=0; g[n]=2; for(int i=n-1;i>=0;i--) g[i]=g[i+1]*g[i+1]%mod; for(int i=0;i<=n;i++) { int f=(wn[0]+wn[1]+wn[2]+wn[3])%mod*inv4%mod; ans+=f*c(n,i)%mod*(g[i]-1/*使集合除去交集后非空,保证技能种类不同*/)%mod; ans%=mod; for(int j=0;j<4;j++) wn[j]=wn[j]*w[j]%mod; } cout<<ans+1/*可以不放任何技能*/; }
另外有些题目至少的情况不好算,可以拿计算至多的情况f(x)来算,恰好k个的方案数就大于等于k的x的f(x)组合容斥一下。
有些题目有多个属性,每个属性满足的数量都要恰好等于某数,这种的话就容斥套容斥。
即:设题目要求满足要求1的数量为a,满足要求2的数量为2.设f(x,y)表示在至少/至多要x个满足要求1,至少/至多y个满足要求2。
利用这个容斥出g(x,y)表示至少/至多x个满足要求1,恰好y个满足要求2。
然后再利用这个容斥出h(x,y)表示恰好x个满足要求1,恰好y个满足要求2。
然后输出要求的h(x,y)
在实现时一般把这些式子合并成一条式子,一次性求解。
例题:[JSOI2015]染色问题
#include<bits/stdc++.h> using namespace std; #define mod 1000000007 #define int long long #define N 410 int fac[N],ifac[N],g[N],kpow[N*N]; int qpow(int aa,int bb) { int res=1; int pp=mod; aa%=pp; while(bb) { if(bb&1) res*=aa,res%=pp; aa*=aa,aa%=pp; bb>>=1ll; } return res; } int C(int a,int b) { if(a==0||b==0)return 1; return fac[a]*ifac[b]%mod*ifac[a-b]%mod; } signed main() { int n,m,c; cin>>n>>m>>c; fac[0]=ifac[0]=1; int t=max(n,max(m,c)); for(int i=1;i<=t;i++) fac[i]=fac[i-1]*i%mod; ifac[t]=qpow(fac[t],mod-2); for(int i=t-1;i;i--) ifac[i]=ifac[i+1]*(i+1)%mod; for(int k=0;k<=c;k++) { kpow[0]=1; for(int i=1;i<=n*m;i++) kpow[i]=kpow[i-1]*(k+1)%mod; for(int i=0;i<=n;i++) { for(int j=0;j<=m;j++) { int tag=1; if((i+j)&1) tag=-1; g[k]+=1ll*tag*C(n,i)*C(m,j)%mod*kpow[(n-i)*(m-j)]%mod+mod; g[k]%=mod; } } } int ans=0; for(int i=c,tag=1;i>=0;i--,tag=-tag) { ans=(ans+(tag*g[i]*C(c,i)%mod+mod)%mod)%mod; } cout<<ans; }