[JOI2025 预选赛 R1H3] 不可兼或 的题解
记住只在没有思路时使用题解,不要从它复制粘贴代码。请尊重题目和题解的作者。
在解题之前提交题解的代码会导致封禁。
在解题之前提交题解的代码会导致封禁。
作者:
概述
本题要求统计 到
之间只能被
与
之一整除的整数的个数。核心解法是容斥:分别数出
的倍数与
的倍数,再减去同时被两者整除的数的两倍。
分析
核心观察
「只能被 与
之一整除」等价于落在「
的倍数」与「
的倍数」两个集合的对称差中。因此答案等于两个集合的大小之和减去两倍的集合交大小:既不被
整除也不被
整除的数不参与计算,而同时被两者整除的数一个都不该算,却在求和时被计了两次。
思路
在 中,
的倍数恰有
个,所以两个集合的大小之和为
同时被 与
整除,等价于被二者的最小公倍数
整除,这样的数有
个。它们在上述求和里被各计了一次、共两次,而正确答案中它们不应出现,于是答案要减去两倍交集大小:
其中最小公倍数取 ,实用写法是先除后乘,即
。
这里必须用最小公倍数而不是 :当
与
不互质时
严格大于
,此时
会偏小,减得不够。例如
、
、
时正确结果是
,而用
会得到
。
朴素做法是从 枚举到
逐一判断,时间为
。本题
时两种做法都能通过,但公式法只需一次最大公约数计算,可以承受远大于
的
。
具体示例
样例 中
、
、
,两者互质,故
。
| 部分 | 计算 | 结果 |
|---|---|---|
| 同时是两个数的倍数 |
答案为 ,对应
三个数:其中
被两个集合各计一次后恰好抵消,而
与
不属于任何集合,与样例一致。
样例 中
、
、
,
,答案为
。
算法步骤
- 读入
N、A、B。 - 令
g为A与B的最大公约数,令L为A / g * B(先除后乘),即二者的最小公倍数。 - 令
ans为N整除A的商与N整除B的商之和,再减去N整除L的商的两倍。 - 输出
ans。
复杂度分析
- 时间:
,由求最大公约数的欧几里得算法决定,其余运算均为
。
- 空间:
。
实现注意事项
- 交集是「同时被
与
整除」的数的个数,即最小公倍数的倍数个数,不能用
代替最小公倍数。
- 同时在两个集合中的数要减两倍,只减一倍会把这些数误算成「只能被之一整除」。
- 计算最小公倍数时先除后乘(
a / std::gcd(a, b) * b),避免中间乘积溢出。 - 输入固定为三行三个正整数,按顺序读入即可,无需额外处理分隔符。
- 答案恒非负;本题范围
时
int已足够,C++ 版本仍用long long以便范围扩大后直接复用。
源代码
#include <iostream>
#include <numeric>
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
long long n = 0, a = 0, b = 0;
std::cin >> n >> a >> b;
const long long g = std::gcd(a, b);
const long long l = a / g * b; // 先除后乘,避免中间结果溢出
// 被 A 或 B 整除的个数,减去同时被两者整除的个数(每个数被计了两次)
std::cout << n / a + n / b - 2 * (n / l) << '\n';
return 0;
}
import sys
from math import gcd
def main():
n, a, b = map(int, sys.stdin.buffer.read().split())
l = a // gcd(a, b) * b
print(n // a + n // b - 2 * (n // l))
if __name__ == "__main__":
main()
评论