容斥原理基础

文章介绍了容斥原理如何解决关于喜欢课程的学生数量问题,以及在处理不被特定数字整除的数和不定方程解的问题中,二进制枚举和深度优先搜索(DFS)的效率对比。着重展示了在处理这类数学问题时,特别是涉及取模运算时的技巧和优化方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

容斥原理的引入


从一个小学奥数问题引入:
一个班级有50人
喜欢语文的人有20人
喜欢数学的人有30人
同时喜欢语文数学的人有10人。
问题:

  1. 两门都不喜欢的有多少人
  2. 至少喜欢一个的有多少人

至少喜欢一门 20+30-10=40 都不喜欢 50-40=10


再将上面的课程门数进一步扩展为3门,问题变为
一个班级有60人
喜欢A的人有20人
喜欢B的人有30人
喜欢C的人有25人
同时喜欢AB的人有10人
同时喜欢AC的人有7人
同时喜欢BC的人有8人
同时喜欢ABC的人有2人

至少喜欢一门的人:A+B+C-AB-AC—BC+ABC=20+30+25-10-7-8+2=52 有60-52=8个同学一门都不喜欢


从集合的角度考虑

将上述问题抽象用数学的形式表示
∣ A ∪ B ∣ = ∣ A ∣ + ∣ B ∣ − ∣ A ∩ B ∣ |A \cup B|=|A|+|B|-|A \cap B| AB=A+BAB

∣ A ∪ B ∪ C ∣ = ∣ A ∣ + ∣ B ∣ + ∣ C ∣ − ∣ A ∩ B ∣ − ∣ A ∩ C ∣ − ∣ B ∩ C ∣ + ∣ − ∣ A ∩ B ∪ C ∣ |A \cup B \cup C|=|A|+|B|+|C|-|A \cap B|-|A \cap C|-|B \cap C|+|-|A \cap B \cup C| ABC=A+B+CABACBC+ABC


推广

image


例子

不被2、3、5整除的数

image

890. 能被整除的数

两种实现方法
虽然说理论上dfs只有 O ( 2 n ) O(2^n) O(2n)的复杂度,而二进制枚举的复杂度是 O ( m 2 n ) O(m2^n) O(m2n)但是实际上跑起来的效果二进制枚举跑的快很多,dfs还是跑的比较慢,可能是递归调用的开销比较大吧。
屏幕截图 2024-02-08 014954.png
屏幕截图 2024-02-08 015006.png


二进制枚举超集的写法


#include <bits/stdc++.h> 
#define int long long
#define rep(i,a,b) for(int i = (a); i <= (b); ++i)
#define fep(i,a,b) for(int i = (a); i >= (b); --i)
#define pii pair<int, int>
#define pll pair<long long, long long>
#define ll long long
#define db double
#define endl '\n'
#define x first
#define y second
#define pb push_back

using namespace std;

const int N=2e5+10;

void solve()
{
	int n,m;cin>>n>>m;
	vector<int>p(m);
	rep(i,0,m-1)	cin>>p[i];
	int ans=0;
	rep(i,1,(1<<m)-1){
		
		int sgn=0,mul=1;
		rep(j,0,m-1){
			if(i&(1<<j)){
				sgn++;
				if(mul*p[j]>n){
					mul=-1;
					break;
				}
				mul*=p[j];
			}
		}
		if(mul!=-1){
			if(sgn&1){
				//符号为正
				ans+=n/mul;
			}else{
				ans-=n/mul;
			}
		}
	}
	cout<<ans<<endl;
}

signed main(){
	ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
//   	freopen("1.in", "r", stdin);
  	int _;
//	cin>>_;
//	while(_--)
	solve();
	return 0;
}

dfs写法


#include <bits/stdc++.h> 
#define int long long
#define rep(i,a,b) for(int i = (a); i <= (b); ++i)
#define fep(i,a,b) for(int i = (a); i >= (b); --i)
#define pii pair<int, int>
#define pll pair<long long, long long>
#define ll long long
#define db double
#define endl '\n'
#define x first
#define y second
#define pb push_back

using namespace std;

const int N=2e5+10;

int ans=0,n,m;
vector<int>path;
vector<int>p(N);
void dfs(int u){
	if(u==m){
		if(!path.size()){
			return;
		}
		int sgn=path.size()&1?1:-1;
		int mul=1;
		for(auto i:path){
			if(mul>n){
				mul=0;
				break;
			}
			mul*=i;
		}
		if(mul)    ans+=n/mul*sgn;
		return;
	}
	dfs(u+1);
	path.pb(p[u]);
	dfs(u+1);
	path.pop_back();
};

void solve()
{
	cin>>n>>m;
	rep(i,0,m-1)	cin>>p[i];
	dfs(0);
	cout<<ans<<endl;
}

signed main(){
	ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
//   	freopen("1.in", "r", stdin);
  	int _;
//	cin>>_;
//	while(_--)
	solve();
	return 0;
}


错排问题

错排问题:
P i ≠ i , P i 为排列 P_i \not = i,P_i为排列 Pi=iPi为排列
image

求不定方程的解

image
如果没有对 X i X_i Xi进行限制的话,可以直接用隔板法去做,直接插入 n − 1 n-1 n1个隔板,答案就是 C ( m + n − 1 n − 1 ) C(_{m+n-1}^{n-1}) C(m+n1n1)
但是现在对于每一个 X i X_i Xi我们都有一个上限 b i b_i bi

考虑容斥原理
这道题目并没有明显的容斥不像上面的倍数
我们把 0 < = x i < = b i 0<=x_i<=b_i 0<=xi<=bi转化成
( x i > = 0 ) − ( x i > = b i + 1 ) (x_i>=0)-(x_i>=b_i +1) (xi>=0)(xi>=bi+1)
( x i > = 0 ) (x_i>=0) (xi>=0)可以理解为没有限制
( x i > = b i + 1 ) (x_i>=b_i +1) (xi>=bi+1)可以理解为最少选 b i + 1 b_i+1 bi+1个,做一个映射,可以用隔板法去做。
$ (^{m-b_i+1+n-1} _{n-1})$

#include <bits/stdc++.h> 
#define int long long
#define rep(i,a,b) for(int i = (a); i <= (b); ++i)
#define fep(i,a,b) for(int i = (a); i >= (b); --i)
#define pii pair<int, int>
#define pll pair<long long, long long>
#define ll long long
#define db double
#define endl '\n'
#define x first
#define y second
#define pb push_back

using namespace std;

const int N=2e5+10,mod=1e9+7;

int n,m,b[20];
int ifac,inv[20],ans;
int cal(int x){
	//C(x+n-1,n-1)
	//x+1,...,x+n-1/(n-1)!
	int ans=1;
	rep(i,1,n-1){
		ans=ans*(x+i)%mod;
	}
	ans=ans*ifac%mod;
	return ans;
}
void dfs(int d,int sgn, int sum){
	if(d==n){
		if(sum>m)	return;
		ans=(ans+sgn*cal(m-sum))%mod;
	}else{
		//x[i]>=0,当前这个没有违反
		dfs(d+1,sgn,sum);
		//x[i]>=b[i]+1,当前这个违反了
		dfs(d+1,-sgn,sum+b[d]+1);
	}
}

void solve()
{
	cin>>n>>m;
	//求n-1阶乘的逆元
	ifac=1;
	rep(i,1,n-1){
		if(i==1)	inv[i]=1;
		else inv[i]=(mod-mod/i)*inv[mod%i]%mod;
		ifac=ifac*inv[i]%mod;
	}
	rep(i,0,n-1){
		cin>>b[i];
	}
	//第i个变量,容斥系数,数值之和
	dfs(0,1,0);
	cout<<ans<<endl;
}

signed main(){
	ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
   	freopen("1.in", "r", stdin);
  	int _;
//	cin>>_;
//	while(_--)
	solve();
	return 0;
}

Devu和鲜花

这个是上面的题目套了一个皮套,本质上还是不定方程的求解
这道题目取模极其恶心,每一步都要进行取模,不然就可能有溢出的风险,写数论、组合数学相关的题目一定要特别注意取模


dfs写法


#include <bits/stdc++.h> 
#define int long long
#define rep(i,a,b) for(int i = (a); i <= (b); ++i)
#define fep(i,a,b) for(int i = (a); i >= (b); --i)
#define pii pair<int, int>
#define pll pair<long long, long long>
#define ll long long
#define db double
#define endl '\n'
#define x first
#define y second
#define pb push_back

using namespace std;

const int N=2e5+10,mod=1e9+7;

int n,m,b[22];
int ifac,inv[22],ans;
int cal(int x){
	//C(x+n-1,n-1)
	//x+1,...,x+n-1/(n-1)!
	int ans=1;
	rep(i,1,n-1){
		ans=ans%mod*(x%mod+i%mod)%mod;
	}
	ans=ans*ifac%mod;
	return ans;
}
void dfs(int d,int sgn, int sum){
	if(d==n){
		if(sum>m)	return;
		ans+=sgn*cal(m-sum)%mod;
// 		ans=(ans+sgn*cal(m-sum))%mod;
	}else{
		//x[i]>=0,当前这个没有违反
		dfs(d+1,sgn,sum);
		//x[i]>=b[i]+1,当前这个违反了
		dfs(d+1,-sgn,sum+b[d]+1);
	}
}

void solve()
{
	cin>>n>>m;
	ifac=1;
	rep(i,1,n-1){
		if(i==1)	inv[i]=1;
		else inv[i]=(mod-mod/i)*inv[mod%i]%mod;
		ifac=ifac*inv[i]%mod;
	}
	rep(i,0,n-1){
		cin>>b[i];
	}
	//第i个变量,容斥系数,数值之和
	dfs(0,1,0);
	ans%=mod;
	if(ans<0)   ans+=mod;
	cout<<ans%mod<<endl;
}

signed main(){
	ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
//   	freopen("1.in", "r", stdin);
  	int _;
//	cin>>_;
//	while(_--)
	solve();
	return 0;
}

二进制枚举写法


#include <bits/stdc++.h> 
#define int long long
#define rep(i,a,b) for(int i = (a); i <= (b); ++i)
#define fep(i,a,b) for(int i = (a); i >= (b); --i)
#define pii pair<int, int>
#define pll pair<long long, long long>
#define ll long long
#define db double
#define endl '\n'
#define x first
#define y second
#define pb push_back

using namespace std;

const int N=2e5+10,mod=1e9+7;

int n,m,s[22];
int inv[22],fac[22],ifac;

int ksm(int a,int b,int p){
	int res=1;
	for(;b;b>>=1){
		if(b&1)	res=res*a%p;
		a=a*a%p;	
	}
	return res;
}

int C(int a,int b){
	if(a<b)	return 0;
	int res=1;
	//这里一定要注意要先对i取模再乘不然就会寄
	rep(i,a-b+1,a)	res=i%mod*res%mod;
	return res*inv[n-1]%mod;
}


void solve()
{
	cin>>n>>m;
	rep(i,0,n-1){
		cin>>s[i];
	}

	fac[0]=inv[0]=1;
	rep(i,1,n-1){
		fac[i]=fac[i-1]%mod*i%mod;
		inv[i]=ksm(fac[i],mod-2,mod)%mod;
	}
	int res=0;
	rep(i,0,(1<<n)-1){
		int a=m+n-1,b=n-1,sgn=1;
		rep(j,0,n-1){
			if((i>>j)&1){
				sgn*=-1;
				a-=s[j]+1;
			}   
		}
		res=(res+C(a,b)*sgn)%mod;
	}
	if(res<0)	res+=mod;
	cout<<res<<endl;
}

signed main(){
	ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
//   	freopen("1.in", "r", stdin);
  	int _;
//	cin>>_;
//	while(_--)
	solve();
	return 0;
}
```[E. Devu and Flowers](https://codeforces.com/problemset/problem/451/E "E. Devu and Flowers")
<think>嗯,用户想了解如何在C语言中实现容斥原理的应用,并且需要示例代码。首先,我需要回忆一下容斥原理的基本概念。容斥原理通常用于计算多个集合的并集大小,通过交替加减不同集合的交集来避免重复计算。这在组合数学和算法问题中很常见,比如计算满足某些条件的数的个数。 接下来,我得想想如何在C语言中具体实现这个原理。通常,容斥原理的实现会涉及到遍历所有可能的子集组合,然后根据子集的大小来决定是加还是减。例如,对于m个集合,需要遍历从1到2^m-1的所有子集,然后计算每个子集的交集大小,并根据子集元素的奇偶性来决定符号。 然后,用户提供的引用中有几个代码示例,比如引用[1]中的代码,看起来像是使用位运算来生成子集,并且通过递归的gcd函数来计算最小公倍数。这可能是一个应用容斥原理的例子,用于计算在n范围内能被多个数整除的数的数量。例如,计算1到n中能被数组l中的至少一个数整除的数的个数,这可以通过容斥原理来避免重复计算。 我需要将这些思路整理成一个清晰的步骤,解释如何用C语言实现容斥原理。首先,可能需要一个函数来计算最大公约数(gcd),然后计算最小公倍数(lcm),因为交集的大小通常涉及到lcm。例如,两个数的lcm是它们的乘积除以gcd。然后,遍历所有子集,对于每个子集,计算这些数的lcm,并根据子集的大小决定加减。 然后,考虑示例代码的结构。引用[1]中的代码似乎是这样做的:输入n和m,然后输入m个数,遍历所有可能的子集(通过位掩码k从0到2^m-1),计算每个子集的lcm,并根据子集的元素个数奇偶性来调整总和s的值。例如,如果子集有奇数个元素,就减去n/lcm,否则加上。这是因为容斥原理中,奇数次交集加,偶数次减,但这里可能是相反,需要仔细看代码逻辑。 此外,用户提到的引用[2]和[3]可能涉及其他应用场景或优化方法,比如使用前缀和或其他数据结构来简化计算。不过当前问题主要是容斥原理的实现,所以可能需要集中在位运算遍历子集和lcm的计算上。 在解释代码时,需要分步骤说明,比如如何生成子集,如何计算交集大小,如何处理符号。同时,需要给出具体的例子,比如计算1到n中能被至少一个数整除的个数,并解释代码的运行流程。 可能还需要提到代码中的关键点,比如如何用位掩码k来枚举子集,如何通过位与操作检查哪些元素被包含在当前子集中,如何维护当前子集的lcm,以及如何处理溢出问题(比如r可能会超过n,这时候可以提前终止)。 最后,应用场景部分需要举几个实际的例子,比如数学问题中的计数,或者几何问题中的区域计算,引用中的几何工具应用可能相关,比如poj题目中的几何问题可能用到容斥原理来计算覆盖区域。 总结来说,回答需要包括容斥原理的C语言实现步骤,示例代码的解释,以及应用场景的举例。同时,根据用户提供的引用,特别是引用[1]中的代码,可能需要将其作为示例进行详细说明,并引用相关部分。</think>### C语言中容斥原理的实现与应用 #### 一、容斥原理基础 容斥原理用于计算多个集合的并集大小,公式为: $$|A_1 \cup A_2 \cup \dots \cup A_m| = \sum |A_i| - \sum |A_i \cap A_j| + \sum |A_i \cap A_j \cap A_k| - \dots + (-1)^{m+1}|A_1 \cap \dots \cap A_m|$$ #### 二、实现步骤 1. **位运算枚举子集** 通过二进制位掩码遍历所有子集组合(共$2^m-1$种),例如: ```c for (int k = 1; k < (1 << m); k++) ``` 2. **计算集合交集** 使用最大公约数(GCD)和最小公倍数(LCM)处理交集: ```c int gcd(int a, int b) { return b ? gcd(b, a % b) : a; } long long lcm(long long a, long long b) { return a / gcd(a, b) * b; } ``` 3. **符号控制** 根据子集元素个数的奇偶性决定加减符号: ```c if (__builtin_popcount(k) % 2) sum += n / current_lcm; else sum -= n / current_lcm; ``` #### 三、完整示例代码 以下代码实现**计算1~n中能被数组l中任意数整除的数的个数**: ```c #include <stdio.h> int gcd(int a, int b) { return b ? gcd(b, a % b) : a; } int main() { int n, m, l[15]; while (scanf("%d%d", &n, &m) != EOF) { for (int i = 0; i < m; i++) scanf("%d", &l[i]); int result = 0; for (int k = 1; k < (1 << m); k++) { long long current_lcm = 1; int cnt = 0; for (int i = 0; i < m; i++) { if (k & (1 << i)) { current_lcm = current_lcm / gcd(current_lcm, l[i]) * l[i]; if (current_lcm > n) break; // 防止溢出 cnt++; } } if (cnt % 2) result += n / current_lcm; else result -= n / current_lcm; } printf("%d\n", result); } return 0; } ``` #### 四、关键代码解析 1. **位掩码遍历** `k`从1到$2^m-1$遍历所有非空子集,每位表示是否选中对应元素[^1]。 2. **动态计算LCM** 通过不断更新`current_lcm`处理多个数的LCM,同时防止数值溢出[^3]。 3. **奇偶性判断** `__builtin_popcount(k)`统计二进制中1的个数,决定加减符号[^2]。 #### 五、应用场景 1. **数学问题** 计算区间内满足多个条件的整数数量(如同时被2/3/5整除的数)。 2. **几何覆盖** 求多个几何图形覆盖区域的总面积(如圆、矩形的交集计算)[^4]。 3. **组合计数** 解决排列组合中的包含排除问题,如错位排列数的计算。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值