题解:AcWing 890 能被整除的数

【题目来源】

AcWing:890 能被整除的数 - AcWing题库

【题目描述】

给定一个整数 \(n\)\(m\) 个不同的质数 \(p_1,p_2,\dots,p_m\)

请你求出 \(1\sim n\) 中能被 \(p_1,p_2,\dots,p_m\) 中的至少一个数整除的整数有多少个。

【输入】

第一行包含整数 \(n\)\(m\)

第二行包含 \(m\) 个质数。

【输出】

输出一个整数,表示满足条件的整数的个数。

【输入样例】

10 2
2 3

【输出样例】

7

【核心思想】

  1. 问题分析:给定整数 \(n\)\(m\) 个不同的质数 \(p_1, p_2, \ldots, p_m\),求 \(1 \sim n\) 中能被这些质数中至少一个整除的整数个数。若直接用并集公式 \(|A_1 \cup A_2 \cup \ldots \cup A_m|\),需要处理大量交集项,而容斥原理正是解决多集合并集计数问题的经典方法。

  2. 算法选择

    • 容斥原理(Inclusion-Exclusion Principle)\(|\bigcup_{i=1}^{m} A_i| = \sum_{i} |A_i| - \sum_{i<j} |A_i \cap A_j| + \sum_{i<j<k} |A_i \cap A_j \cap A_k| - \ldots + (-1)^{m+1} |A_1 \cap \ldots \cap A_m|\)
    • 二进制枚举(Subset Enumeration):用 \(m\) 位二进制数表示质数子集的选择状态,第 \(j\) 位为 \(1\) 表示选中 \(p_j\),共枚举 \(2^m - 1\) 个非空子集
    • 集合交集大小计算\(|A_{i_1} \cap A_{i_2} \cap \ldots \cap A_{i_k}| = \lfloor n / (p_{i_1} \cdot p_{i_2} \cdot \ldots \cdot p_{i_k}) \rfloor\),即能被这些质数乘积整除的数的个数
  3. 关键步骤

    • 读取输入\(n, m\)\(m\) 个质数 \(p[0..m-1]\)
    • 二进制枚举所有非空子集
      • 外层循环 \(i\)\(1\)\((1 << m) - 1\),每个 \(i\) 代表一个子集
      • 内层循环检查 \(i\) 的每一位:if (i >> j & 1) 表示选中第 \(j\) 个质数
    • 计算当前子集的贡献
      • cnt:记录子集中质数个数
      • t:子集中所有质数的乘积(即交集对应的最小公倍数,因质数互异故为乘积)
      • \(t > n\),该子集无贡献(没有数能被大于 \(n\) 的数整除),标记为无效
    • 容斥加减(根据子集大小的奇偶性):
      • cnt 为奇数:ans += n / t(加上交集大小)
      • cnt 为偶数:ans -= n / t(减去交集大小)
    • 输出答案ans 即为能被至少一个质数整除的数的个数
  4. 时间/空间复杂度

    • 时间复杂度:\(O(2^m \cdot m)\)。共枚举 \(2^m - 1\) 个非空子集,每个子集计算乘积需要遍历 \(m\) 个质数
    • 空间复杂度:\(O(m)\),仅存储质数数组和少量变量
  5. 容斥原理与二进制枚举的核心思想

    • 容斥原理的本质:通过交替加减交集大小来消除重复计数。奇数个集合的交集被加,偶数个集合的交集被减,最终恰好每个元素被计数一次
    • 二进制枚举的技巧:用位运算 i >> j & 1 判断第 \(j\) 个元素是否在子集中,将子集枚举转化为整数循环,代码简洁且效率高
    • 剪枝优化:当当前子集的质数乘积 \(t > n\) 时,该子集及所有包含该子集的超集都无贡献(因为 \(n/t = 0\)),可直接标记无效并跳出
    • 与莫比乌斯函数的联系:容斥原理本质上是对莫比乌斯函数 \(\mu\) 的应用,当元素为无平方因子数时,\(\mu\) 取值为 \((-1)^k\)\(k\) 为质因子个数),与容斥的"加奇减偶"一致
    • 适用于:多条件覆盖计数、集合的并/交计数、求能被若干数中至少一个整除的数的个数等问题

【解题思路】

【算法标签】

容斥原理

【代码详解】

#include <bits/stdc++.h>
using namespace std;

typedef long long LL; // 定义 LL 为 long long 类型
const int N = 20; // 定义常量 N,表示质数的最大数量
int p[N]; // p 数组存储质数
int n, m; // n 表示范围上限,m 表示质数的数量

int main()
{
    cin >> n >> m; // 输入范围上限 n 和质数的数量 m
    for (int i = 0; i < m; i++) cin >> p[i]; // 输入质数

    int ans = 0; // 初始化答案为 0
    for (int i = 1; i < 1 << m; i++) { // 遍历所有可能的质数组合(二进制枚举)
        int s = 0; // s 表示当前组合中质数的个数
        int t = 1; // t 表示当前组合中质数的乘积
        for (int j = 0; j < m; j++) { // 遍历每个质数
            if (i >> j & 1) { // 如果当前质数被选中
                if ((LL)t * p[j] > n) { // 如果乘积超过 n
                    t = -1; // 标记为无效组合
                    break; // 跳出循环
                } else {
                    t = t * p[j]; // 更新乘积
                    s++; // 增加质数个数
                }
            }
        }
        if (t != -1) { // 如果组合有效
            if (s % 2) ans = ans + n / t; // 如果质数个数为奇数,加上 n/t
            else ans = ans - n / t; // 如果质数个数为偶数,减去 n/t
        }
    }

    cout << ans << endl; // 输出答案
    return 0; // 程序结束
}
#include <bits/stdc++.h>
using namespace std;

#define int long long  // 使用long long防止溢出
const int N = 20;  // 最大质数个数

int n, m;        // n: 范围上限,m: 质数个数
int p[N];        // 存储质数

signed main()  // 因为使用了#define int long long,所以用signed main
{
    // 输入:计算1~n中不能被p[0..m-1]中任意一个整除的数的个数
    cin >> n >> m;

    for (int i = 0; i < m; i++)
    {
        cin >> p[i];  // 输入m个质数
    }

    int res = 0;  // 结果:能被至少一个质数整除的数的个数

    // 遍历所有非空子集(用二进制位表示,共2^m-1个子集)
    for (int i = 1; i < (1 << m); i++)  // i从1到(2^m-1)
    {
        int t = 1;   // 存储当前子集中所有质数的乘积
        int cnt = 0; // 记录当前子集中质数的个数

        // 检查i的每一位
        for (int j = 0; j < m; j++)
        {
            if (i >> j & 1)  // 如果第j位是1,表示包含第j个质数
            {
                cnt++;  // 质数个数加1

                // 如果乘积会超过n,标记为无效
                if (t * p[j] > n)
                {
                    t = -1;  // 标记无效
                    break;   // 退出内层循环
                }

                t *= p[j];  // 更新乘积
            }
        }

        if (t != -1)  // 如果当前子集有效
        {
            // 容斥原理:加奇减偶
            if (cnt % 2)  // 如果包含奇数个质数
            {
                res += n / t;  // 加上能被t整除的数的个数
            }
            else  // 如果包含偶数个质数
            {
                res -= n / t;  // 减去能被t整除的数的个数
            }
        }
    }

    // 输出:1~n中不能被任何给定质数整除的数的个数
    cout << n - res << endl;

    return 0;
}

【运行结果】

10 2
2 3
7
posted @ 2026-06-23 11:25  团爸讲算法  阅读(6)  评论(0)    收藏  举报