容斥原理-acwing890.能被整除的数

学完只想说,实在是妙蛙~

容斥原理

容斥原理:

求并集

\[\left | S_{1} \cup S_{2} \cup S_{3} \right | = \left | S_{1} \right | + \left | S_{2} \right | + \left | S_{3} \right | - \left | S_{1} \cap S_{2} \right | - \left | S_{1} \cap S_{3} \right | - \left | S_{2} \cap S_{3} \right | + \left | S_{1} \cap S_{2} \cap S_{3}\right | \]

如果是两个集合相交的话

\[\left | S_{1} \cup S_{2}\right | = \left | S_{1} \right | + \left | S_{2} \right | - \left | S_{1} \cap S_{2} \right | \]

可以发现规律 加上一个的,减去两个相交的,加上三个相交的,减去四个相交的,加上五个相交的...

容斥原理时间复杂度

选一个集合的时间(\(S_{1}、S_{2}...\)=>\(C_{n}^{1}\))+选两个集合的时间(\(S_{1}\cap S_{2}\)=>\(C_{n}^{2}\))+选三个集合的时间(\(S_{1}\cap S_{2}\cap S_{3}\)=>\(C_{n}^{3}\))...

即:\(C_{n}^{1}+C_{n}^{2}+C_{n}^{3}+...+C_{n}^{n}\)

不妨添加一个\(C_{n}^{0}\) => \(C_{n}^{0} + C_{n}^{1}+C_{n}^{2}+C_{n}^{3}+...+C_{n}^{n} = 2^n\)(相当于从n个数中挑任意多个数出来,相当于每一个数挑或者不挑)

因此其时间复杂度为\(O(2^n)\)

acwing890.能被整除的数

原题链接:https://www.acwing.com/problem/content/892/

样例解释

n = 10,\(p_1 = 2\),\(p_2 = 3\),求1~10中能满足整除\(p_1\)\(p_2\)的个数即2,3,4,6,8,9,10,共7个,即求能整除2的集合\(S_1\)和能整除3的集合\(S_2\)的并集

思路

\(S_i\)为1~n中能整除\(p_i\)的集合,答案即为所有集合的并集

然后用容斥原理求并集

并不需要求出每个集合具体是什么,我们可以求出每个集合的大小,然后根据容斥原理去做

集合\(S_i\)的大小怎么确定呢?
\(\left \lfloor \frac{n}{p_i} \right \rfloor\) 即为1~n中能整除\(p_i\)的元素的个数
即$\left | S_i \right | = \frac{n}{p_i} $

交集的大小如何确定?
因为所有的\(p_i\)都为质数,因此所有任意两数的乘积就是两数的最小公倍数,因此同理$\left | S_i \bigcap S_j \right |= \frac{n}{p_i * p_j} $

如何使用代码表示每一个集合的状态?
二进制枚举(有点状态压缩的味道),把某些集合选与不选的情况看做一个二进制数,然后枚举所有情况(枚举所有二进制数),m的大小就是集合数量的大小,就是二进制数位数的多少。
然后使用容斥原理。

比如\(m=4\),那么就有情况1101,即选中了集合\(S_1,S_2,S_4\),那么\(S_1 \bigcup S_2 \bigcup S_4\)的大小,即\(\frac{n}{p_1*p_2*p_4}\),选了3个而\((-1)^{3-1} = 1\),因此到这一步应该是res += \(\frac{n}{p_1*p_2*p_4}\)

ps:这种二进制枚举很常用!~

代码 O(\(m*2^m\))
#include<iostream>


using namespace std;

const int N = 20;

typedef long long LL;

int p[N];

int main()
{
    int n,m;
    cin >> n >> m;
    for(int i = 0; i < m; i ++) cin >> p[i]; // 将m个素数读进来
    
    int res = 0;
    for(int i = 1; i < 1 << m; i ++) // 二进制枚举每一种选某些集合不选某些集合的情况
    {
        int mul = 1,cnt = 0; // mul记录最小公倍数 cnt记录正负号
        for(int j = 0; j < m; j ++) // 枚举二进制数的每一位
        {
            if(i >> j & 1) // 该集合选中
            {
                if((LL)mul * p[j] > n) // p[j]的范围是1e9,多个p[j]相乘有可能爆int
                {
                    mul = -1;
                    break;
                }
                mul *= p[j];
                cnt ++;
            }
        }
        if(mul != -1)
        {
            if(cnt % 2) res += n/mul; // 奇数+,偶数- n/最小公倍数
            else res -= n/mul;
        }
    }
    
    cout << res << endl;
    
    return 0;
}
posted @ 2022-09-21 16:44  r涤生  阅读(88)  评论(0编辑  收藏  举报