使用容斥原理将多边形的几何滚动转化为在直线上统计公倍数节点的问题,极简且高效。
OJ: roj
题目 ID: 10013
难度:提高
标签:数学数论容斥原理
创建: 2026-10-10 01:42
更新: 2026-10-10 01:42
形式化题目
给定两个正多边形:多边形 A 边长为
多组测试数据。
数据范围:
正解
思路
这是一道经典的数学模型转化题。
首先,多边形 A 究竟什么时候才算作“回到原位置”?
由于正多边形具有完全的旋转对称性(所有的边和角完全等价),题目中的“回到原位置”并不要求 A 必须是某个特定的端点贴合原位,只要 A 的任意一条边整条贴合在 B 的起始边上,它的状态就和最开始一模一样。因此,变量
我们可以把 A 在 B 外围滚动的过程拉直:相当于把 B 的边界拉直成一条总长度为
在这段总长度为
- A 走完了一条完整的边长
,前端点落下进行新的旋转。对应的事件位置为 中所有 的倍数。 - A 在延伸时,碰到了 B 的凸角(因为 B 有拐角,A 被迫在此截断并改变旋转中心)。对应的事件位置为
中所有 的倍数。
若 A 的端点恰好和 B 的凸角重合(即该位置既是
因此,旋转次数等于
这便是最终的数学解。
代码
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-09 22:30
# update_at: 2026-10-10 00:05
import math
import sys
def solve() -> None:
"""多组数据读到 EOF,每组 A M B N,输出 A 绕 B 滚动回原位所需的旋转次数。
把 B 的边界拉直成总长 N*B 的线段(每隔 B 有一个拐角),A 每走完一条边 A
旋转一次、每碰到一个拐角也多旋转一次,两时刻重合只算一次
⇒ 答案 = (0, X] 内「A 的倍数或 B 的倍数」的个数,X = lcm(A, N*B)。
"""
data = iter(map(int, sys.stdin.buffer.read().split()))
out: list[str] = []
# 四份引用共用同一个迭代器,每组正好吃掉 4 个;末尾不足一组则整组丢弃,
# 与官方 std.cpp 的 while (cin >> a >> m >> b >> n) 行为一致。m 只消费不参与计算。
for a, m, b, n in zip(data, data, data, data):
nb = n * b
x = a // math.gcd(a, nb) * nb # x = lcm(a, n*b):回到原位走过的总路程
y = a // math.gcd(a, b) * b # y = lcm(a, b)
ans = x // a + x // y * (y // b - 1) # 容斥:A 的倍数 ∪ B 的倍数 的个数
out.append(str(ans))
if out:
print('\n'.join(out))
if __name__ == '__main__':
solve()/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-09 22:30
* update_at: 2026-10-10 00:05
*/
// main.cpp:卡片。
// 把 B 的边界拉直成一条总长 N*B 的线段,每隔 B 有一个向外的拐角。
// Alice 每走完一条边长 A 要旋转一次,碰到 B 的拐角也要多旋转一次;
// 两种时刻重合时只算一次 ⇒ 旋转次数 = (0, X] 内「A 的倍数或 B 的倍数」的个数,
// 其中回到原位所需的总路程 X = lcm(A, N*B)。(由正多边形的旋转对称性,
// 只要 A 的整条边重新贴回起始边就算回到原位,故 A 的边数 M 不影响答案。)
// 容斥:ans = X/A + X/B - X/lcm(A,B)。这里把后两项合并成 (X/lcm(A,B))*(lcm(A,B)/B - 1)。
// 多组测试数据,while 读到 EOF,每组输出一行。
#include <iostream>
#include <numeric>
using namespace std;
typedef long long ll;
void solve() {
ll a, m, b, n; // m 只读入、不参与计算:正多边形对称,A 的边数 M 与答案无关
while (cin >> a >> m >> b >> n) {
ll nb = n * b;
ll x = a / std::gcd(a, nb) * nb; // x = lcm(a, n*b),先除后乘避免中间乘积溢出
ll c1 = x / a; // (0, x] 中 A 的倍数个数
ll y = a / std::gcd(a, b) * b; // y = lcm(a, b)
ll c2 = x / y * (y / b - 1); // (0, x] 中 B 的倍数个数去掉与 A 的倍数重合的部分
cout << c1 + c2 << "\n";
}
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
solve();
return 0;
}复杂度
- 时间复杂度:单次询问的复杂度取决于求最大公约数
gcd的消耗,即。对于多组查询均能轻松通过。 - 空间复杂度:
。
总结
本题的验证依赖与官方参考代码(std.cpp)及异源程序的交叉验证。基于详细的大规模对拍,我们得出以下结论:
- 核心算法验证与等价性
容斥公式
Ans = X/A + X/B - X/lcm(A, B)经过组恒等式检验(在 的组合内)与官方 c1 + c2分块算法完全等效,分歧为 0。 - 多测到 EOF 是承重约定
尽管题面描述为“一行,四个整数”,但实际为多组数据读入直到 EOF。若只读一组(变异
V1a),在所有组穷举测试中将失败 组,样例也会在第二组直接错掉。最小复现: printf '2 4 3 4\n3 4 4 4\n' | ./main应当输出8 24,改成单次读入则会丢失第二个答案。 - M 变量完全不参与计算
对拍证明:固定
,改变 ( 组测试)输出恒定不变。且让 参与计算的三个变异公式(C8、C9、C10)在包含样例 1 与样例 3 的测试中全部直接错误。这从数据表现和代数逻辑上均坐实了“ 无效”并非 bug,而是题面本身的迷惑项。 - 溢出防范(V1b 不承重 vs V1f 承重)
V1b(先乘后除计算) 不承重:因为 ,完全在 long long的安全范围内(),即便直接写 a * n * b / gcd也不会溢出(穷举 0 失败、50万压测 0 失败),不必宣称先除后乘对计算是防溢出的必要操作。 V1f(先乘后除计算) 真溢出:若将 c2计算改为c2 = x * (y / b - 1) / y,则在边界、大数以及 50 万压测中产生严重失败。原因是中间量可达(80 bit),必定溢出 long long。最小复现输入:999983 3 999984 1000将因溢出输出错误值1003808947,而正确结果为1999966000。
- 候选策略(V8)区分度
不包含容斥项的变异(C2, C3, C4, C7)在测试中均无法被区分(同等失败 20736 次),这意味着题面数据规模未充分分层。而带有
的 C8/C9/C10 策略直接被样例排除。 - 证据强度声明
由于本题无官方
.in/.out真实测试数据文件,验证属于“四方互相一致”(main.cpp、main.py、官方std.cpp、无公式推导的纯事件步进模拟和集合统计的 Python 脚本brute_set.py)。 测试规模包括:组穷举(变量 )、 组结构与边界测试、 组百万级大数随机、 组压力测试,四方输出完全一致。 - T9 规范化检查项
- 数据范围约束:
, 且 。 - 极值边界:注意
最大为 ,中间步骤求得的 最大在 左右。 - 变量类型:必须使用 64 位整型(
long long)。 - 多测:隐藏条件,必须以 EOF 为终止判定。
- 输出格式:无多余空格要求。
- 数据范围约束: