把最短时间拆成 A→X→Y→D 三段路线,X、Y 是两条传送带上的连续决策点;由距离函数的凸性对下车点、上车点嵌套实数三分搜索,O(log²(1/ε)) 求解。
OJ: roj
题目 ID: 1439
难度:普及+/提高-
标签:三分计算几何浮点数python
创建: 2026-09-30 10:23
更新: 2026-10-07 11:01
形式化题目
平面上有两条线段
样例(
先看路线形状:人只在两条竖直传送带附近活动,中间一大段是斜穿平面的直线飞行。整条路线恰好由"沿 AB 走一段 + 直线飞行 + 沿 CD 走一段"组成,其中下车点
正解
思路
第一步:最优路线一定可以写成三段。 在
三点理由:
- 中间一段以恒定速度
移动,两点之间显然直线最短; - 不会"离开传送带又回来":若某走法离开
兜一圈再回到 ,由三角不等式,把兜圈的那部分换成"直接沿 走"或"直接飞过缺口"(两者取快)不会更慢,逐段替换后每条传送带至多用一整段; - 传送带若比飞行还慢(比如
),最优解干脆整条不用——这正好对应 或 的端点取值,不需要特判。
于是决策只剩两个连续参数。令
"不用传送带直飞"也在这族路线里:取
第二步:固定下车点,上车点可以三分。 固定
关于
第三步:整体再对下车点三分。 外层想三分的函数是
一般的"凸函数取
两步合起来就是嵌套三分:外层三分下车点
若
下表展示样例中固定
| 轮次 | 收缩 | ||||||
|---|---|---|---|---|---|---|---|
| 1 | 0.0000 | 1.0000 | 0.3333 | 151.7950 | 0.6667 | 139.4321 | |
| 2 | 0.3333 | 1.0000 | 0.5556 | 142.3668 | 0.7778 | 137.5817 | |
| 3 | 0.5556 | 1.0000 | 0.7037 | 138.6998 | 0.8519 | 136.8982 | |
| 4 | 0.7037 | 1.0000 | 0.8025 | 137.3073 | 0.9012 | 136.6670 | |
| 5 | 0.8025 | 1.0000 | 0.8683 | 136.8019 | 0.9342 | 136.6067 |
每一轮都是右边的试探点更优,于是左端不断右移,最小值被夹在
顺带一提,样例里外层
代码
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-09-30 10:23
# update_at: 2026-09-30 10:34
import sys
from collections.abc import Callable
from math import hypot
Point = tuple[float, float]
A: Point = (0.0, 0.0) # 起点,也是 AB 的左端点
B: Point = (0.0, 0.0) # AB 的右端点
C: Point = (0.0, 0.0) # CD 的左端点
D: Point = (0.0, 0.0) # 终点,也是 CD 的右端点
P = 1.0 # AB 上的移动速度
Q = 1.0 # CD 上的移动速度
R = 1.0 # 平面上(不踩传送带)的移动速度
def dist(u: Point, v: Point) -> float:
"""两点间的欧氏距离。"""
return hypot(u[0] - v[0], u[1] - v[1])
def on_seg(seg: tuple[Point, Point], t: float) -> Point:
"""线段上参数为 t 的点:t=0 是起点,t=1 是终点。"""
(x1, y1), (x2, y2) = seg
return (x1 + t * (x2 - x1), y1 + t * (y2 - y1))
def belt_time(t: float, s: float) -> float:
"""走 A →X(t)→Y(s)→ D 的总时间:AB 段速度 P、中途飞行 R、CD 段速度 Q。"""
x = on_seg((A, B), t) # 在 AB 上的下车点
y = on_seg((C, D), s) # 在 CD 上的上车点
return dist(A, x) / P + dist(x, y) / R + dist(y, D) / Q
def ternary_min(f: Callable[[float], float], lo: float, hi: float) -> float:
"""对 [lo, hi] 上的单峰(凸)函数 f 做实数三分,返回最小值。"""
for _ in range(100):
m1 = lo + (hi - lo) / 3
m2 = hi - (hi - lo) / 3
if f(m1) < f(m2):
hi = m2
else:
lo = m1
return f((lo + hi) / 2)
def entry_time(t: float) -> float:
"""固定 AB 上的下车点 X(t),选好 CD 上的上车点后能达到的最短全程时间。"""
return ternary_min(lambda s: belt_time(t, s), 0.0, 1.0)
def minimum_time() -> float:
"""全程最短时间:外层三分下车点,内层三分上车点。"""
return ternary_min(entry_time, 0.0, 1.0)
def solve() -> None:
"""读入坐标与速度,输出 A 到 D 的最短时间,保留两位小数。"""
global A, B, C, D, P, Q, R
ax, ay, bx, by, cx, cy, dx, dy, p, q, r = map(float, sys.stdin.buffer.read().split())
A, B, C, D = (ax, ay), (bx, by), (cx, cy), (dx, dy)
P, Q, R = p, q, r
print(f"{minimum_time():.2f}")
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-05 01:08
* update_at: 2026-10-05 01:08
*/
// 最优路线一定是三段:A ->X(在AB上)-> Y(在CD上)-> D。
// 固定下车点 X 时,总时间关于上车点参数 s 是凸函数,可以内层三分;
// T(t,s) 联合凸,对 s 取最小后 F(t) 仍凸,外层再对下车点参数 t 三分。
// 嵌套实数三分,O(log^2(1/eps)) 求最小时间。
#include <cstdio>
#include <cmath>
typedef long long ll;
double ax, ay, bx, by; // 线段 AB 两端坐标(A 是起点)
double cx, cy, dx, dy; // 线段 CD 两端坐标(D 是终点)
double P, Q, R; // 分别是 AB 上、CD 上、平面上的移动速度
const int ROUND = 100; // 三分固定迭代轮数,(2/3)^100 远小于所需精度
// 两点间欧氏距离
double dist(double x1, double y1, double x2, double y2) {
return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}
// 走 A -> X(t) -> Y(s) -> D 的总时间
// t, s 分别是 X 在 AB 上、Y 在 CD 上的参数(0 是线段起点,1 是终点)
double total_time(double t, double s) {
double px = ax + t * (bx - ax); // 下车点 X 的坐标
double py = ay + t * (by - ay);
double qx = cx + s * (dx - cx); // 上车点 Y 的坐标
double qy = cy + s * (dy - cy);
return dist(ax, ay, px, py) / P
+ dist(px, py, qx, qy) / R
+ dist(qx, qy, dx, dy) / Q;
}
// 固定下车点参数 t,对上车点参数 s 三分求最短时间
double inner_min(double t) {
double lo = 0.0, hi = 1.0;
for (int i = 1; i <= ROUND; ++i) {
double m1 = lo + (hi - lo) / 3;
double m2 = hi - (hi - lo) / 3;
if (total_time(t, m1) < total_time(t, m2))
hi = m2;
else
lo = m1;
}
return total_time(t, (lo + hi) / 2);
}
// 外层对下车点参数 t 三分,求全程最短时间
double outer_min() {
double lo = 0.0, hi = 1.0;
for (int i = 1; i <= ROUND; ++i) {
double m1 = lo + (hi - lo) / 3;
double m2 = hi - (hi - lo) / 3;
if (inner_min(m1) < inner_min(m2))
hi = m2;
else
lo = m1;
}
return inner_min((lo + hi) / 2);
}
int main() {
scanf("%lf %lf %lf %lf", &ax, &ay, &bx, &by);
scanf("%lf %lf %lf %lf", &cx, &cy, &dx, &dy);
scanf("%lf %lf %lf", &P, &Q, &R);
printf("%.2f\n", outer_min());
return 0;
}复杂度
- 时间复杂度:内层 100 轮、每轮 2 次求值,外层再套 100 轮,总共约
次 求值,即 ( 为目标精度),与坐标大小无关。Python 实测每组数据约 秒。 - 空间复杂度:
,只保存 8 个坐标、3 个速度和三分的区间端点。
总结
- 建模核心是把自由走法压缩成三段式路线
:直线段由"恒速走直线最短"保证,"反复上下传送带不更优"由三角不等式保证,"传送带没用"则被端点 、 自然吸收。 - 两个连续决策点各配一层三分;外层能三分的依据必须说准——
联合凸,部分最小化保凸,而不是含糊的"取 min 后还是凸的"。 - 实数三分模板:每轮比较
缩区间到 ,固定迭代次数后取中点求值;相等时单侧收缩,天然兼容函数平台。 - 样例对照:直飞
s,只用 AB 的最优 s,两段都用 s,可见"两头搭传送带"的收益是逐段累加的。