TSP 求解:KDTree + DFJ 算法实现与实践¶
约 1971 个字 67 行代码 1 张图片 预计阅读时间 7 分钟
一、核心目标¶
实现一种高效的旅行商问题(TSP)求解方案,通过KDTree近邻稀疏化减少变量规模,结合DFJ(Dantzig-Fulkerson-Johnson)子回路消除算法,在保证解质量的前提下提升大规模TSP问题的求解效率。
二、技术框架与依赖库¶
1. 核心依赖¶
| 库名 | 用途 |
|---|---|
coptpy | COPT优化器接口,构建整数规划模型 |
numpy | 向量化计算距离矩阵、高效数据处理 |
scipy.spatial.KDTree | 快速近邻查找,实现边稀疏化 |
matplotlib | 路径可视化展示 |
collections.defaultdict | 子回路检测中的连通分量收集 |
2. 整体架构¶
TSPSolver类(核心求解器)
├── 数据层:read_data() 读取TSP文件,解析坐标与距离类型
├── 预处理层:
│ ├── construct_dist_matrix() 构建距离矩阵(支持EUC_2D/MAN_2D/GEOM)
│ ├── get_k_neighbors_kdtree() KDTree近邻查找,生成稀疏边集
│ └── _adaptive_k() 自适应确定近邻数k
├── 求解层:
│ ├── solve_tsp() 总调度函数
│ ├── DFJ_solver() 整数规划建模+子回路消除
│ ├── _fast_find_subtours() 并查集检测子回路
│ └── _build_route() 从解中构建完整路径
└── 辅助功能:
├── validate_solution() 解验证(路径完整性、距离一致性)
└── plot_tsp_route() 结果可视化 三、关键技术细节¶
1. TSP数据解析(read_data())¶
-
支持标准TSP文件格式,自动识别
EDGE_WEIGHT_TYPE(距离类型) -
提取
NODE_COORD_SECTION中的节点坐标,过滤无效数据 -
输出关键信息:数据量、距离类型、读取耗时
2. 距离矩阵构建(construct_dist_matrix())¶
-
支持3种主流距离计算:
-
EUC_2D:欧几里得距离(四舍五入保留4位小数) -
MAN_2D:曼哈顿距离(绝对值和) -
GEOM:几何距离(欧几里得距离取整)
-
-
采用向量化计算(
numpy广播机制),避免循环,提升效率
3. KDTree近邻稀疏化(get_k_neighbors_kdtree())¶
核心思想¶
TSP问题的完全图(n个节点有n(n-1)/2条边)变量规模过大,通过只保留每个节点的k个近邻边,大幅减少变量数,加速求解。
实现步骤¶
-
构建KDTree索引,查询每个节点的k+1个近邻(排除自身)
-
保证双向近邻:若i是j的近邻,则j也加入i的近邻集
-
限制近邻数上限(k×2),避免稀疏化过度
-
输出排序列表形式的近邻矩阵,用于后续变量创建
自适应k值策略(_adaptive_k())¶
根据节点数量动态调整k,平衡稀疏化率与解质量:
| 节点数n | k值 | 节点数n | k值 |
|---|---|---|---|
| ≤50 | 8 | 301~400 | 18 |
| 51~100 | 10 | 401~500 | 20 |
| 101~200 | 12 | 501~1000 | 25 |
| 201~300 | 15 | 1001~2000 | 30 |
| >2000 | 35 |
4. DFJ算法求解(DFJ_solver())¶
整数规划模型构建¶
-
变量:
x[(i,j)](二进制变量),表示边(i,j)是否被选中 -
目标函数:最小化总路程,
min ∑x[(i,j)]×dist(i,j) -
约束条件:
-
度约束:每个节点的入度=出度=2(
∑x[(i,j)]=2) -
子回路消除约束(懒惰约束):对每个子回路S,
∑x[(i,j)]≤|S|-1
-
关键优化¶
-
懒惰约束(Lazy Constraints):不预先添加所有子回路约束,求解过程中动态检测并添加,减少初始约束数量
-
并查集子回路检测(
_fast_find_subtours()):-
路径压缩+按秩合并优化,高效查找连通分量
-
过滤出长度2≤|S|<n的子回路(完整回路无需约束)
-
-
批量添加约束:一次迭代中添加所有检测到的子回路约束,减少求解器调用次数
-
求解参数调优:
-
线程数自动分配(
Threads=-1) -
强预处理(
Presolve=2) -
时间限制1小时(
TimeLimit=3600) -
关闭日志输出(
Logging=0)
-
5. 路径构建与验证¶
路径构建(_build_route())¶
-
从活跃边集(
active_edges)出发,验证每个节点度为2 -
多起点尝试(前10个节点),避免单一起点构建失败
-
形成闭合回路(起点=终点),路径长度为n+1(含起点重复)
解验证(validate_solution())¶
-
路径长度验证:必须为n+1
-
节点覆盖验证:所有节点必须被访问一次
-
距离一致性验证:计算路径实际距离与求解器结果误差≤0.5
6. 可视化(plot_tsp_route())¶
-
双面板布局:左图展示路径(起点标绿星,节点标红,边标蓝),右图展示关键信息
-
自适应图大小:n>500时扩大画布,避免节点重叠
-
节点标签偏移优化:奇偶节点交替偏移,提升可读性
-
保存高清图片(dpi=600),便于结果存档
四、使用说明¶
1. 环境准备¶
# 安装依赖
pip install coptpy numpy scipy matplotlib - 注意:
coptpy需配合COPT优化器(可申请免费许可证)
2. 数据格式¶
支持标准TSPLIB格式文件,核心段落示例:
EDGE_WEIGHT_TYPE: EUC_2D
NODE_COORD_SECTION
1 41.87500 45.00000
2 39.37500 45.00000
...
EOF 3. 运行方式¶
修改主程序中的test_files列表,添加目标TSP文件路径:
if __name__ == "__main__":
test_files = [
"data/ch600.tsp" # 替换为你的TSP文件路径
]
for filename in test_files:
solver = TSPSolver(filename)
model, obj_val, route, active_edges, time_total = solver.solve_tsp()
# 后续验证与可视化... 五、性能特点¶
优势¶
-
稀疏化高效:KDTree近邻查找时间复杂度O(n log n),变量数仅为完全图的5%~30%
-
解质量有保障:DFJ算法是TSP精确算法,稀疏化仅剔除远邻边(对最优解影响极小)
-
鲁棒性强:多起点路径构建、双向近邻、自适应k值,适应不同规模数据
-
结果可追溯:完整的日志输出+高清可视化+解验证,便于结果分析
适用场景¶
-
节点数n≤2000的TSP问题(n=600时求解时间约数十分钟)
-
距离类型为EUC_2D/MAN_2D/GEOM的TSPLIB标准问题
-
对解质量要求高(需精确解或高质量可行解)的场景
性能基准¶
| 节点数 | 完全变量数 | 稀疏化率 | 求解时间 |
|---|---|---|---|
| 100 | 602 | 12.2% | 1.58s |
| 300 | 2570 | 5.7% | 16.7s |
| 500 | 5622 | 4.5% | 22.20s |
| 600 | 8373 | 4.7% | 123.55s |
时间复杂度分析¶
| 模块 | 函数/方法 | 时间复杂度 | 说明 |
|---|---|---|---|
| 数据读取 | read_data() | O(n) | n为节点数,线性读取文件 |
_adaptive_k() | O(1) | 常数时间判断 | |
| 距离计算 | construct_dist_matrix() | O(n²) | 向量化广播操作,虽然理论O(n²)但NumPy优化后常数因子小 |
| 邻居查找 | get_k_neighbors_kdtree() | O(n log n) | KDTree构建O(n log n),查询O(n log n + nk),双向修复O(nk) |
| 建模求解 | DFJ_solver() | 最坏:指数级 实际:O(nk × I) | nk为变量数,I为迭代次数,受子回路数量影响 |
_fast_find_subtours() | O(α(n)×E) | E为激活边数,α(n)为反阿克曼函数,接近常数 | |
| 路径构建 | _build_route() | O(n) | 线性遍历节点 |
| 验证 | validate_solution() | O(n) | 向量化验证 |
| 绘图 | plot_tsp_route() | O(n) | 绘图操作,与节点数线性相关 |
空间复杂度分析¶
| 模块 | 函数/方法 | 空间复杂度 | 说明 |
|---|---|---|---|
| 数据存储 | 坐标存储 | O(n) | 存储n个二维坐标 |
| 距离矩阵 | dist_matrix | O(n²) | 主要内存消耗点,n×n浮点数矩阵 |
| 邻居矩阵 | neighbor_matrix | O(nk) | n个列表,每个最多k个邻居 |
| 求解变量 | x 变量字典 | O(nk) | 存储nk/2个二进制变量 |
| 子回路检测 | _fast_find_subtours() | O(n) | 并查集parent数组+size数组 |
| 路径存储 | route, active_edges | O(n) | 存储路径和邻接表 |
六、关键输出日志示例¶
求解问题: data/ch600.tsp
读取数据耗时: 0.0004 秒
K值取25
数据处理完成,共有600条数据
自动识别距离类型:EDGE_WEIGHT_TYPE = EUC_2D
构建距离矩阵耗时: 0.0080 秒
KDTree构建邻居矩阵耗时: 0.0062 秒
创建了8373个变量(稀疏化率: 4.7%)
--- 迭代 0 ---
求解状态: 6 (FEASIBLE)
发现 12 个子回路
添加了 12 个子回路约束
--- 迭代 1 ---
求解状态: 6 (FEASIBLE)
发现 8 个子回路
添加了 8 个子回路约束
...
求解状态: 1 (OPTIMAL)
迭代 20: 无子回路,找到可行解
求解成功!最优总路程 = 20448.9516
DFJ算法求解耗时: 123.5356 秒
总求解时间: 123.55秒
验证通过!求解器=20448.9516,计算距离=20448.9516
七、注意事项与改进方向¶
注意事项¶
-
COPT许可证:需提前申请,否则无法调用求解器
-
内存限制:n>2000时,距离矩阵(n×n)可能占用较多内存,可考虑分块计算
-
时间限制:n=1000时可能需要数小时求解,可调整
TimeLimit参数
改进方向¶
-
混合启发式:在DFJ之前加入贪心算法(如最近邻)生成初始可行解,加速收敛
-
动态k值:根据求解进度调整k值(如迭代后期增大k,避免错过最优边)
-
并行计算:子回路检测部分可并行化,提升大规模问题处理速度
-
多目标优化:加入路径平滑性、节点优先级等约束
-
支持更多距离类型:如ATT、CEIL_2D等TSPLIB其他距离类型
-
证明最优性
八、总结¶
本实现通过KDTree稀疏化+DFJ子回路消除的组合策略,有效平衡了TSP问题的求解效率与解质量。
核心亮点在于:
-
自适应近邻策略,无需手动调整k值
-
向量化与并查集优化,提升关键步骤效率
-
完整的解验证与可视化流程,确保结果可靠性
-
兼容标准TSPLIB格式,通用性强
局限性在于:
- 绝对最优性:稀疏化可能排除全局最优解(虽概率低)
- 内存限制:距离矩阵仍需O(n²)内存,超大规模问题需分块
- 求解器依赖:依赖COPT商业求解器,开源替代有限
- 确定性问题:TSP是NP-hard,指数级复杂度无法避免
九、完整代码¶
完整的实现代码已保存在 tsp_kdtree_dfj.py 文件中。
代码包含以下主要模块:
- TSPSolver 类:核心求解器,包含数据读取、距离矩阵构建、KDTree 近邻查找、DFJ 求解等方法
- plot_tsp_route 函数:结果可视化,生成在包含路径和统计信息的双面板图表
- validate_solution 函数:解验证,检查路径完整性和距离一致性
- 主程序:演示如何使用求解器进行完整的 TSP 求解流程
使用方法:
- 将 TSP 文件路径添加到
test_files列表 - 运行脚本:
python tsp_kdtree_dfj.py - 脚本会输出详细的求解过程日志和结果可视化图表
主要类和函数签名:
class TSPSolver:
__init__(filename) - 初始化求解器
read_data() - 读取 TSP 文件
construct_dist_matrix() - 构建距离矩阵
get_k_neighbors_kdtree(k) - 获取 k 近邻邻接表
solve_tsp() - 主求解函数
DFJ_solver(dist, neighbor_matrix) - DFJ 算法求解
def plot_tsp_route(co_dict, route, obj_val, time_total, k) - 绘制结果
def validate_solution(coords, route, obj_val, dist_matrix) - 验证解质量