把电线总长写成点到线段距离之和,用线段距离的凸性证明目标函数凸,再嵌套三分:内层固定 x 对 y 三分、外层对 x 三分。
OJ: roj
题目 ID: 2080
难度:提高
标签:计算几何三分凸函数浮点数usacopython
创建: 2026-10-01 07:21
更新: 2026-10-07 13:50
形式化题目
给定
要求输出取到最小值的点
样例:三段电网分别为 1.0 1.6 3.7。
正解
思路
一句话本质:目标函数是"到线段的距离之和",线段是凸集,所以每个距离函数都是凸函数、
先把目标函数算出来。 题面把"电线"定义为从电源连到一段电网的任意一点,
那么显然应该连到该线段上离电源最近的那一点,于是每段电网的贡献就是点到线段的距离。
设线段端点为
问题?为什么不能直接枚举候选点?
一个很自然的想法是"最优点应该在端点或整数点上"。样例就能否掉它。三段电网的端点只有
整数网格上的最小总长是 3.828,比真最优 3.732 大了近 0.1,四舍五入后正好是 3.8 与 3.7
的区别。随机数据上这个差距还会更大:用 4000 组小规模随机线段做检查,
下面这张图用样例把两件事画在一起:左图是几何布局,右图是固定
左图里电源
关键性质:每段电网贡献一个凸函数。
线段
二维怎么降成一维。 凸函数的边缘最小化仍然凸:令
所以
| 层次 | 被最小化的函数 | 手段 | 作用 |
|---|---|---|---|
| 外层 | 对 |
定位最优的 |
|
| 内层 | 对 |
求 |
|
| 求值 | 遍历全部线段 | 提供比较用的数值 |
三分为什么成立。 三分维护区间
- 若
,则最小值点 :反设 , 则 ,由凸性 ,矛盾。 于是可以安全地令 ; - 否则对称地令
。
每轮区间缩到 TERNS = 60 轮后长度是 %.1f 的舍入毫无影响。
边界。 退化线段 seg_dist 开头用 length2 == 0 分支返回点距;
真实数据 fence39.in 里就有一条 68 97 68 97。
代码
seg_dist 对应上面的距离公式(t 的两次夹紧就是 clamp),total_length 是 minimize_1d 是一维三分的骨架(内外层共用),best_on_line 与 on_line_min 分别给出
内层的最优 solve 里对 on_line_min 的那次三分就是外层。
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 07:21
# update_at: 2026-10-01 07:33
import sys
import math
from collections.abc import Callable
BOUND = 100.0 # 题给坐标范围:最优点必落在 [0, 100]^2 内(沿轴投影不会让任何距离变大)
TERNS = 60 # 三分轮数:区间每轮缩到 2/3,(2/3)^60 ≈ 2.7e-9,远小于输出的 0.1 精度
def seg_dist(px: float, py: float, x1: float, y1: float, x2: float, y2: float) -> float:
"""点到线段的距离:把点投影到线段所在直线,参数 t 夹到 [0, 1] 即得最近点。"""
dx = x2 - x1
dy = y2 - y1
length2 = dx * dx + dy * dy
if length2 == 0: # 退化成点:垂足公式会除零,直接算点距
return math.hypot(px - x1, py - y1)
t = ((px - x1) * dx + (py - y1) * dy) / length2
t = 0.0 if t < 0 else 1.0 if t > 1 else t # 夹紧到线段内部,是"点-线段"而非"点-直线"的距离
return math.hypot(px - (x1 + t * dx), py - (y1 + t * dy))
def total_length(segs: list[tuple[int, int, int, int]], px: float, py: float) -> float:
"""g(x, y):电源放在 (px, py) 时,连到每段电网所需的电线总长。"""
return sum(seg_dist(px, py, *s) for s in segs)
def minimize_1d(f: Callable[[float], float]) -> float:
"""一维凸函数 f 在 [0, BOUND] 上的最小值点:三分每轮把区间缩到 2/3。
凸性保证"较小的那个内点外侧"一定不含最小值点,所以每轮丢掉的那一段可以放心舍弃。
"""
lo, hi = 0.0, BOUND
for _ in range(TERNS):
m1 = lo + (hi - lo) / 3
m2 = hi - (hi - lo) / 3
if f(m1) < f(m2):
hi = m2
else:
lo = m1
return (lo + hi) / 2
def best_on_line(segs: list[tuple[int, int, int, int]], px: float) -> float:
"""固定 x = px,求 g 关于 y 的最小值位置:内层三分。"""
return minimize_1d(lambda py: total_length(segs, px, py))
def on_line_min(segs: list[tuple[int, int, int, int]], px: float) -> float:
"""h(x):固定 x = px 的整条竖线上,电线总长能达到的最小值。"""
return total_length(segs, px, best_on_line(segs, px))
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
f = next(data) # 题面的 F
segs = [(next(data), next(data), next(data), next(data)) for _ in range(f)]
# h 是凸函数(凸函数的边缘最小化仍凸),所以外层还能再套一层三分:
# 外层比较的是"整条竖线上的最小值",而不是某两个点上的值。
x = minimize_1d(lambda px: on_line_min(segs, px))
y = best_on_line(segs, x)
print(f"{x:.1f} {y:.1f} {total_length(segs, x, y):.1f}")
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 11:58
* update_at: 2026-10-06 11:58
*/
// 思路:电线总是连到电网(线段)上离电源最近的点,所以电线总长 g(P) 是 F 个
// "点到线段距离"之和。线段是凸集,凸集上的距离函数是凸函数,凸函数的和仍然凸,
// 并且"固定 x 后对 y 取最小"得到的边缘函数 h(x) 也凸,于是可以嵌套三分:
// 外层对 x 三分(比较的是 h(m1) 与 h(m2),即整条竖线上的最小值),
// 内层固定 x 对 y 三分求出 h。TERN 轮后区间长 100*(2/3)^T,精度远超一位小数。
#include <cmath>
#include <cstdio>
typedef long long ll;
const double BOUND = 100.0; // 题给坐标范围,最优点必落在 [0,100]^2 内:沿轴投影回去每段距离只会变小
const int TERN = 60; // 三分轮数:100*(2/3)^60 ≈ 2.7e-9,远小于输出要求的 0.1
const int MAXF = 155;
// 每段电网的两个端点坐标(与坐标轴平行,端点是整数)
double ax[MAXF], ay[MAXF], bx[MAXF], by[MAXF];
ll F; // 电网段数(题面的 F)
// 点 (px, py) 到线段 AB 的距离:投影参数 t 夹回 [0,1] 得到线段上的最近点
double seg_dist(double px, double py, ll i) {
double dx = bx[i] - ax[i];
double dy = by[i] - ay[i];
double length2 = dx * dx + dy * dy;
if (length2 == 0) { // 退化成点的线段:垂足公式会除零,直接返回点距
return sqrt((px - ax[i]) * (px - ax[i]) + (py - ay[i]) * (py - ay[i]));
}
double t = ((px - ax[i]) * dx + (py - ay[i]) * dy) / length2;
if (t < 0) t = 0; // 夹紧到线段内部:这是"点到线段"而非"点到直线"的距离
if (t > 1) t = 1;
double qx = ax[i] + t * dx;
double qy = ay[i] + t * dy;
return sqrt((px - qx) * (px - qx) + (py - qy) * (py - qy));
}
// g(x, y):电源放在 (x, y) 时需要的电线总长,即到每段电网的距离之和
double total_length(double x, double y) {
double sum = 0;
for (ll i = 0; i < F; i++) sum += seg_dist(x, y, i);
return sum;
}
// 内层三分:固定 x = fx,在 y ∈ [0,100] 上找 g 的最小值,返回最优的 y
// 凸性保证较小内点函数值更小时,最小值点不会在较大内点的右侧
double best_on_line(double fx) {
double lo = 0, hi = BOUND;
for (int it = 0; it < TERN; it++) {
double m1 = lo + (hi - lo) / 3;
double m2 = hi - (hi - lo) / 3;
if (total_length(fx, m1) < total_length(fx, m2))
hi = m2;
else
lo = m1;
}
return (lo + hi) / 2;
}
// h(x):固定 x = fx 的整条竖线上电线总长的最小值
double on_line_min(double fx) {
double y = best_on_line(fx);
return total_length(fx, y);
}
int main() {
scanf("%lld", &F);
for (ll i = 0; i < F; i++) {
ll x1, y1, x2, y2;
scanf("%lld %lld %lld %lld", &x1, &y1, &x2, &y2);
ax[i] = x1; ay[i] = y1; bx[i] = x2; by[i] = y2;
}
// 外层三分:h 是凸的(凸函数的边缘最小化仍凸),比较对象是整条竖线上的最小值
double lo = 0, hi = BOUND;
for (int it = 0; it < TERN; it++) {
double m1 = lo + (hi - lo) / 3;
double m2 = hi - (hi - lo) / 3;
if (on_line_min(m1) < on_line_min(m2))
hi = m2;
else
lo = m1;
}
double x = (lo + hi) / 2;
double y = best_on_line(x);
printf("%.1f %.1f %.1f\n", x, y, total_length(x, y));
return 0;
}复杂度
- 一次
求值: 。 - 一次内层三分:
次求值, 。 - 外层三分:
轮 × 2 次内层,合计 ; 、 时约 次点到线段距离。 - 空间:
,只存线段坐标;实测峰值 15 MB,单点最慢 0.52 s( )。
对照:网格搜索想达到同样的
总结
本题的难点不在实现,而在判断能不能用三分。把"电线"翻译成"点到线段距离"后,
目标函数
需要避开的坑有三个:整数枚举(整点最优值最多差 0.123,会输出错的一位小数)、 把"到线段"写成"到直线"(垂足跑出线段时会低估距离)、退化线段引起的除零。 实测 12 个真实评测数据点全部一致。
图示解析
如果只记一张结构图,就记下面这个三层嵌套——它同时说明了算法的层次和每层的开销。
x ∈ [0, 100] ──三分──► m1, m2 外层 T 轮,每轮 2 个内点
│
└─► h(m) = min_y g(m, y) ← 内层三分,2T 次 g 求值
│
└─► g(x, y) = Σ_i dist((x,y), s_i) 每项一次 clamp 投影各层开销相乘:外层
三层之间的接口非常干净:外层只问"这个