(四)容斥原理

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

以S1,S2,S3三个集合为例,求出三个集合中包含的元素个数,可以通过韦恩图得到S1∪S2∪S3 = S1+S2+S3- S1∩S2 - S1∩S3 - S2∩S3 + S1∩S2∩S3。通过数学归纳法可以证明,对于求n个集合S1,S2,…,Sn集合中包含的元素个数,可以通过下面的公式来计算(注意正负号交替):

计算总共的项数:利用组合数计算,每次从n个元素里面选i个进行交集计算,故总项数

时间复杂度为O(2^n)

接下来验证的一下上面①式中每个元素是不是只算了一次(具体证明略):

AcWing 890.能被整除的数

**实现思路:**记Si为1~n中能被pi整除的集合,根据容斥原理,所有数的个数为各个集合的并集,计算公式

  • 每个集合Si就对应能被质数pi整除的数

  • 对于每个集合Si实际上并不需要知道含有哪些元素,只需要知道各个集合中元素个数,对于单个集合Si中元素个数就是对应质数pi的倍数个数(1~n范围内),计算公式为

  • 对于任意个集合交集中元素的个数:每个质数pi对应一个集合Si,那么

  • 表示每个集合的状态(即选中几个集合的交集,)m个质数,需要m个二进制位表示,共2^m-1种情况(至少选中一个集合),个数前面的符号为(-1)^(n-1)。以m = 4为例,所以需要4个二进制位来表示每一个集合选中与不选的状态,若此时为1011,表示集合S1∩S3∩S4中元素的个数,同时集合个数为3,前面的符号为(-1)^(3-1)=1,即

    怎么取到一个数的每一个二进制位:使用位运算(第一章), 数i的第j位是否为1:i>>m & 1

    注:用二进制表示状态的小技巧非常常用,后面的状态压缩DP也用到了这个技巧,因此一定要掌握

#include <iostream>
using namespace std;
 
typedef long long LL;
const int N=20;
int p[N];//存储质数pi
int n,m;
 
int mian(){
    cin>>n>>m;
    for(int i=0;i<m;i++) cin>>p[i];
    
    int res=0;
    //枚举各个状态 0000..1到1111..1(m个质数) 至少选中一个集合
    for(int i=1;i<1<<m;i++){//1<<m,即1右移m位 有2^m-1种集合交的情况
        int t=1,s=0;//t表示质数乘积,s表示二进制中1的个数即哪几个质数相乘
        
        //枚举状态的每一位
        for(int j=0;j<m;j++){
            //得到每一个二进制位:位运算
            if(i>>m & 1){
                if((LL)t*p[j]>n){//乘积大于n 元素个数自然为0 跳出该轮循环
                    t=-1;//标记一下
                    break;
                }
                s++;//选中的集合个数
                t=(LL)t*p[j];//乘积
            }
        }
        //得到乘积后
        if(t==-1) continue;
        
        //偶数个集合的交为负 奇数为正
        if(s%2) res+=n/t;
        else res-+n/t;
    }
    cout<<res<<endl;
    return 0;
}