ALNS算法实战用Java构建TSP求解器的可视化之旅1. 从零开始的TSP问题探索旅行商问题TSP是组合优化领域最经典的难题之一它要求找到访问所有城市并返回起点的最短路径。这个看似简单的问题背后隐藏着惊人的复杂性——对于48个城市的情况可能的路径数量比宇宙中原子的总数还要多为什么选择ALNS算法自适应大邻域搜索ALNS通过动态调整搜索策略在解空间中进行高效探索。与传统的启发式算法相比ALNS具有以下优势自适应选择算子根据历史表现动态调整算子权重多样化搜索通过破坏和修复操作跳出局部最优灵活性可针对不同问题设计专用算子我们的项目将使用Java实现一个完整的ALNS求解器并添加可视化功能让算法运行过程一目了然。以下是项目的主要组件// 核心类结构 TSP_Instance // 存储城市坐标和问题数据 TSP_Solution // 表示路径及其长度 TSP_Util // 工具类距离计算等 TSP_Solver_ALNS // ALNS算法实现 RunAndPlot // 可视化界面2. 数据准备与初始解构建2.1 读取TSPLIB数据我们从TSPLIB标准数据集加载城市坐标。以att48数据集为例它包含美国48个州首府的坐标NAME : att48 TYPE : TSP DIMENSION : 48 EDGE_WEIGHT_TYPE : ATT NODE_COORD_SECTION 1 6734 1453 2 2233 10 ... 48 3023 1942距离计算采用伪欧式距离公式public static double calcDistance(double[] p1, double[] p2) { return Math.ceil(Math.sqrt((Math.pow(p1[0]-p2[0],2)Math.pow(p1[1]-p2[1],2))/10d)); }2.2 构建初始解最简单的初始解是按城市编号顺序排列// 生成初始解 int[] initialPath new int[n]; for(int i0; in; i) { initialPath[i] i; } double initialLength TSP_Util.calcPathLen(initialPath, distances);初始路径长度通常很差att48约为49840这正是优化算法的起点。3. ALNS核心算法实现3.1 算法框架ALNS通过迭代执行破坏-修复过程改进解质量。基本流程如下初始化各算子的权重通常设为1.0主循环轮盘赌选择破坏算子轮盘赌选择修复算子评估新解更新算子权重输出最优解// ALNS主循环 for(int epoch0; epochepochs; epoch) { // 选择破坏算子 int destroyIndex roulette(destroyScores); PartX partX applyDestroyOperator(destroyIndex, currentPath); // 选择修复算子 int repairIndex roulette(repairScores); int[] newPath applyRepairOperator(repairIndex, partX); // 评估并更新解 updateSolution(newPath); // 冷却温度 c * alphaCooling; }3.2 破坏算子设计我们实现了三种破坏算子随机删除k个城市简单但有效删除随机片段保持部分路径连续性破坏交叉边针对性改进路径质量// 破坏算子1示例随机删除k个城市 PartX destroyOperator1(int[] X) { PartX partX new PartX(); int k random.nextInt(X.length); boolean[] removed new boolean[X.length]; for(int ik; i0; i--) { int r random.nextInt(X.length); while(removed[r]) r random.nextInt(X.length); removed[r] true; } for(int i0; iremoved.length; i) { if(removed[i]) partX.removeIndexList.add(i); else partX.indexList.add(i); } return partX; }3.3 修复算子设计对应的三种修复算子随机插入简单快速启发式插入选择最优插入位置追加到末尾保持部分破坏结果// 启发式插入修复示例 int[] repairOperator2(PartX partX) { for(int removeIndex : partX.removeIndexList) { int bestInsertIndex -1; double bestObj 0; for(int insertIndex0; insertIndexpartX.indexList.size(); insertIndex) { int left (insertIndex-10 ? n-1 : insertIndex-1); int right insertIndex; double obj calcDistance(locations[left], locations[removeIndex]) calcDistance(locations[removeIndex], locations[right]); if(bestInsertIndex0 || bestObjobj) { bestInsertIndex insertIndex; bestObj obj; } } partX.indexList.add(bestInsertIndex, removeIndex); } // 转换为完整路径 return convertToArray(partX); }3.4 自适应权重更新算子的权重根据其表现动态调整// 根据新解质量更新算子分数 if(newPathLen currentPathLen) { // 找到更好解 destroyScores[destroyIndex] lambda*destroyScores[destroyIndex] (1-lambda)*w2; repairScores[repairIndex] lambda*repairScores[repairIndex] (1-lambda)*w2; if(newPathLen bestPathLen) { // 找到全局最优解 destroyScores[destroyIndex] (1-lambda)*(w1-w2); repairScores[repairIndex] (1-lambda)*(w1-w2); } } else if(acceptBySA(newPathLen, currentPathLen)) { // 模拟退火接受劣解 destroyScores[destroyIndex] lambda*destroyScores[destroyIndex] (1-lambda)*w3; repairScores[repairIndex] lambda*repairScores[repairIndex] (1-lambda)*w3; } else { // 拒绝劣解 destroyScores[destroyIndex] lambda*destroyScores[destroyIndex] (1-lambda)*w4; repairScores[repairIndex] lambda*repairScores[repairIndex] (1-lambda)*w4; }4. JavaFX可视化实现4.1 路径绘制我们使用JavaFX的Path类绘制旅行商路径Path path new Path(); path.getElements().add(new MoveTo(p1[0], p1[1])); path.getElements().add(new LineTo(p2[0], p2[1])); pane.getChildren().add(path);4.2 动态展示通过Timeline实现路径的逐步绘制动画Timeline animation new Timeline(new KeyFrame(Duration.millis(50), event - { if(pCnt[0] bestPath.length-1) { drawSegment(bestPath[pCnt[0]], bestPath[pCnt[0]1]); pCnt[0]; } else if(pCnt[0] bestPath.length-1) { drawSegment(bestPath[pCnt[0]], bestPath[0]); pCnt[0]; } })); animation.setCycleCount(Timeline.INDEFINITE); animation.play();4.3 坐标适配将实际坐标转换为屏幕坐标Listdouble[] fitLocation(double[][] locations) { // 计算最大坐标值 double maxX Arrays.stream(locations).mapToDouble(p - p[0]).max().getAsDouble(); double maxY Arrays.stream(locations).mapToDouble(p - p[1]).max().getAsDouble(); // 计算缩放比例 double rateX (stageW-2*offsetX)/maxX; double rateY (stageH-2*offsetY)/maxY; // 转换所有坐标 return Arrays.stream(locations) .map(p - new double[]{p[0]*rateXoffsetX, p[1]*rateYoffsetY}) .collect(Collectors.toList()); }5. 算法优化与实践建议5.1 参数调优经验根据实际测试以下参数组合效果较好参数推荐值说明λ0.6-0.8权重衰减系数w11.5找到全局最优解的奖励w21.2找到局部改进的奖励w30.8接受劣解的奖励w40.1拒绝劣解的惩罚初始温度100-300模拟退火初始温度冷却系数0.95温度衰减系数5.2 算子设计技巧破坏算子应平衡随机性和针对性随机删除适合早期多样化搜索针对性破坏如交叉边适合后期精细化优化修复算子应包含随机性算子保持探索能力启发式算子加速收敛简单算子提高效率5.3 性能优化策略距离矩阵预计算避免重复计算增量评估只计算变化部分的路径长度并行化独立算子可以并行执行记忆化缓存优秀解片段// 增量评估示例 double evaluateInsertion(int[] path, int pos, int city) { int prev pos 0 ? path[path.length-1] : path[pos-1]; int next path[pos]; return -distances[prev][next] distances[prev][city] distances[city][next]; }6. 扩展与进阶方向6.1 混合算法设计将ALNS与其他优化技术结合ALNS禁忌搜索使用禁忌表避免循环ALNS遗传算法引入种群概念ALNS局部搜索在好解附近深度挖掘6.2 多目标优化考虑多目标TSP问题路径长度时间窗口约束车辆负载平衡风险最小化6.3 实时可视化增强改进可视化效果不同颜色表示路径质量实时显示算子使用频率交互式参数调整三维地形展示// 彩色路径示例 gc.setStroke(Color.hsb(120 pathLen/100, 1.0, 1.0)); gc.strokeLine(x1, y1, x2, y2);在实现过程中最令人惊喜的是看到算法逐步学习到哪些算子更有效——开始时各算子权重相同经过几百次迭代后表现好的算子权重会显著提高。这种自适应特性正是ALNS的强大之处。
ALNS算法入门实战:手把手教你用Java搞定旅行商问题(TSP)可视化
发布时间:2026/6/11 9:52:11
ALNS算法实战用Java构建TSP求解器的可视化之旅1. 从零开始的TSP问题探索旅行商问题TSP是组合优化领域最经典的难题之一它要求找到访问所有城市并返回起点的最短路径。这个看似简单的问题背后隐藏着惊人的复杂性——对于48个城市的情况可能的路径数量比宇宙中原子的总数还要多为什么选择ALNS算法自适应大邻域搜索ALNS通过动态调整搜索策略在解空间中进行高效探索。与传统的启发式算法相比ALNS具有以下优势自适应选择算子根据历史表现动态调整算子权重多样化搜索通过破坏和修复操作跳出局部最优灵活性可针对不同问题设计专用算子我们的项目将使用Java实现一个完整的ALNS求解器并添加可视化功能让算法运行过程一目了然。以下是项目的主要组件// 核心类结构 TSP_Instance // 存储城市坐标和问题数据 TSP_Solution // 表示路径及其长度 TSP_Util // 工具类距离计算等 TSP_Solver_ALNS // ALNS算法实现 RunAndPlot // 可视化界面2. 数据准备与初始解构建2.1 读取TSPLIB数据我们从TSPLIB标准数据集加载城市坐标。以att48数据集为例它包含美国48个州首府的坐标NAME : att48 TYPE : TSP DIMENSION : 48 EDGE_WEIGHT_TYPE : ATT NODE_COORD_SECTION 1 6734 1453 2 2233 10 ... 48 3023 1942距离计算采用伪欧式距离公式public static double calcDistance(double[] p1, double[] p2) { return Math.ceil(Math.sqrt((Math.pow(p1[0]-p2[0],2)Math.pow(p1[1]-p2[1],2))/10d)); }2.2 构建初始解最简单的初始解是按城市编号顺序排列// 生成初始解 int[] initialPath new int[n]; for(int i0; in; i) { initialPath[i] i; } double initialLength TSP_Util.calcPathLen(initialPath, distances);初始路径长度通常很差att48约为49840这正是优化算法的起点。3. ALNS核心算法实现3.1 算法框架ALNS通过迭代执行破坏-修复过程改进解质量。基本流程如下初始化各算子的权重通常设为1.0主循环轮盘赌选择破坏算子轮盘赌选择修复算子评估新解更新算子权重输出最优解// ALNS主循环 for(int epoch0; epochepochs; epoch) { // 选择破坏算子 int destroyIndex roulette(destroyScores); PartX partX applyDestroyOperator(destroyIndex, currentPath); // 选择修复算子 int repairIndex roulette(repairScores); int[] newPath applyRepairOperator(repairIndex, partX); // 评估并更新解 updateSolution(newPath); // 冷却温度 c * alphaCooling; }3.2 破坏算子设计我们实现了三种破坏算子随机删除k个城市简单但有效删除随机片段保持部分路径连续性破坏交叉边针对性改进路径质量// 破坏算子1示例随机删除k个城市 PartX destroyOperator1(int[] X) { PartX partX new PartX(); int k random.nextInt(X.length); boolean[] removed new boolean[X.length]; for(int ik; i0; i--) { int r random.nextInt(X.length); while(removed[r]) r random.nextInt(X.length); removed[r] true; } for(int i0; iremoved.length; i) { if(removed[i]) partX.removeIndexList.add(i); else partX.indexList.add(i); } return partX; }3.3 修复算子设计对应的三种修复算子随机插入简单快速启发式插入选择最优插入位置追加到末尾保持部分破坏结果// 启发式插入修复示例 int[] repairOperator2(PartX partX) { for(int removeIndex : partX.removeIndexList) { int bestInsertIndex -1; double bestObj 0; for(int insertIndex0; insertIndexpartX.indexList.size(); insertIndex) { int left (insertIndex-10 ? n-1 : insertIndex-1); int right insertIndex; double obj calcDistance(locations[left], locations[removeIndex]) calcDistance(locations[removeIndex], locations[right]); if(bestInsertIndex0 || bestObjobj) { bestInsertIndex insertIndex; bestObj obj; } } partX.indexList.add(bestInsertIndex, removeIndex); } // 转换为完整路径 return convertToArray(partX); }3.4 自适应权重更新算子的权重根据其表现动态调整// 根据新解质量更新算子分数 if(newPathLen currentPathLen) { // 找到更好解 destroyScores[destroyIndex] lambda*destroyScores[destroyIndex] (1-lambda)*w2; repairScores[repairIndex] lambda*repairScores[repairIndex] (1-lambda)*w2; if(newPathLen bestPathLen) { // 找到全局最优解 destroyScores[destroyIndex] (1-lambda)*(w1-w2); repairScores[repairIndex] (1-lambda)*(w1-w2); } } else if(acceptBySA(newPathLen, currentPathLen)) { // 模拟退火接受劣解 destroyScores[destroyIndex] lambda*destroyScores[destroyIndex] (1-lambda)*w3; repairScores[repairIndex] lambda*repairScores[repairIndex] (1-lambda)*w3; } else { // 拒绝劣解 destroyScores[destroyIndex] lambda*destroyScores[destroyIndex] (1-lambda)*w4; repairScores[repairIndex] lambda*repairScores[repairIndex] (1-lambda)*w4; }4. JavaFX可视化实现4.1 路径绘制我们使用JavaFX的Path类绘制旅行商路径Path path new Path(); path.getElements().add(new MoveTo(p1[0], p1[1])); path.getElements().add(new LineTo(p2[0], p2[1])); pane.getChildren().add(path);4.2 动态展示通过Timeline实现路径的逐步绘制动画Timeline animation new Timeline(new KeyFrame(Duration.millis(50), event - { if(pCnt[0] bestPath.length-1) { drawSegment(bestPath[pCnt[0]], bestPath[pCnt[0]1]); pCnt[0]; } else if(pCnt[0] bestPath.length-1) { drawSegment(bestPath[pCnt[0]], bestPath[0]); pCnt[0]; } })); animation.setCycleCount(Timeline.INDEFINITE); animation.play();4.3 坐标适配将实际坐标转换为屏幕坐标Listdouble[] fitLocation(double[][] locations) { // 计算最大坐标值 double maxX Arrays.stream(locations).mapToDouble(p - p[0]).max().getAsDouble(); double maxY Arrays.stream(locations).mapToDouble(p - p[1]).max().getAsDouble(); // 计算缩放比例 double rateX (stageW-2*offsetX)/maxX; double rateY (stageH-2*offsetY)/maxY; // 转换所有坐标 return Arrays.stream(locations) .map(p - new double[]{p[0]*rateXoffsetX, p[1]*rateYoffsetY}) .collect(Collectors.toList()); }5. 算法优化与实践建议5.1 参数调优经验根据实际测试以下参数组合效果较好参数推荐值说明λ0.6-0.8权重衰减系数w11.5找到全局最优解的奖励w21.2找到局部改进的奖励w30.8接受劣解的奖励w40.1拒绝劣解的惩罚初始温度100-300模拟退火初始温度冷却系数0.95温度衰减系数5.2 算子设计技巧破坏算子应平衡随机性和针对性随机删除适合早期多样化搜索针对性破坏如交叉边适合后期精细化优化修复算子应包含随机性算子保持探索能力启发式算子加速收敛简单算子提高效率5.3 性能优化策略距离矩阵预计算避免重复计算增量评估只计算变化部分的路径长度并行化独立算子可以并行执行记忆化缓存优秀解片段// 增量评估示例 double evaluateInsertion(int[] path, int pos, int city) { int prev pos 0 ? path[path.length-1] : path[pos-1]; int next path[pos]; return -distances[prev][next] distances[prev][city] distances[city][next]; }6. 扩展与进阶方向6.1 混合算法设计将ALNS与其他优化技术结合ALNS禁忌搜索使用禁忌表避免循环ALNS遗传算法引入种群概念ALNS局部搜索在好解附近深度挖掘6.2 多目标优化考虑多目标TSP问题路径长度时间窗口约束车辆负载平衡风险最小化6.3 实时可视化增强改进可视化效果不同颜色表示路径质量实时显示算子使用频率交互式参数调整三维地形展示// 彩色路径示例 gc.setStroke(Color.hsb(120 pathLen/100, 1.0, 1.0)); gc.strokeLine(x1, y1, x2, y2);在实现过程中最令人惊喜的是看到算法逐步学习到哪些算子更有效——开始时各算子权重相同经过几百次迭代后表现好的算子权重会显著提高。这种自适应特性正是ALNS的强大之处。