集合计数(容斥原理)

题目传送门-洛谷

思路分析

首先对于一个有 n n n个元素的集合一定有 2 n 2^n 2n个不同的子集,在这些子集中,也一定有 2 2 n 2^{2^n} 22n个不同的子集取法,刨去空集,那么我们就获得了 2 2 n − 1 2^{2^n}-1 22n1种取法,那么因为你选的若干个集合中一定有 k k k个元素作为交集,那么我们现在先要选出来这些元素都是啥,因此要求出 C n k C_n^k Cnk,现在我们的答案就是要求 C n k ∗ ( 2 2 n − k − 1 ) C_n^k*(2^{2^{n-k}}-1) Cnk(22nk1),你以为完了吗,不!这并不是最终的结果!因为一个问题,我们现在只是选出交集至少为 k k k的情况,但是答案要求我们选出交集为 k k k的情况,因此我们求大了,考虑容斥。
首先我们考虑容斥的范围是 i : i: i: k ≤ i ≤ n k \le i \le n kin,那么我们现在就是这样的一个情况,我们需要找到 C n i C_n^i Cni代表我们当前在哪些数里面计算容斥, C i k C_i^k Cik代表我们现在选的这些集合必须包含这 k k k个数字,然后利用乘法原理,就可以计算出来答案是:
C n n ∗ C n k ∗ ( 2 2 n − k − 1 ) − C n n − 1 ∗ C n − 1 k ∗ ( 2 2 n − 1 − k − 1 ) + C n n − 2 ∗ C n − 2 k ∗ ( 2 2 n − 2 − k − 1 ) − . . . . . . C_n^n*C_n^k*(2^{2^{n-k}}-1)-C_n^{n-1}*C_{n-1}^k*(2^{2^{n-1-k}}-1)+C_n^{n-2}*C_{n-2}^k*(2^{2^{n-2-k}}-1)-...... CnnCnk(22nk1)Cnn1Cn1k(22n1k1)+Cnn2Cn2k(22n2k1)......
那么针对组合数,我们可以考虑预处理阶乘和逆元。

#include<iostream>
using namespace std;
#define int long long 
#define LL long long
#define mod 1000000007
#define TLE ios::sync_with_stdio(0),cin.tie(0)
int inv[2102100];
int A[2102100];
int qmi(int a, int k, int p)
{
    int res = 1ll % p;
    while (k)
    {
        if (k & 1ll) res = (LL)res * a % p;
        a = (LL)a * a % p;
        k >>= 1;
    }
    return res;
}

int C(int a,int b,int p=mod){
	return A[a]%p*inv[b]%p*inv[a-b]%p;
} 

signed main(){
	TLE;
	int n,k;
	cin>>n;
	int ans=0;
	A[1]=1ll;
	A[0]=1ll;
	for(int i=2;i<=n;i++){
		A[i]=A[i-1]*i%mod;
	}
	for(int i=n;i>=0;i--){
		inv[i]=qmi(A[i],mod-2,mod);
	}
	int T;
	cin>>T;
	while(T--){
		cin>>k;
		ans=0;
		for(int i=k,j=1;i<=n;i++){
			int res=qmi(2ll,n-i,mod-1ll);
			res=qmi(2ll,res,mod)-1ll;
			int u=C(i,k)%mod*C(n,i)%mod;
			ans+=j*u*res;
			ans%=mod;
			j*=-1ll;
			ans=(ans+mod)%mod;
		}
	cout<<ans<<endl;
	}
}

然后tle了,因为题目中要求使用线性的时间复杂度来优化本题,当前我们的算法的时间复杂度是 O ( n l o g n ) O(nlogn) O(nlogn)主要在于求逆元和每一次遍历求快速幂那里浪费掉了时间,那么我们现在如何优化?

1.对于求逆元的优化

由于本题的模数是素数,因此我们可以使用费马小定理来求阶乘的逆元
a p − 1 ≡ 1 ( m o d    p ) a^{p-1}≡1(\mod p) ap11(modp)
那么我们就可以求出逆元是 a p − 2 a^{p-2} ap2
1 n ! = 1 ( n + 1 ) ! ∗ ( n + 1 ) \frac{1}{n!}=\frac{1}{(n+1)!}*(n+1) n!1=(n+1)!1(n+1)
因此 i n v [ n ! ] = i n v [ ( n + 1 ) ! ] ∗ ( n + 1 ) inv[n!]=inv[(n+1)!]*(n+1) inv[n!]=inv[(n+1)!](n+1)
因此可以用如下代码优化

	inv[n]=qmi(A[n],mod-2,mod);
	for(int i=n-1;i>=0;i--){
		inv[i]=inv[i+1]*(i+1ll)%mod;
	}

2.对于求幂值的优化

因为底下如果我们要求幂值,那么我们用ksm就一定会有一个log,那么就会超时,但是我们观察到他们底下的底数是2,指数也是2的指数,那么我们就可以每一次循环连续乘 O ( 1 ) O(1) O(1)的时间复杂度,我们现在要算的是 2 2 n − i 2^{2^{n-i}} 22ni我们的 i i i是倒序遍历,每一次都是呈指数倍增加。
2 2 0 2^{2^{0}} 220 2 2 1 2^{2^{1}} 221 2 2 2 2^{2^{2}} 222
又知道一个规律
2 2 0 ∗ 2 2 0 = 2 2 1 2^{2^{0}}*2^{2^{0}}=2^{2^{1}} 220220=221 2 2 1 ∗ 2 2 1 = 2 2 2 2^{2^{1}}*2^{2^{1}}=2^{2^{2}} 221221=222
因此可以找一个res来记住我们的这个数值,每一次的循环都可以累成。

#include<iostream>
using namespace std;
#define int long long 
#define LL long long
#define mod 1000000007
#define TLE ios::sync_with_stdio(0),cin.tie(0)
int inv[2102100];
int A[2102100];
int pp[102100];
int qmi(int a, int k, int p)
{
    int res = 1ll % p;
    while (k)
    {
        if (k & 1ll) res = (LL)res * a % p;
        a = (LL)a * a % p;
        k >>= 1;
    }
    return res;
}

int C(int a,int b,int p=mod){
	return A[a]%p*inv[b]%p*inv[a-b]%p;
} 

signed main(){
	TLE;
	int n,k;
	cin>>n;
	int ans=0;
	A[1]=1ll;
	A[0]=1ll;
	for(int i=2;i<=n;i++){
		A[i]=A[i-1]*i%mod;
	}
	inv[n]=qmi(A[n],mod-2,mod);
	for(int i=n-1;i>=0;i--){
		inv[i]=inv[i+1]*(i+1ll)%mod;
	}
	int T;
	cin>>T;
	int tot=0;
	while(T--){
		cin>>k;
		int res=2;
		ans=0;
		for(int i=n,j=((n-k)&1==1?-1:1);i>=k;i--){
			int u=C(i,k)%mod*C(n,i)%mod;
			ans+=j*u*(res-1);
			res*=res;
			res%=mod;
			ans%=mod;
			j*=-1ll;
			ans=(ans+mod)%mod;
		}
		pp[++tot]=ans;
	} 

	for(int i=1;i<=tot;i++){
		cout<<pp[i]<<endl;
	}
	
}

2021-9-14 UPD:
至于这道题为什么会使用容斥原理呢?
我们现在来分析的这个问题实际上不能保证我当前一定当且仅当选了k个元素的交集,但是我可以很容易的求出来我现在的交集元素大于等于k的集合种类数,因此我可以使用容斥原理最终解决这个问题

2022-04-12 UPD:
本题上述代码的时间复杂度并不可观,上述程序跑出了3.5s的好成绩,现在经更改,可以800ms通过,因此时限也就从原来的5s压缩到了现在的2s。

#include<iostream>
using namespace std;
#define ll long long 
#define LL long long
//#define int long long
#define endl '\n'
#define mod 1000000007
//#define TLE ios::sync_with_stdio(0),cin.tie(0)
int inv[2102100];
int A[2102100];
int pp[102100];
int qmi(int a, int k, int p)
{
    int res = 1ll % p;
    while (k)
    {
        if (k & 1ll) res = (LL)res * a % p;
        a = (LL)a * a % p;
        k >>= 1;
    }
    return res;
}

int C(int a,int b){
	return 1ll*A[a]*inv[b]%mod*inv[a-b]%mod;
} 

signed main(){
	//TLE;
	int n,k;
	scanf("%d",&n);
	//cin>>n;
	ll ans=0;
	A[1]=1;
	A[0]=1;
	for(int i=2;i<=n;++i){
		A[i]=1ll*A[i-1]*i%mod;
	}
	inv[n]=qmi(A[n],mod-2,mod);
	for(int i=n-1;i>=0;--i){
		inv[i]=1ll*inv[i+1]*(i+1ll)%mod;
	}
	int T;
	scanf("%d",&T);
	//cin>>T;
	int tot=0;
	while(T--){
		scanf("%d",&k);
		//cin>>k;
		ll res=2;
		ans=0;
		for(int i=n-k,j=(i&1?-1:1);i>=0;--i){
			//ll u=1ll*;
			ans=(ans+1ll*j*C(n-k,i)*(res-1)%mod)%mod;
			res=(res*res)%mod;
			//res%=mod;
			ans=(ans+mod)%mod;
			j*=-1;
		}
		ans=1ll*ans*C(n,k)%mod;
		cout<<ans<<endl;
	} 
	
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值