二分最大误差,使用随机增量最小覆盖圆与最长可行前缀贪心构造连续分组。
OJ: luogu
题目 ID: P3517
难度:省选/NOI-
标签:二分答案计算几何最小覆盖圆随机化python
日期: 2026-07-16 18:28
题意
把点序列切成不超过 m 个连续非空段,每段用一个新点代替。最小化所有原点到所属新点的最大距离,并输出误差、段数和各段代表点。
思路
一段点能被误差 radius 代替,当且仅当它们的最小覆盖圆半径不超过 radius。
固定半径后从左到右贪心:每次取当前起点能覆盖的最长前缀。这不会比提前截断使用更多段,因为更长的第一段只会给后续留下更短的后缀。段的可行性随右端点单调,因此先指数扩展范围,再二分出最长端点。
区间最小覆盖圆使用随机增量算法。按随机顺序加入点;只有新点落在当前圆外时,才依次固定它为边界点,再固定第二、第三个边界点重建圆。最小覆盖圆最多由三个边界点确定。
外层对答案做 48 次浮点二分。最后再执行一次贪心,缓存中的最小圆圆心就是各段要输出的代表点。
Python 知识
random.Random(seed).shuffle(selected)创建局部、可复现的随机顺序,不污染全局随机状态。math.hypot(dx, dy)直接计算欧氏距离,数值行为比手写开方清楚。- 字典以
(left, right)为键缓存重复区间的最小覆盖圆。 keep_segments=False让同一个函数既能只判可行,也能在最后返回构造方案。
代码
python
import math
import random
import sys
data = iter(map(int, sys.stdin.buffer.read().split()))
n, group_limit = next(data), next(data)
points = [(next(data), next(data)) for _ in range(n)]
cache = {}
def diameter_circle(a, b):
center_x = (a[0] + b[0]) / 2
center_y = (a[1] + b[1]) / 2
radius = math.hypot(a[0] - b[0], a[1] - b[1]) / 2
return center_x, center_y, radius
def circumcircle(a, b, c):
ax, ay = a
bx, by = b
cx, cy = c
denominator = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by))
if abs(denominator) < 1e-18:
pairs = ((a, b), (a, c), (b, c))
return diameter_circle(*max(pairs, key=lambda pair:
(pair[0][0] - pair[1][0]) ** 2 + (pair[0][1] - pair[1][1]) ** 2))
a2 = ax * ax + ay * ay
b2 = bx * bx + by * by
c2 = cx * cx + cy * cy
center_x = (a2 * (by - cy) + b2 * (cy - ay) + c2 * (ay - by)) / denominator
center_y = (a2 * (cx - bx) + b2 * (ax - cx) + c2 * (bx - ax)) / denominator
return center_x, center_y, math.hypot(center_x - ax, center_y - ay)
def minimum_circle(left, right):
key = (left, right)
circle = cache.get(key)
if circle is not None:
return circle
selected = points[left:right]
if len(selected) == 1:
circle = selected[0][0], selected[0][1], 0.0
elif len(selected) == 2:
circle = diameter_circle(*selected)
else:
random.Random(left * 1000003 + right).shuffle(selected)
center_x, center_y = selected[0]
radius = 0.0
for i, point in enumerate(selected):
if math.hypot(point[0] - center_x, point[1] - center_y) <= radius + 1e-9:
continue
center_x, center_y = point
radius = 0.0
for j in range(i):
other = selected[j]
if math.hypot(other[0] - center_x, other[1] - center_y) <= radius + 1e-9:
continue
center_x, center_y, radius = diameter_circle(point, other)
for third in selected[:j]:
if math.hypot(third[0] - center_x, third[1] - center_y) > radius + 1e-9:
center_x, center_y, radius = circumcircle(point, other, third)
circle = center_x, center_y, radius
if len(cache) < 250000:
cache[key] = circle
return circle
def partition(limit, keep_segments=False):
segments = []
left = 0
while left < n:
length = 1
while (left + 2 * length <= n
and minimum_circle(left, left + 2 * length)[2] <= limit + 1e-8):
length *= 2
low = left + length
high = min(n, left + 2 * length)
while low < high:
middle = (low + high + 1) // 2
if minimum_circle(left, middle)[2] <= limit + 1e-8:
low = middle
else:
high = middle - 1
segments.append((left, low))
if len(segments) > group_limit:
return None
left = low
return segments if keep_segments else True
low = 0.0
high = minimum_circle(0, n)[2]
for _ in range(48):
middle = (low + high) / 2
if partition(middle):
high = middle
else:
low = middle
segments = partition(high + 1e-7, True)
print(f"{high + 1e-7:.10f}")
print(len(segments))
for left, right in segments:
center_x, center_y, _ = minimum_circle(left, right)
print(f"{center_x:.10f} {center_y:.10f}")复杂度
随机增量最小覆盖圆单次期望线性。设浮点二分次数为常数 B=48,结合端点的指数查找与二分,整体期望时间可记为
总结
本题把“最小化最大值”转成二分判定,再用连续分段的最长前缀贪心。由于是 Special Judge,样例圆心不唯一,只要输出圆心确实覆盖对应连续段即可。
