Loading

浅谈容斥原理

套路

恰好和至多的转换

如果要求某些东西恰好有 \(k\) 个的时候,有时候会很难算,而求至多有 \(k\) 个的时候会很好算。

\(f_i\) 表示至多有 \(k\) 个的方案数,\(g_i\) 表示恰好有 \(k\) 个的方案数,则有

\[f_k=\sum\limits_{i=0}^k \binom{k}{i}g_i \]

根据二项式反演有

\[g_k=\sum\limits_{i=0}^k (-1)^{k-i} \binom{k}{i}f_i \]

然后 \(f_i\) 一般在很短的时间内就可以方便求出,再用 \(f\)\(g\) 就可以得到答案。

恰好和至少的转换

求至少 \(k\) 个更方便的时候。

\(f_i\) 表示至少有 \(k\) 个的方案数,\(g_i\) 表示恰好有 \(k\) 个的方案数,则有

\[f_k=\sum\limits_{i=k}^n \binom{i}{k}g_i \]

根据二项式反演有

\[g_k=\sum\limits_{i=k}^n(-1)^{i-k}\binom{i}{k}f_i \]

一些题

ABC266 G

入门题,考虑设 \(f(k)\) 为至少有 \(k\)RG 段,那么就是 \(k\)RG\(R-k\)R\(G-k\)G\(B\)B 做可重集排列,所以 \(f(k)=\frac{(k+R-k+G-k+B)!}{k!(R-k)!(G-k)!B!}\)。我们发现如果我们钦定一个 RG 段在某些位置的时候,之前可能会出现单个 RG 组成 RG 且也在这个位置的 一模一样的 串,如恰好要有两个 RG,但是我们至少一次时却算了两次(第一个位置枚举了一次,第二个位置也枚举了一次),所以需要去除重复算的。

\(g(k)\) 为恰好有 \(k\)RG,那么有 \(f(k)=\sum\limits_{i=k}^{\min(r,g)} \binom{i}{k} g(i)\),因为我们每一次都会从中枚举出 \(k\) 个不同的位置作为必选的“至少”部分,剩下的 \(i-k\)RG 位置则会和下一次选出的 \(k\) 个位置至少重复一个,这与每一种 \(k\) 的枚举状态一一对应,所以重复方案数就是 \(\binom{i}{k}\)

最后对其二项式反演,得 \(Ans=\sum\limits_{i=k}^{\min(r,g)} (-1)^{i-k} \binom{i}{k} f(i)\)

Code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int _=4e6+5,mod=998244353;
int r,g,b,k,fac[_],inv[_],ans;
int f(int x){
    return fac[x+r-x+g-x+b]*inv[x]%mod*inv[r-x]%mod*inv[g-x]%mod*inv[b]%mod;
}
int C(int x,int y){
    return fac[x]*inv[y]%mod*inv[x-y]%mod;
}
signed main(){
    cin>>r>>g>>b>>k;
    fac[0]=inv[0]=inv[1]=1;
    for(int i=1;i<=r+g+b;++i) fac[i]=fac[i-1]*i%mod;
    for(int i=2;i<=r+g+b;++i) inv[i]=inv[mod%i]*(mod-mod/i)%mod;
    for(int i=2;i<=r+g+b;++i) inv[i]=inv[i-1]*inv[i]%mod;
    for(int i=k;i<=min(r,g);++i){
        ans=(ans+((i-k)&1?mod-1:1)*f(i)%mod*C(i,k)%mod)%mod;
    }
    cout<<ans;
    return 0;
}

LuoguP1450 [HAOI2008]硬币购物

如果没有硬币数的限制,就是做一个完全背包,相当于求出了总方案数,记为 \(f\) 数组。对于这个硬币超过限制时,我们就强制选 \(d+1\) 个这种硬币,用了 \((d+1)*w\) 的容量,那么对于这个硬币的不合法的方案数为 \(f_{i-(d+1)*w}\),减去即可。但是可能会多减,比如钦定一枚硬币超过限制时第一枚硬币不合法的方案会减一次,钦定两枚硬币超过限制时第一枚硬币不合法的方案数又会减,这样会重复减,所以我们要加减加减,于是我们容斥一下。

#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read(){
	int ans=0,f=0;char ch=getchar();
	while(!isdigit(ch)) f|=(ch=='-'),ch=getchar();
	while(isdigit(ch)) ans=(ans<<3)+(ans<<1)+(ch^48),ch=getchar();
	return f?-ans:ans;
}
const int N=1005,INF=1e9,M=1e5+5;
int tot,s,now,ans;
int c[N],d[N];
int f[M];
int one[16]={0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4};
signed main(){
	f[0]=1;
	for(int i=1;i<=4;++i){
		c[i]=read();
		for(int j=c[i];j<M;++j) f[j]+=f[j-c[i]];
	}
	for(int tot=read();tot;--tot){
		for(int i=1;i<=4;++i) d[i]=read();
		s=read();
		ans=f[s];
		for(int i=1;i<16;++i){
			now=s;
			for(int j=0;(1<<j)<=i;++j)
				if((i>>j)&1) now-=(d[j+1]+1)*c[j+1];
			if(now>=0) one[i]&1?ans-=f[now]:ans+=f[now];
		}
		printf("%lld\n",ans);
	}
	return 0;
}

LuoguP6076 [JSOI2015]染色问题

这题有三个限制条件,显然一次容斥已经无法胜任了,所以我们考虑多次容斥来求解。

我们先设 \(f_i\) 表示棋盘上至多出现了 \(i\) 种颜色,则有

\[Ans=\sum\limits_{i=0}^C(-1)^{c-i}\binom{n}{i}f_i \]

对每一行考虑,我们钦定这一行有颜色,当前至多有 \(i\) 种颜色,至多有 \(j\) 列有颜色,方案数为\((i+1)^j-1\)。(有 \(i\) 种颜色和不选,每种情况都可以用在 \(j\) 列中的任意一列,但是不能一列都没有颜色,所以减去 \(1\)

我们钦定每一行都有颜色,当前至多有 \(i\) 种颜色,再设 \(g_j\) 表示至多有 \(j\) 列有颜色,则有

\[g_j=((i+1)^j-1)^n \]

容斥一下,则有

\[f_i=\sum\limits_{j=1}^m(-1)^{m-j}\binom{m}{j}g_j \]

就做完了。

#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read(){
	int ans=0,f=0;char ch=getchar();
	while(!isdigit(ch)) f|=(ch=='-'),ch=getchar();
	while(isdigit(ch)) ans=(ans<<3)+(ans<<1)+(ch^48),ch=getchar();
	return f?-ans:ans;
}
const int N=405,INF=1e9,mod=1e9+7;
int n,m,c;
int f[N];
int fac[N],inv[N];
int C(int x,int y){
	if(y>x) return 0;
	return fac[x]*inv[y]%mod*inv[x-y]%mod;
}
int qpow(int x,int y){
	int res=1;
	while(y){
		if(y&1) res=(res*x)%mod;
		x=(x*x)%mod;
		y>>=1;
	}
	return res;
}		
signed main(){
	n=read(),m=read(),c=read();
	inv[0]=inv[1]=fac[0]=fac[1]=1;
	for(int i=2;i<=400;++i) fac[i]=fac[i-1]*i%mod,inv[i]=inv[mod%i]*(mod-mod/i)%mod;
	for(int i=2;i<=400;++i) inv[i]=inv[i-1]*inv[i]%mod;
	for(int i=1;i<=c;++i){
		int tmp=0;
		for(int j=m,opt=1;j>=1;--j,opt=mod-opt)
			tmp=(tmp+C(m,j)*qpow((qpow(i+1,j)-1+mod)%mod,n)%mod*opt%mod)%mod;
		f[i]=tmp;
	}
	int ans=0;
	for(int i=c,opt=1;i>=1;--i,opt=mod-opt) 
		ans=(ans+f[i]*C(c,i)%mod*opt%mod)%mod;
	printf("%lld",ans);
	return 0;
}

LuoguP5505 [JSOI2011]分特产

\(f_i\) 表示至少有 \(i\) 个人没分到特产的方案数,\(a_j\) 为第 \(j\) 个特产的数量,则有

\[f_i=\binom{n}{i}\prod\limits_{j=1}^m\binom{a_j+n-i-1}{n-i-1} \]

连乘这一坨怎么理解?我们设有 \(k=n-i\) 个人有特产,那么就是有 \(k\) 人,每个人可以无限拿特产,问拿出 \(a_j\) 个特产的方案数,根据可重集组合即可。

所以

\[Ans=\sum\limits_{i=0}^n(-1)^if_i \]

#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read(){
	int ans=0,f=0;char ch=getchar();
	while(!isdigit(ch)) f|=(ch=='-'),ch=getchar();
	while(isdigit(ch)) ans=(ans<<3)+(ans<<1)+(ch^48),ch=getchar();
	return f?-ans:ans;
}
const int N=5005,mod=1e9+7;
int n,m,ans;
int a[N];
int fac[N],inv[N];
int C(int x,int y){
	if(y>x) return 0;
	return fac[x]*inv[y]%mod*inv[x-y]%mod;
}
signed main(){
	fac[0]=fac[1]=inv[0]=inv[1]=1;
	for(int i=2;i<=4000;++i) fac[i]=fac[i-1]*i%mod,inv[i]=inv[mod%i]*(mod-mod/i)%mod;
	for(int i=2;i<=4000;++i) inv[i]=inv[i-1]*inv[i]%mod;
	n=read(),m=read();
	for(int i=1;i<=m;++i) a[i]=read();
	for(int i=0,opt=1;i<n;++i,opt=mod-opt){//i==n -> n-i-1=-1 skip
		int tmp=C(n,i);
		for(int j=1;j<=m;++j) tmp=(tmp*C(a[j]+n-i-1,n-i-1))%mod;
		ans=(ans+(tmp*opt)%mod)%mod;
	}
	printf("%lld",ans);
	return 0;
}

Bzoj2839 集合计数

\(f_i\) 表示至少有 \(i\) 个重复元素(交集大小至少为 \(i\) ),则有

\[f_i=\binom{n}{i}(2^{2^{n-i}}-1) \]

表示选出 \(i\) 个重复元素,剩下的 \(2^{n-i}\) 个子集中任意选,但是不能全为空集(全为空集哪来的交集)。

容斥一下,有

\[Ans=\sum\limits_{i=k}^n(-1)^{i-k}\binom{i}{k}f_i \]

#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read(){
	int ans=0,f=0;char ch=getchar();
	while(!isdigit(ch)) f|=(ch=='-'),ch=getchar();
	while(isdigit(ch)) ans=(ans<<3)+(ans<<1)+(ch^48),ch=getchar();
	return f?-ans:ans;
}
const int N=1e6+5,INF=1e9,mod=1e9+7;
int n,k,ans;
int fac[N],inv[N];
int f[N];
inline void init(){
	fac[0]=fac[1]=inv[0]=inv[1]=1;
	for(int i=2;i<=1e6;++i) fac[i]=fac[i-1]*i%mod,inv[i]=inv[mod%i]*(mod-mod/i)%mod;
	for(int i=2;i<=1e6;++i) inv[i]=inv[i-1]*inv[i]%mod;
}
inline int qpow(int x,int y,int p){
	int res=1;
	while(y){
		if(y&1) res=(res*x)%p;
		x=(x*x)%p;
		y>>=1;
	}
	return res;
}
inline int C(int x,int y){return x<0||y<0||y>x?0:fac[x]*inv[y]%mod*inv[x-y]%mod;}
signed main(){
	init();
	n=read(),k=read();
	for(int i=k;i<=n;++i) f[i]=C(n,i)*(qpow(2,qpow(2,n-i,mod-1),mod)-1)%mod;
	for(int i=k,opt=1;i<=n;++i,opt=mod-opt) ans=(ans+opt*C(i,k)%mod*f[i]%mod)%mod;
	printf("%lld",ans);
	return 0;
}

Loj#570 Misaka Network 与任务

我们可以用高位前缀和(差分)求出每一个情况会有多少种方法可以到达,那么对于 \(k\) 步,每一步都可以选这种情况,总数为 \({cnt_s}^{k}\)。但是会重复!比如当前状态的下一个状态也会算到当前状态的,所以我们用容斥,加减加减就好了。

#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read(){
	int ans=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-') f^=1;ch=getchar();}
	while(isdigit(ch)){ans=(ans<<3)+(ans<<1)+ch-48;ch=getchar();}
	return f?ans:-ans;
}
const int N=1e6+5,SIZE=1<<22,INF=1e9,MOD=1e9+7;
int n,m,k;
int cnt[SIZE+15];
int ans;
int one[SIZE+15];
int qpow(int x,int y){
	int res=1;
	while(y){
		if(y&1) res=(res*x)%MOD;
		x=(x*x)%MOD;
		y>>=1;
	}
	return res;
}
signed main(){
	n=read(),m=read(),k=read();
	for(int i=1;i<=m;i++) cnt[read()]++;
	for(int i=1;i<=1<<n;i++) one[i]=one[i>>1]+(i&1);
	for(int i=0;i<n;i++)
		for(int j=1;j<(1<<n);j++)
			if((1<<i)&j) cnt[j-(1<<i)]+=cnt[j];
	for(int j=1;j<1<<n;j++){
		if(one[j]&1) ans=(ans+qpow(cnt[j],k))%MOD;
		else ans=(ans-qpow(cnt[j],k)+MOD)%MOD;
	}
	printf("%lld",ans);
	return 0;
}

LuoguP4916 [MtOI2018]魔力环

神仙zhs暴切的题。

断环为链,题目就是把 \(m\) 个黑球放入 \(n-m\) 个空隙里(由于是环,所以首尾只能有一个空)。由于会被重复统计,所以我们类似硬币购物的做法,我们可以枚举序列中有多少项超过了 \(k\),先预先给它们每项都分配 \(k+1\) 个黑球,然后将剩下的黑球随意分配(分配到之前预分配的项也没关系,用隔板法计算),用容斥原理进行计算,则有

\[\sum\limits_{i=0}^{\lfloor\frac{m}{k+1}\rfloor}(-1)^i\binom{n-m}{i}\binom{n-i\times (k+1)-1}{n-m-1} \]

考虑怎么解决重复统计

可发现一种方案被重复统计的次数是其序列最短循环节的长度(如 \(2,1,2,1\) 被重复统计了 \(2\) 次,为 \(2,1,2,1\)\(1,2,1,2\)),所以我们枚举循环节个数。当有 \(i\) 个循环节时,循环长度为 \(\frac{n}{i}\),相当于把 \(m\) 个黑球变成了 \(\frac{m}{i}\) 个黑球,把 \(n-m\) 个白球变成了 \(\frac{n-m}{i}\) 个白球,因为是由 \(i\) 循环节构成,所以 \(i\) 满足 \(i|gcd(n-m,m)\),对于每个 \(i\) 都可以用上述的容斥计算,将第 \(i\) 次计算的结果记为 \(f_i\)

最后,考虑如何统计答案。前面说不一定是最小循环节,这是因为对某个循环节长度的情况计算时可能包括循环节更小的结果,如循环节为 \(4\) 时包含了 \(1,2,1,2,1,2,1,2\),而它实际最小循环节为 \(2\)。考虑再次容斥,记 \(g(i)\) 为循环节个数恰好\(i\) 的方案数,则有

\[f_i=\sum\limits_{i|d}g_d \]

莫反,则有

\[g_i=\sum\limits_{i|d}\mu(\frac{d}{i})f_d \]

除去重复算的,则有

\[Ans=\sum\limits_{d|gcd(m,n-m)} g_d \times \frac{d}{n-m} \]

#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read(){
	int ans=0,f=0;char ch=getchar();
	while(!isdigit(ch)) f|=(ch=='-'),ch=getchar();
	while(isdigit(ch)) ans=(ans<<3)+(ans<<1)+(ch^48),ch=getchar();
	return f?-ans:ans;
}
const int N=2e5+5,INF=1e9,mod=998244353;
int n,m,k,ans;
int fac[N],inv[N],invfac[N];
int mu[N],vis[N],prim[N],pn;
int f[N];
inline void init(){
	mu[1]=1;
	for(int i=2;i<=1e5;++i){
		if(!vis[i]) prim[++pn]=i,mu[i]=-1;
		for(int j=1;j<=pn&&prim[j]*i<=1e5;++j){
			vis[prim[j]*i]=1;
			if(i%prim[j]==0) break;
			else mu[i*prim[j]]=-mu[i];
		}
	}
	fac[0]=fac[1]=inv[0]=inv[1]=invfac[0]=invfac[1]=1;
	for(int i=2;i<=1e5;++i) fac[i]=fac[i-1]*i%mod,inv[i]=inv[mod%i]*(mod-mod/i)%mod;
	for(int i=2;i<=1e5;++i) invfac[i]=invfac[i-1]*inv[i]%mod;
}
inline int C(int x,int y){return y>x||n<0||m<0?0:fac[x]*invfac[y]%mod*invfac[x-y]%mod;}
inline int gcd(int x,int y){return !x?y:gcd(y%x,x);}
signed main(){
	init();
	n=read(),m=read(),k=read();
	if(n==m) return printf(m<=k?"1":"0"),0;
	if(!m) return printf("1"),0;
	int slzs=gcd(n-m,m);
	for(int x=slzs;x>0;--x){
		if(slzs%x) continue;
		for(int i=0,opt=1,n1=n/x,m1=m/x;i<=m1/(k+1);++i,opt=mod-opt) f[x]=(f[x]+opt*C(n1-m1,i)%mod*C(n1-i*(k+1)-1,n1-m1-1)%mod)%mod;
	}
	for(int x=1;x<=slzs;++x){
		if(slzs%x) continue;
		int tmp=0;
		for(int i=x;i<=slzs;i+=x) tmp=(tmp+mu[i/x]*f[i]%mod+mod)%mod;
		ans=(ans+tmp*x%mod)%mod;
	}
	printf("%lld",ans*inv[n-m]%mod);
	return 0;
}

暂时完结了

posted @ 2021-07-25 21:51  Quick_Kk  阅读(489)  评论(1编辑  收藏  举报