Files
mcm-26-b/project/q1/reference.py
T
2026-09-12 16:58:25 +08:00

179 lines
6.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""问题一独立参考解:大框 Sutherland–Hodgman 半平面裁剪 + 精确直径/MEC。
与生产实现 ``mcm26b.geometry.localization.solve`` 的独立性:
- 生产实现枚举"半平面边界直线两两交点"再筛可行点;
- 本参考解从一个大方框出发,逐条半平面做 Sutherland–Hodgman 裁剪,
是另一条构造路径,且这里的半平面系数、直径、最小包围圆都在本文件内
独立编码(不复用生产实现的 ``halfplanes`` / ``diameter`` / ``mec``)。
只要区域有界,大框(±1e7 m)裁剪得到的顶点即精确多边形;顶点触到大框
边界则判无界。最小包围圆用"枚举所有 1/2/3 点支撑圆"的暴力算法,直径用
顶点对暴力枚举,二者都是 O(m²)/O(m³) 的朴素精确算法,m 为多边形顶点数。
"""
from __future__ import annotations
import math
from time import perf_counter
from project.common import Method
METHOD = Method(
key="reference",
name="大框半平面裁剪 + 精确直径/MEC",
kind="reference",
note="独立于生产顶点枚举的精确参考解",
)
BOX_M = 1.0e7
_PARALLEL_EPS = 1e-12
def _planes(case: dict) -> list[tuple[float, float, float]]:
"""独立重写 a*x+b*y<=c 的半平面构造(与题面角域定义一致)。"""
planes = []
for row in case["measurements"]:
half = math.radians(row.get("half_angle_deg", 1.0))
theta = math.radians(row["bearing_deg"] % 360.0)
sx, sy = row["x"], row["y"]
for angle, sign in ((theta - half, 1.0), (theta + half, -1.0)):
a = sign * math.sin(angle)
b = -sign * math.cos(angle)
planes.append((a, b, a * sx + b * sy))
# 前向约束:不允许把角域取成反向无限双楔
a, b = -math.cos(theta), -math.sin(theta)
planes.append((a, b, a * sx + b * sy))
return planes
def _clip(polygon, a, b, c, tol=1e-7):
"""Sutherland–Hodgman:保留 a*x+b*y<=c 的一侧。"""
if not polygon:
return []
output = []
count = len(polygon)
for index in range(count):
p, q = polygon[index], polygon[(index + 1) % count]
fp = a * p[0] + b * p[1] - c
fq = a * q[0] + b * q[1] - c
p_in, q_in = fp <= tol, fq <= tol
if p_in:
output.append(p)
if p_in != q_in:
ratio = fp / (fp - fq)
output.append((p[0] + ratio * (q[0] - p[0]),
p[1] + ratio * (q[1] - p[1])))
deduped = []
for point in output:
if not deduped or math.dist(point, deduped[-1]) > 1e-9:
deduped.append(point)
if len(deduped) > 1 and math.dist(deduped[0], deduped[-1]) <= 1e-9:
deduped.pop()
return deduped
def _brute_force_diameter(points):
best = 0.0
pair = (points[0], points[0])
for i, pi in enumerate(points):
for pj in points[i + 1:]:
value = math.dist(pi, pj)
if value > best:
best, pair = value, (pi, pj)
return best, pair
def _circumcircle(a, b, c):
ux, uy = b[0] - a[0], b[1] - a[1]
vx, vy = c[0] - a[0], c[1] - a[1]
det = 2.0 * (ux * vy - uy * vx)
if abs(det) <= _PARALLEL_EPS * max(
math.hypot(ux, uy) * math.hypot(vx, vy), 1e-300):
return None
uu, vv = ux * ux + uy * uy, vx * vx + vy * vy
center = (a[0] + (uu * vy - vv * uy) / det,
a[1] + (ux * vv - vx * uu) / det)
return center, math.dist(center, a)
def _brute_force_mec(points):
"""枚举 1/2/3 点支撑圆的精确最小包围圆(朴素但无迭代)。"""
xs = [p[0] for p in points]
ys = [p[1] for p in points]
best_c = ((min(xs) + max(xs)) / 2.0, (min(ys) + max(ys)) / 2.0)
best_r = max(math.dist(best_c, p) for p in points)
candidates: list[tuple[tuple[float, float], float]] = []
for i, p in enumerate(points):
for q in points[i + 1:]:
candidates.append((((p[0] + q[0]) / 2.0, (p[1] + q[1]) / 2.0),
math.dist(p, q) / 2.0))
for i, p in enumerate(points):
for j in range(i + 1, len(points)):
for k in range(j + 1, len(points)):
circle = _circumcircle(p, points[j], points[k])
if circle is not None:
candidates.append(circle)
for center, radius in candidates:
if radius >= best_r:
continue
if all(math.dist(center, p) <= radius + 1e-7 for p in points):
best_c, best_r = center, radius
return best_c, best_r
def solve_case(case: dict) -> dict:
start = perf_counter()
planes = _planes(case)
polygon = [(-BOX_M, -BOX_M), (BOX_M, -BOX_M), (BOX_M, BOX_M), (-BOX_M, BOX_M)]
for a, b, c in planes:
polygon = _clip(polygon, a, b, c)
if not polygon:
return _finish(_degenerate("empty"), start)
boundary_tol = 1e-6 * BOX_M
if any(abs(point[0]) >= BOX_M - boundary_tol
or abs(point[1]) >= BOX_M - boundary_tol for point in polygon):
return _finish(_degenerate("unbounded"), start)
if len(polygon) == 1:
summary = {"status": "point", "diameter_m": 0.0, "covers": True,
"mec_radius_m": 0.0, "max_excess_m": 0.0,
"vertex_count": 1}
else:
value, (a, b) = _brute_force_diameter(polygon)
if len(polygon) == 2:
summary = {"status": "segment", "diameter_m": value, "covers": True,
"mec_radius_m": value / 2.0, "max_excess_m": 0.0,
"vertex_count": 2}
else:
_mec_center, radius = _brute_force_mec(polygon)
diameter_center = ((a[0] + b[0]) / 2.0, (a[1] + b[1]) / 2.0)
max_distance = max(math.dist(diameter_center, p) for p in polygon)
summary = {
"status": "polygon",
"diameter_m": value,
"covers": bool(radius <= value / 2.0 + 1e-7),
"mec_radius_m": radius,
"max_excess_m": round(max_distance - value / 2.0, 9),
"vertex_count": len(polygon),
}
return _finish(summary, start)
def _degenerate(status: str) -> dict:
return {"status": status, "diameter_m": None, "covers": None,
"mec_radius_m": None, "max_excess_m": None, "vertex_count": 0}
def _finish(summary: dict, start: float) -> dict:
result = {
"method": METHOD.key,
"algorithm": METHOD.name,
"resolution_m": 0.0,
"diameter_infinite": summary["status"] == "unbounded",
"outside_vertex_count": None,
"runtime_ms": round((perf_counter() - start) * 1000.0, 4),
}
result.update(summary)
return result