旅行商问题(TSP)(2)(heuristics.py)(TSP 的两种贪心启发式算法实现)

旅行商问题从实现到测试

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

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

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

3.TSP 项目的 "时间统计与属性注入" 工具

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

在旅行商问题(TSP)中,由于 "找到最优解" 的复杂度极高,实际应用中常使用启发式算法(贪心策略)快速得到近似解。heuristics.py 文件实现了两种核心的贪心算法:最近邻点法和最近插入法。

heuristics.py 完整代码在文章末尾

代码源:旅行商问题(TSP)代码和数据资源-CSDN下载


一、算法前置知识:装饰器 @display_time

在看算法之前,先注意每个算法函数上方的 @display_time 装饰器 ------ 它来自 decorator.py,核心作用是:

  1. 统计算法的运行时间;
  2. 自动为输出的 Route 对象注入 run_time(运行时间)和 algo_name(算法名)属性;
  3. 打印算法的运行结果(运行时间、路径总距离)。

装饰器的存在让算法代码更简洁,无需手动处理时间统计和属性注入,后续在 Lab_test.py 中调用算法时,能直接获取时间和路径信息。


二、最近邻点法(nearestNeighbor)

1. 算法核心思想

从指定的起点出发,每次选择 "当前点的最近未访问点" 作为下一个点,直到所有点都被访问,最后回到起点形成闭合路径。

贪心逻辑 :每一步只关注 "当前最优"(最近的点),不考虑后续步骤的影响,因此不一定能得到全局最优解,但计算速度快。

2. 代码实现与解析

python 复制代码
@display_time
def nearestNeighbor(pointsL, startPoint):
    """最近邻算法:从起点出发,每次选择最近未访问点(案例图2逻辑)"""
    # 第一步:输入验证(确保参数符合要求,避免后续错误)
    if not isinstance(startPoint, Point):
        raise TypeError("startPoint must be a Point instance")
    if startPoint not in pointsL:
        raise ValueError("startPoint must be in pointsL")
    
    # 第二步:初始化"未访问点列表"和"路径列表"
    unvisited = pointsL.copy()  # 复制所有点,作为未访问集合
    unvisited.remove(startPoint)  # 移除起点(起点已访问)
    path = [startPoint]  # 路径从起点开始
    current = startPoint  # 当前所在的点(初始为起点)
    
    # 第三步:核心循环------反复选择最近未访问点,直到所有点被访问
    while unvisited:  # 当还有未访问点时
        # 找到未访问点中距离当前点最近的点(用min+lambda,以距离为排序依据)
        closest = min(unvisited, key=lambda p: current.dist_to_point(p))
        # 将最近点加入路径
        path.append(closest)
        # 从"未访问列表"中移除该点(标记为已访问)
        unvisited.remove(closest)
        # 更新当前点为刚加入的最近点
        current = closest
    
    # 第四步:闭合路径------最后回到起点
    path.append(startPoint)
    
    # 第五步:返回路径(封装成Route对象,符合输出要求)
    return Route(path)

3. 关键步骤拆解

以 "5 点用例"(A (0,0)、B (0,3)、C (4,0)、D (4,3)、E (2,1.5))、起点为 A 为例,手动走一遍流程:

  1. 初始化:unvisited = [B,C,D,E]path = [A]current = A
  2. 第一次循环:A 的最近未访问点是 E(距离≈2.5),path 变为 [A,E]unvisited 变为 [B,C,D]current = E
  3. 第二次循环:E 的最近未访问点是 B(距离≈2.5),path 变为 [A,E,B]unvisited 变为 [C,D]current = B
  4. 第三次循环:B 的最近未访问点是 D(距离 = 4),path 变为 [A,E,B,D]unvisited 变为 [C]current = D
  5. 第四次循环:D 的最近未访问点是 C(距离 = 3),path 变为 [A,E,B,D,C]unvisited 为空;
  6. 闭合路径:path 最终为 [A,E,B,D,C,A],总距离≈15.898(与 Lab_test.py 中的预期结果一致)。

4.最近邻算法(nearestNeighbor)过程图

网页文件:5点用例最近邻算法流程可视化(Prayer).html资源-CSDN下载

5. 算法特点

  • 优点:逻辑简单,代码短,计算速度快(时间复杂度 O (n²),n 为点的数量);
  • 缺点:易陷入 "局部最优",最终路径总距离可能较长(如上述例子中,若起点不同,路径可能更优)。

三、最近插入法(nearestInsertion)

1. 算法核心思想

与最近邻法不同,最近插入法从 "两个起点构成的初始路径" 出发,每次找到 "距离当前路径最近的未访问点",并将其插入到 "使路径总长度++增量++最小" 的位置,直到所有点被访问,最后回到起点。贪心逻辑:不仅关注 "选哪个点",还关注 "插在哪里",因此通常比最近邻法得到的路径更优(总距离更短),但计算量稍大。

2. 代码实现与解析

python 复制代码
@display_time
def nearestInsertion(pointsL, startPoint1, startPoint2):
    """最近插入算法:从两个起点出发,插入最近点并最小化路径增量(案例图3逻辑)"""
    # 第一步:输入验证(确保两个起点不同且都在点列表中)
    if startPoint1 == startPoint2:
        raise ValueError("Start points must be different")
    if startPoint1 not in pointsL or startPoint2 not in pointsL:
        raise ValueError("Start points must be in pointsL")
    
    # 第二步:初始化"路径列表"和"剩余未访问点列表"
    path = [startPoint1, startPoint2]  # 初始路径是两个起点(如A→B)
    remaining = [p for p in pointsL if p not in path]  # 未访问的点(排除两个起点)
    
    # 第三步:核心循环------选最近点→找最小插入位置→插入,直到所有点被访问
    while remaining:
        # 子步骤1:找到距离当前路径最近的未访问点
        # 定义"点到路径的距离":点到路径中所有点的最小距离
        def dist_to_path(point):
            return min(point.dist_to_point(p) for p in path)
        # 用min找到"距离路径最近"的点
        best_point = min(remaining, key=dist_to_path)
        
        # 子步骤2:找到插入该点后,路径总长度增量最小的位置
        min_increase = math.inf  # 初始化"最小增量"为无穷大
        best_pos = 0  # 初始化"最优插入位置"为0
        # 遍历路径中的每一段(如路径[A,B,D]的段是A→B、B→D)
        for i in range(len(path)):
            a, b = path[i], path[(i + 1) % len(path)]  # a是段的起点,b是段的终点(%处理循环)
            original_dist = a.dist_to_point(b)  # 原段的距离(a→b)
            new_dist = a.dist_to_point(best_point) + best_point.dist_to_point(b)  # 插入后的距离(a→best_point→b)
            increase = new_dist - original_dist  # 插入导致的距离增量(越小越好)
            # 更新"最小增量"和"最优位置"
            if increase < min_increase:
                min_increase = increase
                best_pos = i + 1  # 插入位置在a和b之间(即i+1索引)
        
        # 子步骤3:将最优点点插入路径,并从剩余列表中移除
        path.insert(best_pos, best_point)
        remaining.remove(best_point)
    
    # 第四步:闭合路径------回到第一个起点
    path.append(startPoint1)
    
    # 第五步:返回路径(封装成Route对象)
    return Route(path)

3. 关键步骤拆解

同样以 "5 点用例"、两个起点为 A (0,0) 和 B (0,3) 为例,手动走一遍流程:

  1. 初始化:path = [A,B]remaining = [C,D,E]
  2. 子步骤 1:找距离路径 [A,B] 最近的点 ------E 到 A 的距离≈2.5,到 B 的距离≈2.5,是最近点(best_point=E);
  3. 子步骤 2:找 E 的最优插入位置:
    • 若插入 A 和 B 之间(位置 1):原段 A→B 距离 = 3,插入后 A→E→B 距离≈2.5+2.5=5,增量 = 2;
    • 无其他段(路径只有 A→B),因此 best_pos=1;
  4. 插入 E:path 变为 [A,B,E]remaining = [C,D]
  5. 下一轮循环:
    • 子步骤 1:找距离路径 [A,B,E] 最近的点 ------D 到 B 的距离 = 4,到 E 的距离≈2.5,是最近点(best_point=D);
    • 子步骤 2:找 D 的最优插入位置:
      • 段 A→B:插入后 A→D→B 距离≈5+4=9,原距离 3,增量 = 6;
      • 段 B→E:插入后 B→D→E 距离 = 4+2.5=6.5,原距离 2.5,增量 = 4;
      • 段 E→A:插入后 E→D→A 距离≈2.5+5=7.5,原距离 2.5,增量 = 5;
      • 最小增量是 4,对应位置 3(B 和 E 之间);
    • 插入 D:path 变为 [A,B,D,E]remaining = [C]
  6. 再下一轮循环:
    • 子步骤 1:找距离路径 [A,B,D,E] 最近的点 ------C 到 E 的距离≈2.5,到 A 的距离 = 4,是最近点(best_point=C);
    • 子步骤 2:找 C 的最优插入位置(计算后最优位置是 5,E 和 A 之间);
    • 插入 C:path 变为 [A,B,D,E,C]remaining 为空;
  7. 闭合路径:path 最终为 [A,B,D,E,C,A](或等价路径),总距离≈14.898(比最近邻法更优)。

4.最近插入法(nearestInsertion)过程图

网页文件:5点用例最近插入算法流程可视化(Prayer).html资源-CSDN下载

4. 算法特点

  • 优点:路径总距离通常比最近邻法短(因为考虑了插入位置的优化),是更实用的近似算法;
  • 缺点:逻辑比最近邻法复杂,计算量稍大(时间复杂度仍为 O (n²),但常数项更大)。

heuristics.py 是整个 TSP 项目的 "算法核心":

  1. 实现了两种贪心策略,为 TSP 提供不同精度和速度的近似解;
  2. 输出结果统一封装为 Route 对象,与 Route.py 无缝衔接,方便后续测试和可视化;
  3. 借助 @display_time 装饰器,自动统计运行时间,为后续对比算法效率提供数据支持。

若忘记算法步骤,可重点回忆:

  • 最近邻法:"选最近点→加路径→标记已访问" 循环;
  • 最近插入法:"选最近点→找最小增量位置→插入" 循环。两者的核心差异在于 "是否优化插入位置",这也是最近插入法结果更优的关键。

四、heuristics.py完整代码

python 复制代码
from Route import Route, Point
from decorator import display_time  # 导入案例要求的装饰器
import math


# ---------------------- 案例必做:最近邻点法(任务1要求) ----------------------
@display_time
def nearestNeighbor(pointsL, startPoint):
    """最近邻算法:从起点出发,每次选择最近未访问点(案例图2逻辑)"""
    # 输入验证(符合案例对函数参数的要求)
    if not isinstance(startPoint, Point):
        raise TypeError("startPoint must be a Point instance")
    if startPoint not in pointsL:
        raise ValueError("startPoint must be in pointsL")

    unvisited = pointsL.copy()
    unvisited.remove(startPoint)
    path = [startPoint]
    current = startPoint

    # 核心逻辑:反复选择最近未访问点
    while unvisited:
        closest = min(unvisited, key=lambda p: current.dist_to_point(p))
        path.append(closest)
        unvisited.remove(closest)
        current = closest

    path.append(startPoint)  # 闭合路径(回到起点,符合TSP定义)
    return Route(path)


# ---------------------- 案例必做:最近插入法(任务1要求) ----------------------
@display_time
def nearestInsertion(pointsL, startPoint1, startPoint2):
    """最近插入算法:从两个起点出发,插入最近点并最小化路径增量(案例图3逻辑)"""
    # 输入验证(符合案例对函数参数的要求)
    if startPoint1 == startPoint2:
        raise ValueError("Start points must be different")
    if startPoint1 not in pointsL or startPoint2 not in pointsL:
        raise ValueError("Start points must be in pointsL")

    path = [startPoint1, startPoint2]
    remaining = [p for p in pointsL if p not in path]

    # 核心逻辑:选最近点→找最小插入位置→插入
    while remaining:
        # 步骤1:找到距离当前路径最近的点
        def dist_to_path(point):
            return min(point.dist_to_point(p) for p in path)

        best_point = min(remaining, key=dist_to_path)

        # 步骤2:找到插入后路径增量最小的位置
        min_increase = math.inf
        best_pos = 0
        for i in range(len(path)):
            a, b = path[i], path[(i + 1) % len(path)]
            original_dist = a.dist_to_point(b)
            new_dist = a.dist_to_point(best_point) + best_point.dist_to_point(b)
            increase = new_dist - original_dist
            if increase < min_increase:
                min_increase = increase
                best_pos = i + 1

        # 步骤3:插入点并更新剩余列表
        path.insert(best_pos, best_point)
        remaining.remove(best_point)

    path.append(startPoint1)  # 闭合路径
    return Route(path)
相关推荐
koo3644 小时前
李宏毅机器学习笔记21
人工智能·笔记·机器学习
爱吃橘的橘猫4 小时前
嵌入式系统与嵌入式 C 语言(2)
c语言·算法·嵌入式
235164 小时前
【LeetCode】146. LRU 缓存
java·后端·算法·leetcode·链表·缓存·职场和发展
一壶浊酒..4 小时前
ajax局部更新
前端·ajax·okhttp
Bony-4 小时前
奶茶销售数据分析
人工智能·数据挖掘·数据分析·lstm
木头左4 小时前
波动率聚类现象对ETF网格密度配置的启示与应对策略
python
山烛5 小时前
YOLO v1:目标检测领域的单阶段革命之作
人工智能·yolo·目标检测·计算机视觉·yolov1
Wx-bishekaifayuan5 小时前
基于微信小程序的社区图书共享平台设计与实现 计算机毕业设计源码44991
javascript·vue.js·windows·mysql·pycharm·tomcat·php
华仔AI智能体5 小时前
Qwen3(通义千问3)、OpenAI GPT-5、DeepSeek 3.2、豆包最新模型(Doubao 4.0)通用模型能力对比
人工智能·python·语言模型·agent·智能体