当前位置: 首页 > news >正文

旅行商问题(TSP)(1)(Route.py)(TSP 问题中的点与路径核心类)

旅行商问题从实现到测试

共四篇博客分别讲解,四篇的内容分别是

1.TSP 问题中的点与路径核心类

2.TSP 的两种贪心启发式算法实现(最近邻算法和最近插入算法)

3.TSP 项目的测试验证体系

4.TSP 项目的 “时间统计与属性注入” 工具


在解决旅行商问题(TSP)时,“点” 和 “路径” 是两个最基础的概念。Route.py 文件通过 Point 类和 Route 类,将这两个概念封装成可操作的对象,为后续实现 TSP 启发式算法打下了核心基础。

Route.py完整代码在文章最下面


题目背景

一个商品推销员要去若干个城市推销商品,该推销员从一个城市出发,需要经过所有城市后,回到出发地。应如何选择行进路线,以使总的行程最短。这就是图论和计算机科学中著名的旅行商问题(Traveling Salesperson Problem,TSP)。


一、Point 类:定义 “点” 的属性与行为

Point 类代表 TSP 中的一个 “城市” 或 “节点”,核心是存储坐标并提供与其他点的交互方法。

1. 核心属性

  • x:点的横坐标(数值类型,如整数、浮点数)
  • y:点的纵坐标(数值类型,如整数、浮点数)(与 x 类型一致)

这两个属性通过构造方法 __init__ 初始化,是所有后续计算的基础。

2. 关键方法解析

(1)构造与等价判断:__init__ 和 __eq__

def __init__(self, x, y):self.x = xself.y = ydef __eq__(self, other):if not isinstance(other, Point):return Falsereturn self.x == other.x and self.y == other.y
  • __init__:直接接收 x 和 y 坐标,无复杂逻辑,确保点的初始化简单直观。
  • __eq__:判断两个点是否 “相等”—— 必须都是 Point 实例,且 x、y 坐标完全一致。这是后续判断 “路径是否包含某点”“两点是否重复” 的关键。

(2)字符串表示:__repr__

def __repr__(self):return f"Point({self.x}, {self.y})"

打印 Point 实例时,会显示 Point(0, 0) 这样的格式,而非默认的内存地址。调试时能快速识别每个点的坐标,非常实用。

(3)距离计算:dist_to_point

def dist_to_point(self, other):if not isinstance(other, Point):raise TypeError("other must be a Point instance")dx = self.x - other.xdy = self.y - other.yreturn math.sqrt(dx ** 2 + dy ** 2)
  • 功能:计算两个点之间的欧几里得距离(平面上两点间直线距离),是 TSP 中 “路径长度” 的核心计算逻辑。
  • 细节:先校验 other 是否为 Point 实例,避免传入错误类型;通过勾股定理 sqrt(dx² + dy²) 计算距离,返回浮点数。

(4)找最近点:closest_point

def closest_point(self, pointsList):if not pointsList or not all(isinstance(p, Point) for p in pointsList):raise ValueError("pointsList must be non-empty and contain Point instances")return min(pointsList, key=lambda p: self.dist_to_point(p))
  • 功能:从一个点列表中,找到距离当前点最近的点。
  • 细节:
    1. 输入校验:确保列表非空且所有元素都是 Point 实例,避免无效输入;
    2. 核心逻辑:用 min 函数结合 lambda 表达式,以 “当前点到列表中点的距离” 为排序依据,返回距离最小的点。
  • 用途:后续 “最近邻算法” 中,每次选择 “下一个最近点” 会直接用到这个方法。

(5)可哈希支持:__hash__

def __hash__(self):return hash((self.x, self.y))
  • 作用:使 Point 实例支持 “哈希操作”,从而可以用 in 判断 “点是否在列表中”(如 startPoint in pointsL),这是算法中 “筛选未访问点” 的基础。

注:转四、为什么 Point 类需要 hash 方法?—— 从 “哈希操作” 到 TSP 算法需求


二、Route 类:定义 “路径” 的属性与行为

Route 类代表 TSP 的一条 “路径”(即访问城市的顺序),核心是管理路径列表、计算总距离、可视化路径等。

1. 核心属性

  • pointsList:路径中的点列表(元素均为 Point 实例),如 [A, B, C, A](闭合路径,起点 = 终点);
  • run_time:算法运行时间(由装饰器注入,用于后续统计和可视化);
  • algo_name:生成路径的算法名称(如 “nearestNeighbor”,同样由装饰器注入,用于区分不同算法的结果)。

2. 关键方法解析

(1)构造与初始化:__init__

def __init__(self, pointsList):if not all(isinstance(p, Point) for p in pointsList):raise TypeError("All elements in pointsList must be Point instances")self.pointsList = pointsList.copy()  # 深拷贝避免外部列表修改影响内部self.run_time = 0.0self.algo_name = "Unknown"
  • 输入校验:确保路径列表中所有元素都是 Point 实例,避免混入其他类型;
  • 深拷贝:用 copy() 复制输入列表,防止外部修改原列表时影响 Route 内部的路径数据。

(2)路径等价判断:__eq__(重点!)

TSP 中,“路径等价” 的定义很特殊:忽略起点和方向(如 A→B→C→A 与 B→C→A→B 等价,与 A→C→B→A 不等价)。这个方法是判断 “算法结果是否符合预期” 的核心。

def __eq__(self, other):if not isinstance(other, Route):return False# 第一步:排除长度和总距离不同的路径if len(self.pointsList) != len(other.pointsList):return Falseif round(self.total_distance(), 3) != round(other.total_distance(), 3):return False# 第二步:提取有效节点(去掉闭合的终点,如[A,B,C,A]→[A,B,C])self_nodes = self.pointsList[:-1] if self.pointsList[0] == self.pointsList[-1] else self.pointsListother_nodes = other.pointsList[:-1] if other.pointsList[0] == other.pointsList[-1] else other.pointsList# 第三步:验证节点完全一致(无遗漏、无多余)all_in = all(node in other_nodes for node in self_nodes)len_equal = len(self_nodes) == len(other_nodes)if not (all_in and len_equal):return False# 第四步:验证循环等价(忽略起点)或反向等价(忽略方向)combined = self_nodes * 2  # 拼接成循环列表,如[A,B,C]→[A,B,C,A,B,C]other_len = len(other_nodes)has_forward_match = any(combined[i:i + other_len] == other_nodes for i in range(len(self_nodes)))  # 正向匹配has_reverse_match = any(combined[i:i + other_len] == other_nodes[::-1] for i in range(len(self_nodes)))  # 反向匹配return has_forward_match or has_reverse_match
  • 逻辑拆解:
    1. 先排除 “长度不同” 或 “总距离不同” 的路径(快速过滤明显不等价的情况);
    2. 去掉闭合路径的终点(避免 A→B→C→A 和 A→B→C 因长度差异被误判);
    3. 确保两个路径包含的节点完全一致(避免 “少城市” 或 “多城市” 的情况);
    4. 通过 “循环列表匹配” 判断是否忽略起点(如 A→B→C 和 B→C→A),通过 “反向列表匹配” 判断是否忽略方向(如 A→B→C 和 C→B→A)。

(3)字符串表示:__repr__

def __repr__(self):return f"Route(Points: {len(self.pointsList)-1}个城市 | 总距离: {self.total_distance():.3f} | 算法: {self.algo_name})"

打印 Route 实例时,会显示 “城市数量、总距离、算法名”,如 Route(Points: 5个城市 | 总距离: 14.898 | 算法: nearestInsertion),方便快速查看路径核心信息。

(4)路径可视化:display_route

将路径用 matplotlib 绘制成图表并保存为 PDF,是直观验证路径正确性的重要手段。

def display_route(self, file_name):if not self.pointsList:raise ValueError("Route is empty, cannot display")# 1. 提取坐标并确保路径闭合(若未闭合,补充起点到终点的连线)x = [p.x for p in self.pointsList]y = [p.y for p in self.pointsList]if self.pointsList[0] != self.pointsList[-1]:x.append(self.pointsList[0].x)y.append(self.pointsList[0].y)# 2. 绘制图表plt.figure(figsize=(10, 6))plt.plot(x, y, marker='o', linestyle='-', color='#1f77b4', markersize=6, linewidth=1.5)  # 蓝色实线+圆点标记plt.scatter(x[0], y[0], color='red', s=100, zorder=5, label="Start/End")  # 起点用红色大圆点标记# 3. 设置标题(含算法名、总距离、运行时间)title = f"""TSP Route - {self.algo_name}
Total Distance: {self.total_distance():.3f} | Run Time: {self.run_time:.6f}s"""plt.title(title, fontsize=12, pad=20)plt.xlabel("X Coordinate", fontsize=10)plt.ylabel("Y Coordinate", fontsize=10)plt.legend(fontsize=9)plt.grid(alpha=0.3)  # 浅色网格,方便看坐标# 4. 保存PDF到指定目录output_dir = "TSP_Results"if not os.path.exists(output_dir):os.makedirs(output_dir)  # 目录不存在则创建save_path = f"{output_dir}/{file_name}_{self.algo_name}.pdf"plt.savefig(save_path, dpi=300, bbox_inches='tight', format='pdf')plt.close()print(f"📊 路线图已保存:{save_path}")
  • 核心细节:
    • 路径闭合:若输入路径未闭合(如 [A,B,C]),自动补充起点到终点的连线,符合 TSP “回到起点” 的要求;
    • 视觉区分:路径用蓝色实线,起点用红色大圆点(zorder=5 确保在最上层,不被其他点遮挡);
    • 结果保存:自动创建 TSP_Results 目录,文件名包含 “自定义名称 + 算法名”,避免不同结果覆盖。

(5)总距离计算:total_distance

计算路径的总长度(闭合路径,起点到终点的总距离)。

def total_distance(self):if len(self.pointsList) < 2:return 0.0total = 0.0n = len(self.pointsList)# 第一步:计算路径中相邻点的距离之和(如A→B、B→C、C→A)for i in range(n - 1):total += self.pointsList[i].dist_to_point(self.pointsList[i + 1])# 第二步:若路径未闭合,补充终点到起点的距离if self.pointsList[0] != self.pointsList[-1]:total += self.pointsList[-1].dist_to_point(self.pointsList[0])return total
  • 逻辑:先累加相邻点的距离,再判断是否需要补充 “终点到起点” 的距离,确保返回的是 TSP 要求的 “闭合路径总长度”。

三、Route.py 的核心作用

Route.py 不是孤立的文件,而是整个 TSP 项目的 “数据基础”:

  1. 为 heuristics.py 提供算法所需的 “点” 和 “路径” 对象(算法输入是 Point 列表,输出是 Route 对象);
  2. 为 Lab_test.py 提供测试验证的核心方法(如 __eq__ 判断路径是否正确,total_distance 对比预期距离);
  3. 为可视化提供支持(display_route 生成直观的路径图,方便调试和结果展示)。

若忘记某个方法的逻辑,可重点查看对应方法的注释和输入输出,尤其是 Point.closest_point 和 Route.__eq__ 这两个对算法和测试至关重要的方法。


四、为什么 Point 类需要 hash 方法?—— 从 “哈希操作” 到 TSP 算法需求

在理解 Point 类为何需要 __hash__ 方法前,我们需要先理清 Python 中 “哈希” 的核心作用,再结合 TSP 算法的实际需求,就能明白这个方法的必要性。

1、先搞懂:Python 中的 “哈希” 到底是啥?

“哈希”(Hash)本质是一种快速查找技术——通过一个“哈希函数”(比如 __hash__ 方法),把对象(比如 Point 实例)转换成一个固定的整数(称为 “哈希值”)。这个哈希值会被 Python 用来:

  1. 快速定位对象:比如在列表、集合中判断 “某个对象是否存在” 时,Python 会先通过哈希值缩小查找范围,而不是逐个遍历所有元素(遍历效率极低,尤其当元素数量多的时候);
  2. 支持 “可哈希对象” 的特殊操作:只有 “可哈希对象”(实现了 __hash__ 和 __eq__ 方法的对象)才能被放入 set(集合)、作为 dict(字典)的键 —— 这些数据结构的核心优势就是 “快速查找”,依赖的正是哈希值。

2、关键前提:__hash__ 和 __eq__ 是 “黄金搭档”

Python 规定:要让对象支持哈希操作,必须同时实现 __hash__ 和 __eq__ 方法,两者的逻辑必须一致 ——

  • 如果两个对象通过 __eq__ 判断为 “相等”(比如 Point(0,0) 和 Point(0,0)),它们的 __hash__ 方法必须返回相同的哈希值;
  • 如果两个对象的 __hash__ 返回相同的哈希值(称为 “哈希碰撞”),Python 会再通过 __eq__ 确认它们是否真的相等(避免误判)。

在 Point 类中:

  • __eq__ 方法判断 “两个点的 x、y 坐标完全相同” 即为相等;
  • __hash__ 方法用 hash((self.x, self.y)) 生成哈希值 —— 因为元组 (x,y) 是可哈希的,且只要 xy 相同,元组的哈希值就相同,完美匹配 __eq__ 的逻辑。

3、回到 TSP 算法:为什么需要 __hash__

这里有个关键问题:怎么判断 “两个点是不是同一个”?

对我们来说,只要两个点的 x、y 坐标一样(比如 A 是 (0,0),另一个点也是 (0,0)),那就是同一个点。但 Python 不知道啊 —— 如果没做特殊处理,Python 会把 “哪怕坐标一样的两个 Point 实例” 当成 “两个不同的东西”(因为它们在电脑内存里存的位置不一样)。

__hash__ 方法的核心作用,是让 Point 实例支持 in 关键字判断(比如 startPoint in pointsL),而这个判断是 TSP 算法中 “筛选未访问点” 的基础 —— 我们以 “最近邻算法” 为例,看具体需求:

①最近邻算法的核心步骤:筛选“未访问点”

最近邻算法的逻辑是:从起点出发,每次选择 “当前点的最近未访问点”,直到所有点都被访问。为了实现这个逻辑,我们需要:

  • 维护一个 “未访问点列表”(比如 unvisited = pointsL.copy());
  • 每次选中一个点后,从 “未访问点列表” 中移除它(标记为已访问);
  • 关键前提:判断 “某个点是否在未访问列表中”(比如 startPoint 是否在 pointsL 中选中的点是否在 unvisited 中)。

②没有 __hash__in 判断会出问题吗?

如果 Point 类没有实现 __hash__ 方法:

  • Python 会使用默认的哈希逻辑(基于对象的内存地址生成哈希值)—— 即使两个点的 xy 坐标完全相同(比如 p1 = Point(0,0) 和 p2 = Point(0,0)),它们的内存地址不同,哈希值也不同;
  • 此时用 p1 in [p2] 判断会返回 False(因为 Python 先比较哈希值,发现不同就直接判定 “不存在”),这完全违背了我们对 “相同点” 的定义 —— 比如算法中可能误把 “已访问的点” 当作 “未访问”,导致逻辑错误。

③有了 __hash__in 判断才正确

当 Point 类实现了 __hash__ 后:

  • 只要两个点的 xy 相同,它们的哈希值就相同,in 判断会正确返回 True
  • 比如 startPoint = Point(0,0)pointsL = [Point(0,0), Point(1,1)]startPoint in pointsL 会正确返回 True,确保我们能从列表中找到起点;
  • 再比如 “未访问列表” unvisited = [Point(1,1), Point(2,2)],当我们选中 Point(1,1) 后,用 unvisited.remove(Point(1,1)) 能正确移除它,不会因为 “内存地址不同” 而报错 “点不在列表中”。

4、总结:__hash__ 的作用链

__hash__ 方法看似简单,实则是 TSP 算法正常运行的 “隐形基石”,作用链可概括为:实现 __hash__ → Point 实例支持正确的哈希操作 → 支持用 in 判断“点是否在列表中” → 正确维护“未访问点列表” → TSP 算法(如最近邻)能正常筛选未访问点

简单来说:没有 __hash__,算法就无法正确识别 “相同的点”,更无法判断 “某个点是否已访问”,最终导致逻辑错误 —— 这就是 Point 类必须实现 __hash__ 方法的根本原因。

hash 方法的本质,就是给每个 Point 实例发一个 “专属身份证号”,但这个身份证号有个规矩:只要两个点的 x、y 坐标一样,身份证号就必须一样;坐标不一样,身份证号尽量不一样。

一句话说清

hash 方法就是给每个 Point 实例按 “x、y 坐标” 发个 “专属身份证号”,帮 Python 快速认清楚 “哪个点是同一个”,这样 TSP 算法才能正确判断 “点有没有访问过”,不会重复走或漏掉点。


五、Route.py完整代码

import math
import os
import matplotlib.pyplot as pltclass Point:def __init__(self, x, y):"""初始化点坐标(案例任务2要求)"""self.x = xself.y = ydef __eq__(self, other):"""判断两点相等:x、y完全一致(案例任务2要求)"""if not isinstance(other, Point):return Falsereturn self.x == other.x and self.y == other.ydef __repr__(self):"""点的字符串表示(如Point(0,0),案例任务2要求)"""return f"Point({self.x}, {self.y})"def dist_to_point(self, other):"""计算欧几里得距离(案例任务2要求)"""if not isinstance(other, Point):raise TypeError("other must be a Point instance")dx = self.x - other.xdy = self.y - other.yreturn math.sqrt(dx ** 2 + dy ** 2)def closest_point(self, pointsList):"""找到列表中最近的点(案例任务2要求)"""if not pointsList or not all(isinstance(p, Point) for p in pointsList):raise ValueError("pointsList must be non-empty and contain Point instances")return min(pointsList, key=lambda p: self.dist_to_point(p))def __hash__(self):"""使Point实例可哈希(支持列表包含判断)"""return hash((self.x, self.y))class Route:def __init__(self, pointsList):"""初始化路径(案例任务2要求)"""if not all(isinstance(p, Point) for p in pointsList):raise TypeError("All elements in pointsList must be Point instances")self.pointsList = pointsList.copy()self.run_time = 0.0  # 装饰器注入的运行时间self.algo_name = "Unknown"  # 装饰器注入的算法名def __eq__(self, other):"""路径等价性:忽略起点和方向(案例任务2要求,如A→B→C与B→C→A相等)"""if not isinstance(other, Route):return False# 排除长度和距离不同的情况if len(self.pointsList) != len(other.pointsList):return Falseif round(self.total_distance(), 3) != round(other.total_distance(), 3):return False# 提取有效节点(排除闭合的终点)self_nodes = self.pointsList[:-1] if self.pointsList[0] == self.pointsList[-1] else self.pointsListother_nodes = other.pointsList[:-1] if other.pointsList[0] == other.pointsList[-1] else other.pointsList# 验证节点完全一致all_in = all(node in other_nodes for node in self_nodes)len_equal = len(self_nodes) == len(other_nodes)if not (all_in and len_equal):return False# 验证循环等价(忽略起点)和反向等价(忽略方向)combined = self_nodes * 2other_len = len(other_nodes)has_forward_match = any(combined[i:i + other_len] == other_nodes for i in range(len(self_nodes)))has_reverse_match = any(combined[i:i + other_len] == other_nodes[::-1] for i in range(len(self_nodes)))return has_forward_match or has_reverse_matchdef __repr__(self):"""路径的字符串表示(案例任务2要求)"""return f"Route(Points: {len(self.pointsList)-1}个城市 | 总距离: {self.total_distance():.3f} | 算法: {self.algo_name})"def display_route(self, file_name):"""绘制路线图并保存为PDF(案例任务2要求,含算法名和运行时间)"""if not self.pointsList:raise ValueError("Route is empty, cannot display")# 提取坐标并闭合路径x = [p.x for p in self.pointsList]y = [p.y for p in self.pointsList]if self.pointsList[0] != self.pointsList[-1]:x.append(self.pointsList[0].x)y.append(self.pointsList[0].y)# 绘制图表(符合案例图1风格:蓝色实线+红色起点)plt.figure(figsize=(10, 6))plt.plot(x, y, marker='o', linestyle='-', color='#1f77b4', markersize=6, linewidth=1.5)plt.scatter(x[0], y[0], color='red', s=100, zorder=5, label="Start/End")# 标题含算法名、运行时间、总距离(案例任务4要求)title = f"""TSP Route - {self.algo_name}
Total Distance: {self.total_distance():.3f} | Run Time: {self.run_time:.6f}s"""plt.title(title, fontsize=12, pad=20)plt.xlabel("X Coordinate", fontsize=10)plt.ylabel("Y Coordinate", fontsize=10)plt.legend(fontsize=9)plt.grid(alpha=0.3)# 保存PDF(案例要求输出到文件)output_dir = "TSP_Results"if not os.path.exists(output_dir):os.makedirs(output_dir)save_path = f"{output_dir}/{file_name}_{self.algo_name}.pdf"plt.savefig(save_path, dpi=300, bbox_inches='tight', format='pdf')plt.close()print(f"📊 路线图已保存:{save_path}")def total_distance(self):"""计算闭合路径总距离(案例任务2要求,如A→B→C→A)"""if len(self.pointsList) < 2:return 0.0total = 0.0n = len(self.pointsList)for i in range(n - 1):total += self.pointsList[i].dist_to_point(self.pointsList[i + 1])# 补充未闭合路径的起点距离if self.pointsList[0] != self.pointsList[-1]:total += self.pointsList[-1].dist_to_point(self.pointsList[0])return total
http://www.dtcms.com/a/478464.html

相关文章:

  • 学习笔记--文件上传
  • Leetcode 26
  • 淘宝领券网站怎么做上海工程咨询行业协会
  • 泰国网站域名wordpress建网站的优点
  • 解锁 JavaScript 字符串补全魔法:padStart()与 padEnd()
  • Spring Boot 3零基础教程,IOC容器中组件的注册,笔记08
  • TDengine 数学函数 DEGRESS 用户手册
  • 源码:Oracle AWR报告之Top 10 Foreground Events by Total Wait Time
  • 告别繁琐坐标,让公式“说人话”:Excel结构化引用完全指南
  • 【AI论文】CoDA:面向协作数据可视化的智能体系统
  • 从AAAI2025中挑选出对目标检测有帮助的文献——第六期
  • 【深度学习】反向传播
  • 网站开发交接新闻源发稿平台
  • 滴答时钟延时
  • 【C++篇】:ServiceBus RPC 分布式服务总线框架项目
  • 后训练——Post-training技术介绍
  • 获取KeyStore的sha256
  • Linux (5)| 入门进阶:Linux 权限管理的基础规则与实践
  • 常见压缩包格式详解:区别及在不同系统中的解压方式
  • 【数学 进制 数位DP】P9362 [ICPC 2022 Xi‘an R] Find Maximum|普及+
  • .net过滤器和缓存
  • 张家港网站建设培训班电力建设专家答疑在哪个网站
  • 零基础学AI大模型之大模型的“幻觉”
  • 网站快速优化排名排名c语言入门自学零基础
  • MySQL排序规则utf8mb4_0900_ai_ci解析
  • 做网站别名解析的目的是什么同城广告发布平台
  • GPT4Free每日更新的免登录工作AI提供商和模型列表
  • 网站群建设座谈会云浮新增病例详情
  • Proxmox 9 一键更新虚拟机mac
  • C# WPF DataGrid使用Observable<Observable<object>类型作为数据源