容斥原理
1、什么是容斥原理?
容斥原理是一种计数的方法:先不考虑重叠的情况,把包含于某内容中的所有对象的数目先计算出来,然后再把计数时重复计算的数目排斥出去,使得计算的结果既无遗漏又无重复,这种计数的方法称为容斥原理。
注:也可以将容斥定理描述为:要计算几个集合并集的大小,我们要先将所有单个集合的大小计算出来,然后减去所有两个集合相交的部分,再加回所有三个集合相交的部分,再减去所有四个集合相交的部分…依此类推,一直计算到所有集合相交的部分。(可以理解为就是先把所有单个集合全加一遍然后再去重)
为了直观的表达,可以采用韦恩图表示:
那么可以清晰的观察到: A ∪ B = A + B − A ∩ B A\cup B=A+B-A\cap B A∪B=A+B−A∩B
那么再增加一个集合C,结果又会怎么样喃?
同理我们可以得出以下结论:
A ∪ B ∪ C = A + B + C − A ∩ B − A ∩ C − C ∩ B + A ∪ B ∪ C A\cup B\cup C=A+B+C-A\cap B-A\cap C-C\cap B +A\cup B\cup C A∪B∪C=A+B+C−A∩B−A∩C−C∩B+A∪B∪C
…
通过不断地增加集合的数量,最终可以得出容斥原理的一个重要公式。
1.1、容斥原理公式
∣ ∪ i = 1 n A i ∣ = ∑ i = 1 n ∣ A i ∣ − ∑ i , j : 1 ≤ i ≤ j ≤ n ∣ A i ∩ A j ∣ + ∑ i , j , k : 1 ≤ i ≤ j ≤ k ≤ n ∣ A i ∩ A j ∩ A k ∣ − . . + ( − 1 ) n − 1 ∣ A 1 ∩ . . ∩ A n ∣ |\cup_{i=1}^{n}A_i|=\sum_{i=1}^{n}|A_i|-\sum_{i,j:1\leq i\leq j\leq n}|A_i\cap A_j|+\sum_{i,j,k:1\leq i\leq j\leq k\leq n}|A_i\cap A_j\cap A_k|-..+(-1)^{n-1}|A_1\cap ..\cap A_n| ∣∪i=1nAi∣=i=1∑n∣Ai∣−i,j:1≤i≤j≤n∑∣Ai∩Aj∣+i,j,k:1≤i≤j≤k≤n∑∣Ai∩Aj∩Ak∣−..+(−1)n−1∣A1∩..∩An∣
2、二进制枚举
2.1、前提定理:
含有N个元素的集合的一切子集的个数为
2
n
2^n
2n
证明
:
含有
0
个元素的子集有:
C
n
0
含有
1
个元素的子集有:
C
n
1
含有
2
个元素的子集有:
C
n
2
含有
3
个元素的子集有:
C
n
3
.
.
.
.
含有
n
个元素的子集有:
C
n
n
所以:一切子集的总个数为:
C
n
0
+
C
n
1
+
.
.
.
.
C
n
n
由二项式定理可得
∑
i
=
0
n
c
n
i
=
2
n
证明:\\ 含有0个元素的子集有:C_n^0 \\ 含有1个元素的子集有:C_n^1 \\ 含有2个元素的子集有:C_n^2 \\ 含有3个元素的子集有:C_n^3 \\ ....\\ 含有n个元素的子集有:C_n^n\\ 所以:一切子集的总个数为:C_n^0+C_n^1+....C_n^n\\ 由二项式定理可得\sum_{i=0}^{n}c_n^i=2^n
证明:含有0个元素的子集有:Cn0含有1个元素的子集有:Cn1含有2个元素的子集有:Cn2含有3个元素的子集有:Cn3....含有n个元素的子集有:Cnn所以:一切子集的总个数为:Cn0+Cn1+....Cnn由二项式定理可得i=0∑ncni=2n
2.2、如何枚举出N个元素的集合的所有子集?
利用二进制的特性,由于二进制是01字符串,故我们可以用二进制数的每一位表示某个集合是否被选中(1表示该集合被选中,0表示该集合没有被选中)
如以下的例子:
集合编号 | 1 | 2 | 3 |
---|---|---|---|
二进制 | 1 | 0 | 1 |
状态 | 选 | 不选 | 选 |
2.3、原理
由于我们已经知道 n n n个元素的集合的子集一共有 2 n 2^n 2n个,而长度为n的二进制数表示的范围为 [ 0 , 2 n − 1 ] [0,2^{n}-1] [0,2n−1],在这个区间上正好有 2 n 2^n 2n个数,所以这每个数也正好可以表示每个集合。
例如:二进制011表示选择了2、3集合
2.4、巧妙利用位运算实现代码
由于我们在进行操作时,是遍历二进制的十进制表示形式,如果我们将它转化为二进制再来枚举每一位,会非常麻烦。为了便于判断,因此采用先左移(<<)再进行位与运算符(&)
1进行左移几位之后,将变成000100…00的形式;又因为&运算是在同一位上同时为1才为1,所以进行&运算后可以知道此时左移的位数上的集合是否被选中。
代码实现:
#include <iostream>
#include <algorithm>
#include <string>
using namespace std;
int main()
{
int n;
cin >> n;
for(int i = 0; i < (1<<n); i++) //从0~2^n-1个状态
{
for(int j = 0; j < n; j++) //遍历二进制的每一位
{
if(i & (1 << j))//判断二进制第j位是否存在
{
//相关集合操作
}
}
}
return 0;
}
3、二进制枚举实现容斥定理
我们已经知道容斥定理的公式:
∣
∪
i
=
1
n
A
i
∣
=
∑
i
=
1
n
∣
A
i
∣
−
∑
i
,
j
:
1
≤
i
≤
j
≤
n
∣
A
i
∩
A
j
∣
+
∑
i
,
j
,
k
:
1
≤
i
≤
j
≤
k
≤
n
∣
A
i
∩
A
j
∩
A
k
∣
−
.
.
+
(
−
1
)
n
−
1
∣
A
1
∩
.
.
∩
A
n
∣
|\cup_{i=1}^{n}A_i|=\sum_{i=1}^{n}|A_i|-\sum_{i,j:1\leq i\leq j\leq n}|A_i\cap A_j|+\sum_{i,j,k:1\leq i\leq j\leq k\leq n}|A_i\cap A_j\cap A_k|-..+(-1)^{n-1}|A_1\cap ..\cap A_n|
∣∪i=1nAi∣=i=1∑n∣Ai∣−i,j:1≤i≤j≤n∑∣Ai∩Aj∣+i,j,k:1≤i≤j≤k≤n∑∣Ai∩Aj∩Ak∣−..+(−1)n−1∣A1∩..∩An∣
观察这个容斥的式子可以发现,每个元素同样有是否被选中的性质,因此可以用二进制枚举。
首先枚举N个元素的集合的所有子集:
for(int i=1;i<(1<<lena);i++)
然后枚举每一位
for (int j = 0; j < n; j++)
由于容斥定理的式子中有子集中元素的数量这一重要的因子,因此我们要利用变量 c n t cnt cnt记录此时该子集中元素的数量
if ((1 << j) & i)
{
tmp++;
lcm *= a[j];
}
枚举完每一位后,我们需要将答案统计起来
此时通过容斥定理的式子可以发现,当子集中元素个数为偶数时为-;当元素个数为奇数时为+。
if (tmp % 2 == 1)
ans += n / lcm; //奇数是加 偶数是减
else
ans -= n / lcm;
例如:求n以内2、 3、 5、 7、 11的倍数有多少个?
完整代码:
#include <iostream>
#include <stdio.h>
#include <math.h>
using namespace std;
int a[] = {2, 3, 5, 7, 11};
int main()
{
int n;
int lena = 5;
int ans = 0;
cin >> n;
for (int i = 1; i < (1 << lena); i++)
{
///每个数都有2种状态 ,在或不在 ,意思是2,3,5,7,11的倍数或不是,
///一共有2^5-1种可能
int tmp = 0;
int lcm = 1;
for (int j = 0; j < lena; j++)
{
if ((1 << j) & i)
{ ///选第j个b倍数子;与i的二进制的第j位比较,看是否为1,是则选中
tmp++; //标志变量计算i中1的个数,也就是倍数的个数
lcm *= a[j]; ///相乘
}
}
if (tmp % 2 == 1)
ans += n / lcm; //奇数是加 偶数是减
else
ans -= n / lcm;
}
printf("%d\n", ans);
}