Acwing 容斥原理

容斥原理

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

以S1,S2,S3三个集合为例,求出三个集合中包含的元素个数,可以通过韦恩图得到S1∪S2∪S3 = S1+S2+S3- S1∩S2 - S1∩S3 - S2∩S3 + S1∩S2∩S3。通过数学归纳法可以证明,对于求n个集合S1,S2,…,Sn集合中包含的元素个数,可以通过下面的公式来计算(注意正负号交替):
∣ S 1 ∪ S 2 ⋯ ∪ S m ∣ = ∑ i ∣ S i ∣ − ∑ i , j ∣ S i ∩ S j ∣ + ∑ i , j , k ∣ S i ∩ S j ∩ S k ∣ . . . . + ( − 1 ) n − 1 ∑ ∣ S 1 ∩ S 2 ∩ S 3 . . . ∩ S m ∣ . . . . . . . . ① | S_{1}\cup S_{2} \dots \cup S_{m} | = \sum_{i} | S_{i} | - \sum_{i,j} | S_{i} \cap S_{j} |+ \sum_{i,j,k} | S_{i}\cap S_{j} \cap S_{k} |....+(-1)^{n-1}\sum | S_{1}\cap S_{2} \cap S_{3}... \cap S_{m} |........① S1S2Sm=iSii,jSiSj+i,j,kSiSjSk∣....+(1)n1S1S2S3...Sm∣........①
计算总共的项数:利用组合数计算,每次从n个元素里面选i个进行交集计算,故总项数
C n 1 + C n 2 + ⋯ + C n n = 2 n C_{n}^{1} + C_{n}^{2} + \dots + C_{n}^{n} = 2 ^ {n} Cn1+Cn2++Cnn=2n
时间复杂度为 O ( 2 n ) O(2^n) O(2n)
接下来验证的一下上面①式中每个元素是不是只算了一次(具体证明略):
假设 x ∈ S 1 ∪ S 2 ⋯ ∪ S n ,存在于 k 个集合之中, 1 ≤ k ≤ n 那么 x 被计算的次数为 C k 1 − C k 2 + C k 3 − C k 4 + ⋯ + ( − 1 ) k − 1 C k k = 1 假设x\in S_{1} \cup S_{2} \dots \cup S_{n},存在于k个集合之中,1\le k\le n\\那么x被计算的次数为C_{k}^{1} - C_{k}^{2}+C_{k}^{3}-C_{k}^{4}+ \dots + (-1)^{k-1}C_{k}^{k}=1 假设xS1S2Sn,存在于k个集合之中,1kn那么x被计算的次数为Ck1Ck2+Ck3Ck4++(1)k1Ckk=1
Acwing 890.能被整除的数
在这里插入图片描述实现思路:记Si为1~n中能被pi整除的集合,根据容斥原理,所有数的个数为各个集合的并集,计算公式:
∣ S 1 ∪ S 2 ⋯ ∪ S m ∣ = ∑ i ∣ S i ∣ − ∑ i , j ∣ S i ∩ S j ∣ + ∑ i , j , k ∣ S i ∩ S j ∩ S k ∣ . . . . + ( − 1 ) n − 1 ∑ ∣ S 1 ∩ S 2 ∩ S 3 . . . ∩ S m ∣ | S_{1}\cup S_{2} \dots \cup S_{m} | = \sum_{i} | S_{i} | - \sum_{i,j} | S_{i} \cap S_{j} |+ \sum_{i,j,k} | S_{i}\cap S_{j} \cap S_{k} |....+(-1)^{n-1}\sum | S_{1}\cap S_{2} \cap S_{3}... \cap S_{m} | S1S2Sm=iSii,jSiSj+i,j,kSiSjSk∣....+(1)n1S1S2S3...Sm

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

  • 对于每个集合Si实际上并不需要知道含有哪些元素,只需要知道各个集合中元素个数,对于单个集合Si中元素个数就是对应质数pi的倍数个数(1~n范围内),计算公式为
    ∣ S i ∣ = n p i ,下取整 |S_i|=\frac{n}{p_i},下取整 Si=pin,下取整

  • 对于任意个集合交集中元素的个数:每个质数pi对应一个集合Si,那么
    ∣ S i ∩ S j ∣ = n p i ∗ p j ,下取整,即交集就是 p i 和 p j 的公倍数的个数 |S_i \cap S_j|=\frac{n}{p_i*p_j},下取整,即交集就是p_i和p_j的公倍数的个数 SiSj=pipjn,下取整,即交集就是pipj的公倍数的个数

  • 表示每个集合的状态(即选中几个集合的交集,)m个质数,需要m个二进制位表示,共 2 m − 1 2^m-1 2m1种情况(至少选中一个集合),个数前面的符号为(-1)^(n-1)。以m = 4为例,所以需要4个二进制位来表示每一个集合选中与不选的状态,若此时为1011,表示集合S1∩S3∩S4中元素的个数,同时集合个数为3,前面的符号为(-1)^(3-1)=1,即
    ∣ S 1 ∩ S 3 ∩ S 4 ∣ = ( − 1 ) 3 − 1 n p 1 ∗ p 3 ∗ p 4 |S_1 \cap S_3 \cap S_4|=(-1)^{3-1}\frac{n}{p_1*p_3*p_4} S1S3S4=(1)31p1p3p4n怎么取到一个数的每一个二进制位:使用位运算(第一章), 数i的第j位是否为1:i >> j & 1

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

样例分析:
在这里插入图片描述
具体实现代码(详解版):

#include <iostream>

using namespace std;

typedef long long LL;
const int N = 20;
int p[N]; // 存储质数p[i]
int n, m; 

int main() {
    cin >> n >> m; 
    for (int i = 0; i < m; i++) cin >> p[i]; // 输入 m 个质数并存入数组 p 中
    
    int res = 0; // 结果初始化为 0
    // 枚举所有质数的子集状态,状态用二进制表示,从 1 到 (1 << m) - 1
    // 即从000...01到111...11,其中每一位表示是否选中对应的质数
    for (int i = 1; i < 1 << m; i++) {
        int t = 1, s = 0; // t 表示当前质数子集的乘积,s 表示选中的质数个数
        
        // 枚举每一个质数,判断它是否在当前子集中
        for (int j = 0; j < m; j++) {
            // 判断第 j 个质数是否被选中(通过位运算检查第 j 位是否为 1)
            if (i >> j & 1) {
                // 检查当前乘积是否超过 n,若超过则停止计算该子集
                if ((LL)t * p[j] > n) {
                    t = -1; // 标记当前子集不可行
                    break;
                }
                s++; // 记录当前子集中质数的个数
                t = (LL)t * p[j]; // 更新子集的质数乘积
            }
        }
        // 如果当前子集不可行(乘积超出范围),则跳过
        if (t == -1) continue;
        
        // 根据子集质数个数 s 的奇偶性更新结果:
        // 如果质数个数为奇数,则加上 n / t;如果为偶数,则减去 n / t
        if (s % 2) res += n / t; // 奇数个质数,容斥原理加上这个集合的贡献
        else res -= n / t; // 偶数个质数,容斥原理减去这个集合的贡献
    }
    
    cout << res << endl;
    
    return 0;
}


这道题利用了容斥原理,核心思想是通过枚举质数子集并计算其整除数量,来处理多个集合的并集大小问题。通过容斥加减,避免了重复计数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值