枚举前、后齿轮组合:3 倍约束用整数乘法定向剪枝,把传动比按 lcm 标度改成整数后精确比方差,并列按字典序。
OJ: roj
题目 ID: 2068
难度:普及+/提高-
标签:搜索枚举剪枝数学
创建: 2026-10-01 06:12
更新: 2026-10-07 13:50
形式化题目
给定
每组方案产生
- 3 倍约束:最大传动比至少是最小传动比的 3 倍;
- 方差最小:把
个传动比排序为 ,记相邻差 (共 个),使 最小。
并列规则:方差相同时,前齿轮组合字典序最小的优先;仍相同,后齿轮组合字典序最小。输出两行:前、后齿轮齿数各按升序。
样例:39 53、后齿轮 12 13 15 23 27;它排序后的 10 个传动比见「正解 · 思路」,相邻差方差约
正解
思路
这是一道"先读题、后编码"的题。 剥掉奶牛和别墅的故事,任务就是把两组数、
第一步:确定枚举框架与字典序。 前齿轮组合至多
- 合法方案必须通过 3 倍约束,枚举后齿轮组合时可以先过这道闸再算方差;
itertools.combinations按升序枚举,天然产生字典序。只要只在"分数严格更小"时更新答案,第一次拿到某个分数值的方案就是字典序最小的,自动满足并列规则。
流程如下:
flowchart LR
A[按字典序枚举前齿轮组合] --> B[对每个前齿轮组合<br/>枚举后齿轮组合]
B --> C{3 倍约束<br/>f_max*r_max >= 3*f_min*r_min}
C -- "否: 剪枝" --> B
C -- 是 --> D[排序 n 个传动比]
D --> E[整数方差分数<br/>m*sum(d^2) - 首尾差^2]
E --> F{分数严格小于<br/>当前最优?}
F -- 否 --> B
F -- 是 --> G[更新答案并回到 B]
G --> B
图中只有两处分支:3 倍约束剪枝(
第二步:3 倍约束 = 一次整数乘法。 传动比的最大值是
不等号两边交叉相乘,一次
时枚举后齿轮组合,先查上式、不过再算方差; 时(真实数据最重的 cowcycle7 正是 ),设两个后齿轮为 ,合法条件即 。对每个 直接从该下界开始枚举 ,一个非法对都不会访问,把 个齿对压到约 200–340 个。
第三步:方差比较变成纯整数运算。 方差是相邻差的方差。记
两边乘
两个观察让这个表达式可以直接用整数算:
- 望远镜求和:
,只需首尾两个传动比; - 公共标度:令
, ,则每个传动比 等于整数 除以 。于是每个 ,代入上式后 也是常数,最终只需比较整数
排序传动比其实是在排序若干个整数 fractions.Fraction 快一个数量级,同时绝对精确,不会因浮点噪声把"并列取字典序最小"的规则破坏。
第四步:看懂"方差最小"长什么样。 对样例答案前 39 53、后 12 13 15 23 27,把 10 个传动比排序并列出相邻差:
| 排序后的传动比 | 近似值 | 相邻差 |
|---|---|---|
| 39/27 | 1.4444 | — |
| 39/23 | 1.6957 | 0.2512 |
| 53/27 | 1.9630 | 0.2673 |
| 53/23 | 2.3043 | 0.3414 |
| 39/15 | 2.6000 | 0.2957 |
| 39/13 | 3.0000 | 0.4000 |
| 39/12 | 3.2500 | 0.2500 |
| 53/15 | 3.5333 | 0.2833 |
| 53/13 | 4.0769 | 0.5436 |
| 53/12 | 4.4167 | 0.3397 |
观察两点:最大/最小传动比为
代码
Python 版:
#!/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-01 06:19
# update_at: 2026-10-01 06:42
import sys
from itertools import combinations
from math import lcm
# 传动比 a/b 记为整数 a * W[b](W[b] = L//b,L 是 5..40 的公倍数),
# 这样比值、相邻差、方差全在整数域精确计算,不存在浮点误差。
L = lcm(*range(5, 41))
W = [0] * 5 + [L // b for b in range(5, 41)] # W[b] = L//b,下标 5..40 有效
def rank_score(nums: list[int], m: int) -> int:
"""由 m+1 个传动比整数(m 个相邻差)算“比较用方差分数”:m*Σ(相邻差)² - (首尾差)²。
真实方差 = (m·Σd² - (Σd)²) / (L²·m²),L、m 对每个方案相同,
所以只需比较这个整数:越小方差越小。
"""
nums.sort()
first, last = nums[0], nums[-1]
span = last - first # Σd = 最大比 - 最小比(望远镜求和)
sq = sum((b - a) ** 2 for a, b in zip(nums, nums[1:])) # Σ d²
return m * sq - span * span
def best_combo(f1: int, f2: int, r1: int, r2: int, F: int, R: int) -> tuple[tuple[int, ...], tuple[int, ...]]:
"""在所有合法方案里找方差分数最小的 (前齿轮, 后齿轮);分数相同取字典序最小者在前。"""
m = F * R - 1 # 相邻差的个数
best_score: int | None = None # 当前最小方差分数
best: tuple[tuple[int, ...], tuple[int, ...]] | None = None
for front in combinations(range(f1, f2 + 1), F): # 字典序枚举前齿轮组合
f_min, f_max = front[0], front[-1]
# scaled[b] = 这个前齿轮组合配后齿轮 b 时的 F 个传动比整数(内部升序)
scaled = [[a * W[b] for a in front] for b in range(r1, r2 + 1)]
if R == 2:
# 只需后齿轮较小者 x 与较大者 y:
# 3 倍约束 f_max/y >= 3*f_min/x ⇔ y*f_max >= 3*x*f_min,
# 直接算出每个 x 的合法 y 下界,跳过全部不合法对。
for x in range(r1, r2):
lo = (3 * f_min * x + f_max - 1) // f_max # y 的最小合法值(向上取整)
if lo < x + 1:
lo = x + 1
for y in range(lo, r2 + 1):
score = rank_score(scaled[x - r1] + scaled[y - r1], m)
# 只替换严格更小者:枚举顺序即字典序,天然满足并列时取最小组合
if best_score is None or score < best_score:
best_score, best = score, (front, (x, y))
else:
for rear in combinations(range(r1, r2 + 1), R):
if f_max * rear[-1] < 3 * f_min * rear[0]: # 最大比不足最小比 3 倍
continue
nums = [v for b in rear for v in scaled[b - r1]]
score = rank_score(nums, m)
if best_score is None or score < best_score:
best_score, best = score, (front, rear)
return best # 题面保证至少一组合法解
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
F, R = next(data), next(data)
f1, f2, r1, r2 = next(data), next(data), next(data), next(data)
best = best_combo(f1, f2, r1, r2, F, R)
print(' '.join(map(str, best[0])))
print(' '.join(map(str, best[1])))
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* 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-06 10:58
* update_at: 2026-10-06 10:58
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
// 传动比 a/b 用整数 a * W[b] 表示(W[b] = L / b,L 是 5..40 的公倍数),
// 这样传动比、相邻差、方差分数全部是精确整数运算,没有浮点误差。
const ll L_LCM = 5342931457063200LL; // lcm(5,6,...,40)
ll W[45]; // W[b] = L_LCM / b,下标 5..40 有效
ll F, R; // 前、后齿轮个数
ll f1, f2, r1, r2; // 前、后齿轮齿数的可选区间
ll front_g[10]; // 当前枚举的前齿轮组合(升序)
ll rear_g[11]; // 当前枚举的后齿轮组合(升序)
ll f_min, f_max; // 当前前齿轮组合的最小 / 最大齿数
ll nums[55]; // 当前方案的 F*R 个传动比整数,排序后求相邻差
ll ans_front[10]; // 答案:前齿轮组合
ll ans_rear[11]; // 答案:后齿轮组合
bool has_ans = false; // 是否已找到合法方案
__int128 best_score; // 当前最优的方差分数(可能超过 ll,用 __int128 精确保存)
// 用当前 front_g / rear_g 组合计算“比较用方差分数”:
// m*Σ(相邻差)^2 - (首尾差)^2,其中 m = F*R - 1 是相邻差的个数。
// 真实方差 = 分数 / (L^2 * m^2),L、m 对每个方案相同,所以分数越小方差越小。
// 大小估计:span <= 80*W[5] 约 8.6e16,m <= 49,
// m*Σd^2 <= 49*49*7.4e33 约 1.8e37,在 __int128 范围内。
__int128 calc_score() {
ll n = F * R;
ll idx = 0;
for (ll j = 0; j < R; j++) {
for (ll i = 0; i < F; i++) {
nums[idx] = front_g[i] * W[rear_g[j]];
idx++;
}
}
sort(nums, nums + n);
ll m = n - 1; // 相邻差个数
__int128 sq = 0; // Σ(相邻差)^2
for (ll k = 0; k + 1 < n; k++) {
ll d = nums[k + 1] - nums[k];
sq += (__int128)d * d;
}
__int128 span = nums[n - 1] - nums[0]; // Σd = 最大比 - 最小比(望远镜求和)
return (__int128)m * sq - span * span;
}
// 分数严格更小才更新:前后齿轮都是升序枚举,天然保证并列时取字典序最小的组合。
void try_update() {
__int128 score = calc_score();
if (has_ans && score >= best_score) return;
has_ans = true;
best_score = score;
for (ll i = 0; i < F; i++) ans_front[i] = front_g[i];
for (ll j = 0; j < R; j++) ans_rear[j] = rear_g[j];
}
// 枚举后齿轮组合:这一层在后齿轮区间里选第 dep 个齿数(保持升序)。
// 剪枝 1:第一个(最小)齿数 b 若连配上最大后齿 r2 都不满足 3 倍约束,更大的 b 只会更差,直接断开。
// 剪枝 2:选最后一个齿数时,必须 >= ceil(3*f_min*rear_g[0]/f_max) 才可能合法,从下界起选,
// 对 R=2 相当于一个非法齿对都不会被访问。
void dfs_rear(ll dep, ll start) {
if (dep == R) {
// 完整后齿轮组合已生成:3 倍约束 最大比 >= 3*最小比
// 即 f_max/rear_min >= 3*f_min/rear_max,交叉相乘成整数比较
if (f_max * rear_g[R - 1] >= 3 * f_min * rear_g[0]) {
try_update();
}
return;
}
for (ll b = start; b <= r2; b++) {
if (dep == 0 && f_max * r2 < 3 * f_min * b) break; // 最小齿数再增大只会更不合法
rear_g[dep] = b;
if (dep == R - 1) {
ll lo = (3 * f_min * rear_g[0] + f_max - 1) / f_max; // 最后一个齿数的合法下界(向上取整)
if (b < lo) continue;
}
dfs_rear(dep + 1, b + 1);
}
}
// 枚举前齿轮组合:这一层在前齿轮区间里选第 dep 个齿数(保持升序)。
// 升序枚举即字典序,并列规则由“分数严格更小才更新”自动满足。
void dfs_front(ll dep, ll start) {
if (dep == F) {
f_min = front_g[0];
f_max = front_g[F - 1];
dfs_rear(0, r1);
return;
}
for (ll a = start; a <= f2; a++) {
front_g[dep] = a;
dfs_front(dep + 1, a + 1);
}
}
int main() {
// 预处理 W[b] = L / b,把分数 a/b 变成整数 a * W[b]
for (ll b = 5; b <= 40; b++) {
W[b] = L_LCM / b;
}
cin >> F >> R;
cin >> f1 >> f2 >> r1 >> r2;
dfs_front(0, f1);
// 输出前、后齿轮齿数(升序)
for (ll i = 0; i < F; i++) {
cout << ans_front[i] << " \n"[i == F - 1];
}
for (ll j = 0; j < R; j++) {
cout << ans_rear[j] << " \n"[j == R - 1];
}
return 0;
}复杂度
设
与标准 C++ 枚举解法同阶;
真实数据验证:8 组测例全部通过,最重的 cowcycle7(
总结
- 先翻译、再编码:把"传动比、3 倍约束、相邻差方差、字典序并列"逐条变成数学条件后,本题是一个干净的枚举 + 剪枝 + 精确比较问题。
- 整数化三件套:约束用整数乘法判、传动比用 lcm 标度变整数、方差只用整数分数
比较,全程零浮点。 - 并列规则白嫖枚举顺序:组合升序枚举 + 只在严格更小时更新,字典序要求自动满足。
- 剪枝按最重数据特化:
时直接从 3 倍约束解出 的下界,一个非法对都不访问。