容斥原理(二进制枚举实现)


1、什么是容斥原理?

​ 容斥原理是一种计数的方法:先不考虑重叠的情况,把包含于某内容中的所有对象的数目先计算出来,然后再把计数时重复计算的数目排斥出去,使得计算的结果既无遗漏又无重复,这种计数的方法称为容斥原理。

​ 注:也可以将容斥定理描述为:要计算几个集合并集的大小,我们要先将所有单个集合的大小计算出来,然后减去所有两个集合相交的部分,再加回所有三个集合相交的部分,再减去所有四个集合相交的部分………依此类推,一直计算到所有集合相交的部分。(可以理解为就是先把所有单个集合全加一遍然后再去重)

​ 为了直观的表达,可以采用韦恩图表示:

集合A与集合B

那么可以清晰的观察到:$A\cup B=A+B-A\cap B$

那么再增加一个集合C,结果又会怎么样喃?

集合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$

………..

通过不断地增加集合的数量,最终可以得出容斥原理的一个重要公式。

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);
}

文章作者: fatzard
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 fatzard !
评论
  目录
本站总访问量