1、什么是容斥原理?
容斥原理是一种计数的方法:先不考虑重叠的情况,把包含于某内容中的所有对象的数目先计算出来,然后再把计数时重复计算的数目排斥出去,使得计算的结果既无遗漏又无重复,这种计数的方法称为容斥原理。
注:也可以将容斥定理描述为:要计算几个集合并集的大小,我们要先将所有单个集合的大小计算出来,然后减去所有两个集合相交的部分,再加回所有三个集合相交的部分,再减去所有四个集合相交的部分………依此类推,一直计算到所有集合相交的部分。(可以理解为就是先把所有单个集合全加一遍然后再去重)
为了直观的表达,可以采用韦恩图表示:
那么可以清晰的观察到:$A\cup B=A+B-A\cap B$
那么再增加一个集合C,结果又会怎么样喃?
同理我们可以得出以下结论:
$A\cup B\cup C=A+B+C-A\cap B-A\cap C-C\cap B +A\cup B\cup C$
………..
通过不断地增加集合的数量,最终可以得出容斥原理的一个重要公式。
1.1、容斥原理公式
2、二进制枚举
2.1、前提定理:
含有N个元素的集合的一切子集的个数为$2^n$
2.2、如何枚举出N个元素的集合的所有子集?
利用二进制的特性,由于二进制是01字符串,故我们可以用二进制数的每一位表示某个集合是否被选中(1表示该集合被选中,0表示该集合没有被选中)
如以下的例子:
集合编号 | 1 | 2 | 3 |
---|---|---|---|
二进制 | 1 | 0 | 1 |
状态 | 选 | 不选 | 选 |
2.3、原理
由于我们已经知道$n$个元素的集合的子集一共有$2^n$个,而长度为n的二进制数表示的范围为$[0,2^{n}-1]$,在这个区间上正好有$2^n$个数,所以这每个数也正好可以表示每个集合。
例如:二进制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、二进制枚举实现容斥定理
我们已经知道容斥定理的公式:
观察这个容斥的式子可以发现,每个元素同样有是否被选中的性质,因此可以用二进制枚举。
首先枚举N个元素的集合的所有子集:
for(int i=1;i<(1<<lena);i++)
然后枚举每一位
for (int j = 0; j < n; j++)
由于容斥定理的式子中有子集中元素的数量这一重要的因子,因此我们要利用变量$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);
}