【ACM数论】容斥原理

容斥原理

1、什么是容斥原理?

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

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

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

在这里插入图片描述

那么可以清晰的观察到: A ∪ B = A + B − A ∩ B A\cup B=A+B-A\cap B AB=A+BAB

那么再增加一个集合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 ABC=A+B+CABACCB+ABC

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

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=1nAii,j:1ijnAiAj+i,j,k:1ijknAiAjAk..+(1)n1A1..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=0ncni=2n

2.2、如何枚举出N个元素的集合的所有子集?

利用二进制的特性,由于二进制是01字符串,故我们可以用二进制数的每一位表示某个集合是否被选中(1表示该集合被选中,0表示该集合没有被选中)

如以下的例子:

集合编号123
二进制101
状态不选
2.3、原理

由于我们已经知道 n n n个元素的集合的子集一共有 2 n 2^n 2n个,而长度为n的二进制数表示的范围为 [ 0 , 2 n − 1 ] [0,2^{n}-1] [0,2n1],在这个区间上正好有 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=1nAii,j:1ijnAiAj+i,j,k:1ijknAiAjAk..+(1)n1A1..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);
}
  • 29
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值