106 lines
4.1 KiB
Python
106 lines
4.1 KiB
Python
"""图论 / DP 工具箱验收(docs/第三四问图论与动态规划优化方案.md §3.2、§3.4)。
|
|
|
|
要点:
|
|
- Held-Karp 精确开放路径必须**真的**最优(与全排列暴力对拍);
|
|
- 启发式(NN + 2-opt / Or-opt)必须是可接受的上界,且不劣于 NN;
|
|
- MST 权重必须是最优开放路径的合法下界(在线后悔值审计依赖它);
|
|
- 路由度量口径可复算:actual == offline 时 gap 为 0,且 gap 不越界。
|
|
"""
|
|
|
|
import itertools
|
|
import math
|
|
import random
|
|
import unittest
|
|
|
|
from mcm26b.routing import (EXACT_LIMIT, held_karp_open, improve_open_path,
|
|
mst_weight, nearest_neighbor_order,
|
|
optimal_open_path, or_opt_open, path_length,
|
|
route_metrics, two_opt_open)
|
|
|
|
|
|
def _random_points(count: int, seed: int, radius: float = 1500.0):
|
|
rng = random.Random(seed)
|
|
return [(rng.uniform(-radius, radius), rng.uniform(-radius, radius))
|
|
for _ in range(count)]
|
|
|
|
|
|
class HeldKarpTests(unittest.TestCase):
|
|
def test_matches_brute_force_on_small_instances(self):
|
|
for seed in range(6):
|
|
points = _random_points(6, seed)
|
|
order = held_karp_open(points)
|
|
best = min(path_length(list(perm))
|
|
for perm in itertools.permutations(points))
|
|
self.assertAlmostEqual(path_length(order), best, places=6,
|
|
msg=f"seed={seed}")
|
|
|
|
def test_returns_all_points_once(self):
|
|
points = _random_points(9, 42)
|
|
order = held_karp_open(points)
|
|
self.assertEqual(sorted(order), sorted(points))
|
|
|
|
def test_single_and_empty(self):
|
|
self.assertEqual(held_karp_open([]), [])
|
|
point = [(3.0, 4.0)]
|
|
self.assertEqual(held_karp_open(point), point)
|
|
|
|
|
|
class HeuristicTests(unittest.TestCase):
|
|
def test_two_opt_and_or_opt_never_worse_than_nn(self):
|
|
for seed in range(5):
|
|
points = _random_points(30, seed)
|
|
base = path_length(nearest_neighbor_order(points))
|
|
self.assertLessEqual(path_length(two_opt_open(
|
|
nearest_neighbor_order(points))), base + 1e-9)
|
|
self.assertLessEqual(path_length(improve_open_path(points)),
|
|
base + 1e-9)
|
|
|
|
def test_optimal_open_path_beats_heuristics_small(self):
|
|
points = _random_points(10, 7)
|
|
optimal = path_length(optimal_open_path(points))
|
|
self.assertLessEqual(optimal,
|
|
path_length(improve_open_path(points)) + 1e-9)
|
|
|
|
def test_or_opt_accepts_only_improvements(self):
|
|
points = _random_points(20, 3)
|
|
order = nearest_neighbor_order(points)
|
|
improved = or_opt_open(order)
|
|
self.assertLessEqual(path_length(improved), path_length(order) + 1e-9)
|
|
|
|
|
|
class LowerBoundTests(unittest.TestCase):
|
|
def test_mst_is_a_lower_bound(self):
|
|
for seed in range(8):
|
|
points = _random_points(24, seed)
|
|
optimum = path_length(optimal_open_path(points))
|
|
self.assertLessEqual(mst_weight(points), optimum + 1e-6,
|
|
msg=f"seed={seed}")
|
|
|
|
def test_mst_without_start(self):
|
|
points = _random_points(12, 1)
|
|
self.assertLessEqual(mst_weight(points, start=None),
|
|
path_length(optimal_open_path(points)) + 1e-6)
|
|
|
|
|
|
class RouteMetricsTests(unittest.TestCase):
|
|
def test_gap_zero_when_actual_matches_offline(self):
|
|
points = _random_points(8, 5)
|
|
offline = path_length(optimal_open_path(points))
|
|
metrics = route_metrics(points, offline)
|
|
self.assertAlmostEqual(metrics["route_gap_vs_offline"], 0.0, places=6)
|
|
self.assertEqual(metrics["distinct_service_points"], 8)
|
|
|
|
def test_dedupes_points(self):
|
|
points = [(1.0, 1.0), (1.0, 1.0), (2.0, 2.0)]
|
|
metrics = route_metrics(points, 10.0)
|
|
self.assertEqual(metrics["distinct_service_points"], 2)
|
|
|
|
def test_empty_is_safe(self):
|
|
metrics = route_metrics([], 0.0)
|
|
self.assertEqual(metrics["distinct_service_points"], 0)
|
|
self.assertEqual(metrics["route_gap_vs_offline"], 0.0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|