题解: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
【核心思想】
-
问题分析:给定整数 \(n\) 和 \(m\) 个不同的质数 \(p_1, p_2, \ldots, p_m\),求 \(1 \sim n\) 中能被这些质数中至少一个整除的整数个数。若直接用并集公式 \(|A_1 \cup A_2 \cup \ldots \cup A_m|\),需要处理大量交集项,而容斥原理正是解决多集合并集计数问题的经典方法。
-
算法选择:
- 容斥原理(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\),即能被这些质数乘积整除的数的个数
-
关键步骤:
- 读取输入:\(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即为能被至少一个质数整除的数的个数
-
时间/空间复杂度:
- 时间复杂度:\(O(2^m \cdot m)\)。共枚举 \(2^m - 1\) 个非空子集,每个子集计算乘积需要遍历 \(m\) 个质数
- 空间复杂度:\(O(m)\),仅存储质数数组和少量变量
-
容斥原理与二进制枚举的核心思想:
- 容斥原理的本质:通过交替加减交集大小来消除重复计数。奇数个集合的交集被加,偶数个集合的交集被减,最终恰好每个元素被计数一次
- 二进制枚举的技巧:用位运算
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
浙公网安备 33010602011771号