☰
钢板切割路径优化:从TSP到GTSP的建模与启发式算法实践
2026/9/26 18:53:43 网站建设 项目流程

1. 问题背景与核心挑战:从“一刀切”到“最优走刀”

五一数学建模竞赛的A题,每年都以其强烈的工程背景和现实意义吸引着众多参赛者。今年的“钢板最优切割路径问题”,乍一看似乎是一个经典的“旅行商问题”(TSP)变种——给定一堆需要切割的图形,找一条最短的路径把它们都“走”一遍。但如果你真这么想,那可能从一开始就掉进了坑里。这个问题的核心,远不止是“连线”那么简单,它融合了图论、几何、优化和工业常识,是一个典型的“纸上谈兵易,落地实现难”的综合性问题。

我们先来拆解一下题目通常隐含的几个关键约束和优化目标。首先,切割设备(比如激光切割头、等离子切割头)的移动分为两种状态:空程和切割。空程是指切割头从一个切割图形的终点,移动到下一个切割图形的起点,这个过程不进行切割,纯粹是“赶路”,消耗时间和能量,是需要极力最小化的部分。切割过程则是沿着图形的轮廓进行,路径是固定的(就是图形本身的边界),时间与轮廓长度成正比。因此,问题的核心目标就变成了:在必须完整切割所有给定图形的前提下,寻找一个访问所有图形的顺序,使得所有空程的总长度最短。

这里就引出了第一个容易被忽略的细节:切割的起点和终点。对于一个封闭图形(如题目中常见的矩形、圆形、异形件),理论上可以从轮廓上任意一点开始切割,并最终回到该点。但在实际工业切割中,为了减少热变形、保证切口质量,通常会指定一个“切入点”(Piercing Point),并且要求切割路径是连续的。在简化模型中,我们可以将每个图形抽象为一条需要遍历的“边”,其起点和终点可以是同一个点(闭合轮廓),也可以是两个不同的点(如果图形有预开口)。但更常见的处理方式是,将每个图形视为一个“节点”,但这个节点关联着一条闭合的、必须完整遍历的“哈密顿回路”。这大大增加了问题的复杂度,因为你需要为每个图形决定一个最优的“进入点”和“退出点”,而不仅仅是决定图形的访问顺序。

第二个关键点是空程路径的合法性。切割头在空程移动时,能否穿过已经切割好的图形?在实际生产中,如果图形已经被切下,钢板上的材料缺失,切割头理论上可以从“空洞”中穿过。但在许多题目设定或简化模型中,为了避免碰撞和简化计算,可能会规定空程路径不能与任何图形的切割轮廓线相交(因为轮廓线是实际切割路径,设备不能从正在切割或已切割的缝上跨过,这可能导致碰撞或精度问题)。更严格的限制是,空程路径甚至不能进入图形内部(即不能与图形区域相交)。这就需要我们在计算两点间最短空程时,不是简单地计算欧几里得距离,而是要在一个充满“障碍物”(即待切割图形)的平面上,寻找一条无碰撞的最短路径。这瞬间将问题从单纯的组合优化,升级为“带障碍物的最短路径规划”问题。

所以,一个完整的“钢板最优切割路径问题”模型,通常包含三层优化:

  1. 图形内部路径优化:为每个图形确定一个切割起始点,并规划一条高效的内部切割顺序(对于简单图形,这就是其轮廓;对于复杂图形,可能涉及内部镂空等)。
  2. 图形间访问顺序优化:决定先切哪个图形,再切哪个图形,这是一个排序问题。
  3. 图形间空程路径优化:在确定了访问顺序后,对于每一对相邻的图形,需要计算从前一个图形的退出点到后一个图形的进入点之间,避开所有障碍的最短可行路径。

这三层问题相互耦合,构成了一个NP-Hard的复杂优化问题。直接求全局最优解几乎不可能,因此竞赛中的思路通常是设计合理的启发式算法或元启发式算法,在可接受的时间内找到一个高质量的近似最优解。

2. 核心模型构建:如何将钢板转化为可计算的图

面对这样一个复杂问题,直接对连续平面进行搜索是不现实的。我们必须对问题进行离散化和抽象,构建一个可以运算的数学模型。最有效的方法之一,就是图论建模。

2.1 关键点的提取与图的构建

我们的目标是找到一条路径,因此首先需要定义路径的“途经点”。一个自然的想法是,将每个待切割图形的轮廓上一系列关键点作为候选的“进入/退出点”。对于矩形,我们可以选择四个角点;对于圆形,可以选择圆周上等间隔的若干点;对于复杂多边形,可以选择所有顶点。设我们有N个图形,第i个图形提取了M_i个关键点。

接下来,我们构建一个完全图G = (V, E)。图的顶点集V包含以下几类点:

  1. 设备起始点:通常位于钢板一角或外部,记为S。
  2. 每个图形的所有关键点:将第i个图形的M_i个关键点都加入V。
  3. (可选)钢板边界上的辅助点:为了规划绕开障碍的空程路径,有时需要在钢板边界上添加一些辅助点。

图的边集E理论上连接所有顶点对(u, v)。每条边都有一个权重w(u, v),代表从点u移动到点v的代价。这里的代价计算是模型的核心,需要区分两种情况:

  • 切割边:如果u和v属于同一个图形的轮廓,且沿着轮廓从u到v是连续切割的一部分,那么权重w(u, v)就是这两点间沿着轮廓的路径长度。这代表了切割这段轮廓所需的时间/成本。
  • 空程边:如果u和v属于不同图形,或者属于同一图形但路径不连续(比如直接穿越图形内部),那么这条边代表一次空程移动。其权重w(u, v)应该是从u到v的最短无碰撞路径长度。

注意:计算“最短无碰撞路径长度”本身就是一个子问题(比如使用A*算法、Dijkstra算法或在可视性图中搜索)。在竞赛的简化模型中,如果允许空程穿越图形内部或忽略碰撞,则权重就是欧几里得距离。但更严谨的做法是构建“可视性图”(Visibility Graph),图中只连接那些相互“可见”(即连线不穿过任何图形内部)的点对,边的权重就是直线距离。这样,在可视性图中任意两点间的最短路径,就是实际平面上的最短无碰撞路径。

2.2 问题转化为广义旅行商问题(GTSP)

构建好图G之后,我们的问题可以表述为:寻找一条从起点S出发,访问所有图形恰好一次,最后返回起点S(或不返回)的环路,使得总代价最小。

但注意,我们访问的基本单位是“图形”,而不是图的“顶点”。每个图形对应一组顶点(其关键点)。我们必须从每个图形对应的顶点组中,选择恰好一个顶点作为该图形的“访问点”(即切割进入点)。同时,路径必须依次经过这些被选中的顶点。

这正是一个经典的广义旅行商问题(Generalized Traveling Salesman Problem, GTSP)的描述。在GTSP中,所有顶点被划分为若干个互不相交的簇(Cluster),要求找到一条最短环路,它访问每个簇恰好一次。

我们的模型完美匹配:

  • 簇:每个待切割图形及其所有关键点构成一个簇。
  • 访问:路径必须从每个簇中选取一个顶点访问。
  • 目标:最小化路径总长度,其中路径由“切割边”(簇内边)和“空程边”(簇间边)组成。

将问题转化为GTSP是至关重要的一步,因为它为我们提供了明确的算法目标和丰富的现有研究可以参考。GTSP同样是NP-Hard的,但已有许多成熟的启发式算法,如转换到标准TSP求解、蚁群算法、遗传算法等。

2.3 模型补充:切割方向与空程约束

在实际切割中,还有两个因素可能需要考虑:

  1. 切割方向:对于某些图形(如细长条),沿着长边切割和沿着短边切割,产生的热变形和切割质量可能不同。这可能会影响我们对图形“进入点”的选择偏好。在模型中,这可以通过为同一个图形的不同关键点设置不同的“惩罚权重”来体现。例如,从短边中点进入的代价比从角点进入的代价略低。

  2. 空程约束:如前所述,空程路径不能与图形相交。这在构建可视性图或计算边权重时已经考虑。但如果题目要求空程路径必须完全在钢板内部,还需要额外判断路径点是否在钢板边界内。

3. 算法策略设计:从精确到启发式的求解思路

面对GTSP,我们有多种求解策略,需要根据题目规模(图形数量、复杂度)和计算时间限制进行选择。

3.1 精确算法:整数规划(适用于小规模问题)

对于图形数量很少(例如N ≤ 15)的情况,可以考虑建立整数规划模型,使用CPLEX、Gurobi等求解器求精确解。模型可以定义二元决策变量:

  • x_{ij}:是否从顶点i直接移动到顶点j(i, j属于不同簇或为起点)。
  • y_{ik}:是否选择图形k的候选点i作为该图形的访问点。

目标函数是最小化所有被选中的边的权重之和。约束条件包括:每个图形必须选择一个访问点;流量守恒(进入一个顶点的次数等于离开的次数);子回路消除约束(防止形成多个不连通的环)等。这种方法能保证最优解,但规模稍大就会导致变量和约束爆炸,求解时间不可控。

3.2 启发式算法:两阶段法(最常用、最实用)

对于竞赛中更常见的规模(N在几十到上百),两阶段法是平衡效果和效率的黄金选择。

第一阶段:为每个图形确定最优进入点。这一步可以独立进行。对于每个图形,我们评估其所有候选关键点。评估标准可以是:该点距离其他图形的平均距离、该点距离起点的距离、或者基于局部切割工艺的偏好。一个简单有效的策略是,对于每个图形,选择其轮廓上距离钢板中心或距离设备起点最近的点作为进入点。这样做的逻辑是,希望从相对中心的位置开始切割,可能减少后续空程。确定进入点后,每个图形就被简化为一个“代表点”。

第二阶段:解决标准旅行商问题(TSP)。现在,我们有一组代表点(包括起点)。问题简化为:从起点出发,访问所有代表点一次并返回(或不返回),求最短环路。这就是经典的TSP。虽然TSP也是NP-Hard,但针对几十到上百个点的TSP,已有非常高效的启发式算法能快速找到高质量解。

常用TSP启发式算法:

  • 最近邻算法(Nearest Neighbor):从起点开始,每次都前往最近的未访问点。速度快,但解的质量一般,容易在最后留下很长的边。
  • 插入法(Insertion):从一个包含少数点的子环路开始,不断将剩余点以最小代价增量插入到环路的合适位置。比最近邻法效果更好。
  • 2-opt / 3-opt局部搜索:对一个初始环路(可由上述方法生成),不断尝试交换两条或三条边的连接方式,如果能使总距离缩短就接受交换,直到无法改进。这是提升解质量的利器,几乎必用。
  • 蚁群算法(ACO)、遗传算法(GA):更高级的元启发式算法,能更好地跳出局部最优,适合对解质量要求极高的场景。在竞赛中,用Python的DEAP库实现遗传算法,或用ACO-Pants等库实现蚁群算法,是很好的加分项。

两阶段法的优势与不足:优势在于将复杂问题解耦,大大降低了求解难度。不足在于,第一阶段确定进入点时,没有考虑后续图形间的顺序,可能导致局部最优而非全局最优。例如,一个图形的“最优”进入点可能使它远离大多数其他图形,从而在第二阶段TSP中产生很长的空程。为了缓解这个问题,可以引入迭代改进:在第二阶段得到TSP顺序后,反过来调整每个图形的进入点,选择距离“前驱图形退出点”和“后继图形进入点”更近的点,如此反复迭代几次。

3.3 元启发式算法:直接求解GTSP

如果想追求更高的模型完整性和解的质量,可以直接设计元启发式算法来求解我们构建的GTSP模型。以遗传算法为例:

  1. 编码:一条染色体表示一个解。可以采用两层编码:第一层是图形的访问顺序排列(一个排列序列);第二层是对应于每个图形,所选择的进入点在候选点列表中的索引。
  2. 适应度函数:就是总路径代价的倒数(最小化问题)。计算适应度时,需要根据染色体解码出的顺序和进入点,计算切割路径(图形轮廓)和空程路径(可视性图中的最短路径)的总和。
  3. 遗传操作:
    • 交叉:对访问顺序部分,可以采用顺序交叉(OX)、部分映射交叉(PMX);对进入点选择部分,可以采用单点交叉。
    • 变异:对访问顺序部分,可以随机交换两个位置;对进入点选择部分,可以随机改变某个图形的进入点索引。
  4. 运行:通过选择、交叉、变异迭代演化种群,最终收敛到一个较优解。

这种方法理论上能搜索到更好的解,但计算量巨大,因为每评估一个解的适应度,都需要计算N段空程路径(每段都需要路径搜索算法)。在时间有限的竞赛中,需要精心设计,比如缓存常见点对间的最短路径,或者使用更快的路径估计算法。

4. 参考代码框架与实现细节

这里提供一个基于Python的两阶段法求解框架,使用networkx处理图论,shapely处理几何关系,ortools求解TSP。这个框架清晰且易于扩展。

import numpy as np import matplotlib.pyplot as plt from shapely.geometry import Point, Polygon, LineString import networkx as nx from ortools.constraint_solver import routing_enums_pb2 from ortools.constraint_solver import pywrapcp # ========== 第一阶段:数据处理与进入点选择 ========== class CuttingShape: def __init__(self, shape_id, vertices): """ vertices: 列表,表示图形轮廓的顶点坐标 [(x1,y1), (x2,y2), ...] 假设最后一个点与第一个点相连形成闭合多边形。 """ self.id = shape_id self.polygon = Polygon(vertices) self.vertices = vertices # 候选进入点:这里简单取所有顶点。可以优化,如取各边中点。 self.candidate_points = vertices def select_entry_point(self, strategy='centroid', reference_point=None): """为图形选择一个进入点""" if strategy == 'centroid': # 选择距离图形几何中心最近的点 centroid = self.polygon.centroid return min(self.candidate_points, key=lambda p: Point(p).distance(centroid)) elif strategy == 'nearest_to_ref': # 选择距离参考点(如上个图形退出点或起点)最近的点 ref = Point(reference_point) return min(self.candidate_points, key=lambda p: Point(p).distance(ref)) else: # 默认返回第一个顶点 return self.candidate_points[0] def load_shapes_from_file(filepath): """从文件加载图形数据,这里需要根据题目数据格式自定义""" shapes = [] # 假设数据格式:每行一个图形,顶点坐标用空格分隔 with open(filepath, 'r') as f: for idx, line in enumerate(f): coords = list(map(float, line.strip().split())) vertices = [(coords[i], coords[i+1]) for i in range(0, len(coords), 2)] shapes.append(CuttingShape(idx, vertices)) return shapes # ========== 构建可视性图(用于计算无碰撞空程) ========== def build_visibility_graph(shapes, start_point, boundary_polygon): """ 构建可视性图。 shapes: 所有切割图形对象列表。 start_point: 设备起点。 boundary_polygon: 钢板边界多边形,用于约束空程在钢板内。 """ G = nx.Graph() # 添加所有节点:起点 + 所有图形的所有候选点 all_points = [start_point] + [p for shape in shapes for p in shape.candidate_points] node_ids = list(range(len(all_points))) pos_dict = {i: all_points[i] for i in node_ids} # 判断两点连线是否与任何图形内部相交(碰撞检测) def is_collision_free(p1, p2): line = LineString([p1, p2]) # 空程不能穿过任何图形内部 for shape in shapes: if line.crosses(shape.polygon) or line.within(shape.polygon): return False # 空程必须在钢板边界内(如果要求) if boundary_polygon and not line.within(boundary_polygon): return False return True # 添加边:连接所有相互可见的点对 for i in range(len(all_points)): for j in range(i+1, len(all_points)): if is_collision_free(all_points[i], all_points[j]): distance = Point(all_points[i]).distance(Point(all_points[j])) G.add_edge(i, j, weight=distance) return G, pos_dict, all_points # ========== 第二阶段:求解TSP(访问代表点) ========== def solve_tsp_with_ortools(distance_matrix): """使用OR-Tools求解TSP,返回访问顺序和总距离""" manager = pywrapcp.RoutingIndexManager(len(distance_matrix), 1, 0) # 1辆车,起点为0 routing = pywrapcp.RoutingModel(manager) def distance_callback(from_index, to_index): from_node = manager.IndexToNode(from_index) to_node = manager.IndexToNode(to_index) return int(distance_matrix[from_node][to_node] * 1000) # 转换为整数 transit_callback_index = routing.RegisterTransitCallback(distance_callback) routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index) # 设置搜索参数 search_parameters = pywrapcp.DefaultRoutingSearchParameters() search_parameters.first_solution_strategy = ( routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC) search_parameters.local_search_metaheuristic = ( routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH) search_parameters.time_limit.seconds = 30 # 时间限制 solution = routing.SolveWithParameters(search_parameters) if solution: index = routing.Start(0) route = [] total_distance = 0 while not routing.IsEnd(index): route.append(manager.IndexToNode(index)) previous_index = index index = solution.Value(routing.NextVar(index)) total_distance += routing.GetArcCostForVehicle(previous_index, index, 0) route.append(manager.IndexToNode(index)) # 添加终点 return route, total_distance / 1000.0 # 转换回浮点数 else: return None, float('inf') def calculate_total_path(shapes, entry_points, visit_order, visibility_graph, all_points, start_idx=0): """根据访问顺序和进入点,计算总路径长度(切割+空程)""" total_len = 0.0 full_path = [all_points[start_idx]] # 起点 current_point = all_points[start_idx] # 将图形ID映射到其进入点在all_points中的索引和坐标 shape_id_to_entry_idx = {} for shape in shapes: for idx, pt in enumerate(all_points): if pt == entry_points[shape.id]: shape_id_to_entry_idx[shape.id] = idx break for i in range(len(visit_order)): target_shape_id = visit_order[i] target_entry_idx = shape_id_to_entry_idx[target_shape_id] target_point = all_points[target_entry_idx] # 1. 空程:从current_point到target_point if current_point != target_point: # 使用networkx的最短路径算法计算无碰撞空程 try: # 找到当前点和目标点在图中的节点索引 current_idx = all_points.index(current_point) # 注意:效率低,可预先建立映射 target_idx = all_points.index(target_point) sp_length = nx.shortest_path_length(visibility_graph, source=current_idx, target=target_idx, weight='weight') total_len += sp_length # 获取具体路径点(可选,用于可视化) # sp_path = nx.shortest_path(visibility_graph, source=current_idx, target=target_idx, weight='weight') # for node in sp_path[1:]: # full_path.append(all_points[node]) except nx.NetworkXNoPath: print(f"警告:点{current_point}到点{target_point}无可行空程路径,使用直线距离。") total_len += Point(current_point).distance(Point(target_point)) full_path.append(target_point) # 2. 切割:遍历该图形轮廓 shape = shapes[target_shape_id] # 简化:切割长度即为图形周长 cutting_length = shape.polygon.length total_len += cutting_length # 记录切割后的位置(假设退出点就是进入点,闭合切割) current_point = target_point # 切割结束点 full_path.append(current_point) # 记录切割结束点 # 最后空程返回起点(如果要求) # if current_point != all_points[start_idx]: # total_len += nx.shortest_path_length(visibility_graph, source=all_points.index(current_point), target=start_idx, weight='weight') return total_len, full_path # ========== 主程序流程 ========== def main(): # 1. 加载数据 shapes = load_shapes_from_file('shapes_data.txt') start_point = (0, 0) # 设备起点 boundary = None # 钢板边界,可以用Polygon定义 # 2. 第一阶段:选择进入点(简单策略:距离起点最近的点) entry_points = {} for shape in shapes: entry = shape.select_entry_point(strategy='nearest_to_ref', reference_point=start_point) entry_points[shape.id] = entry # 3. 构建可视性图 print("正在构建可视性图...") vis_graph, pos_dict, all_points = build_visibility_graph(shapes, start_point, boundary) # 4. 构建代表点距离矩阵(用于TSP) # 代表点:起点 + 每个图形的进入点 representative_points = [start_point] + [entry_points[s.id] for s in shapes] num_reps = len(representative_points) # 创建距离矩阵,距离为可视性图中的最短路径长度 print("正在计算代表点间距离矩阵...") dist_matrix = np.zeros((num_reps, num_reps)) # 建立代表点坐标到其在all_points中索引的映射 rep_indices = [all_points.index(p) for p in representative_points] for i in range(num_reps): for j in range(num_reps): if i == j: dist_matrix[i][j] = 0 else: try: dist_matrix[i][j] = nx.shortest_path_length(vis_graph, source=rep_indices[i], target=rep_indices[j], weight='weight') except: # 如果不可达,赋予一个大值 dist_matrix[i][j] = 1e9 # 5. 第二阶段:求解TSP(访问代表点,起点固定为0) print("正在求解TSP...") # OR-Tools要求从0号代表点(起点)出发并返回 route_indices, tsp_distance = solve_tsp_with_ortools(dist_matrix) if route_indices: # route_indices包含起点和终点,且首尾都是起点(0)。我们只需要图形的访问顺序。 # 例如 route_indices = [0, 3, 1, 2, 0], 则图形访问顺序为 [3,1,2] 对应的图形ID # 注意:representative_points[0]是起点,representative_points[1:]对应图形进入点 shape_order = [] for idx in route_indices[1:-1]: # 去掉首尾的起点 # idx是代表点列表中的索引,需要转换为图形ID # representative_points中,索引0是起点,索引1对应shapes[0], 索引2对应shapes[1]... shape_id = idx - 1 shape_order.append(shape_id) print(f"图形访问顺序: {shape_order}") print(f"TSP环路空程距离: {tsp_distance:.2f}") # 6. 计算包含切割长度的总路径 total_length, full_path = calculate_total_path(shapes, entry_points, shape_order, vis_graph, all_points, start_idx=rep_indices[0]) print(f"预估总路径长度(空程+切割): {total_length:.2f}") # 7. 可视化(可选) plot_results(shapes, start_point, full_path, entry_points) else: print("未找到可行TSP路径。") def plot_results(shapes, start, path_points, entries): fig, ax = plt.subplots(figsize=(12, 8)) # 绘制所有图形 for shape in shapes: x, y = shape.polygon.exterior.xy ax.fill(x, y, alpha=0.3, fc='lightblue', ec='black') ax.text(shape.polygon.centroid.x, shape.polygon.centroid.y, str(shape.id), ha='center', va='center') # 标记进入点 ep = entries[shape.id] ax.plot(ep[0], ep[1], 'ro', markersize=8) # 绘制起点 ax.plot(start[0], start[1], 'g*', markersize=15, label='Start') # 绘制路径 path_x = [p[0] for p in path_points] path_y = [p[1] for p in path_points] ax.plot(path_x, path_y, 'r-', linewidth=1.5, label='Cutting Path') ax.plot(path_x, path_y, 'bo', markersize=4) # 路径点 ax.set_aspect('equal') ax.grid(True, linestyle='--', alpha=0.7) ax.legend() ax.set_title("Optimal Cutting Path Layout") plt.show() if __name__ == '__main__': main()

代码关键点与注意事项:

  1. 几何处理库:使用shapely进行几何关系判断(相交、包含、距离)非常方便且可靠,避免了手动实现复杂几何运算的麻烦。
  2. 可视性图构建:build_visibility_graph函数是核心。它通过遍历所有点对并检查连线是否与图形相交,来构建一个无碰撞的路径网络。这是一个O(N²)的操作,当点数很多时会变慢。在实际竞赛中,如果图形数量上百,可能需要优化,例如使用空间索引(R-tree)来快速筛选可能相交的图形。
  3. TSP求解器:OR-Tools是Google开源的强大优化工具包,其TSP求解器非常高效。我们将其封装在solve_tsp_with_ortools中。注意,它需要整数成本,因此我们将浮点距离乘以一个因子(如1000)转换为整数。
  4. 两阶段耦合:在主函数main中,我们先基于“距离起点最近”策略选择进入点,然后求解TSP。这是一个贪心策略,可能不是最优。一个改进方法是加入迭代:用TSP得到的顺序,重新为每个图形选择进入点(选择距离前驱和后继图形进入点更近的点),然后基于新的进入点再次求解TSP,迭代几次直至稳定。
  5. 总路径计算:calculate_total_path函数模拟了实际切割过程,累加空程(通过可视性图最短路径)和切割长度(图形周长)。这是验证方案总代价的关键。
  6. 性能与精度权衡:如果时间紧迫,可以简化空程计算,直接用直线距离代替可视性图最短路径(即忽略碰撞),这样速度最快,但方案可能不切实际。反之,如果追求精度,就需要完整的碰撞检测和路径规划,计算开销会增大。

5. 进阶优化与常见问题排查

在基本框架跑通后,要获得更优解或应对更复杂的题目要求,还需要考虑以下进阶策略和避坑点。

5.1 进入点选择的迭代优化策略

两阶段法的核心缺陷在于进入点选择与访问顺序解耦。一个有效的改进是迭代局部搜索(Iterated Local Search, ILS):

  1. 初始解:用前述方法得到一个初始路径(包括进入点顺序和访问顺序)。
  2. 扰动:对当前解进行轻微扰动。例如,随机交换两个图形的进入点;或者对访问顺序进行2-opt操作。
  3. 局部搜索:固定进入点,用2-opt优化访问顺序;或者固定访问顺序,为每个图形重新选择进入点(选择使其前后空程和最小的点)。
  4. 接受准则:如果新解更优,则接受;否则,以一定概率接受(模拟退火思想),避免陷入局部最优。
  5. 重复:重复步骤2-4,直到达到迭代次数或时间限制。
def iterative_improvement(shapes, initial_entry_points, initial_order, vis_graph, all_points, start_idx, max_iter=100): best_entry = initial_entry_points.copy() best_order = initial_order.copy() best_len, _ = calculate_total_path(shapes, best_entry, best_order, vis_graph, all_points, start_idx) for it in range(max_iter): # 扰动:随机交换两个图形的进入点 new_entry = best_entry.copy() i, j = np.random.choice(len(shapes), 2, replace=False) new_entry[i], new_entry[j] = new_entry[j], new_entry[i] # 局部搜索:固定进入点,优化顺序 (简单使用2-opt) new_order = two_opt_for_order(new_entry, shapes, vis_graph, all_points, start_idx) # 计算新解代价 new_len, _ = calculate_total_path(shapes, new_entry, new_order, vis_graph, all_points, start_idx) # 接受更优解 if new_len < best_len: best_len = new_len best_entry = new_entry best_order = new_order print(f"Iteration {it}: Improved to {best_len:.2f}") return best_entry, best_order, best_len

5.2 处理大规模图形与复杂轮廓

当图形数量多或轮廓复杂(如包含内环、岛屿)时:

  • 候选点采样:对于复杂轮廓,不宜将所有顶点作为候选点,会导致可视性图过于庞大。应在轮廓上均匀采样一定数量的点,或在曲率大的地方多采样。
  • 分层规划:先对图形进行聚类,将距离近的图形分为一组。先规划组内最优切割路径,再将每个组视为一个“超级图形”,规划组间路径。
  • 空程路径搜索加速:使用A算法替代在全可视性图中搜索最短路径。A需要定义启发式函数(如欧几里得距离),并在网格或可见点图中搜索,比构建全图更灵活,尤其适合动态障碍物场景(但本题障碍物固定)。

5.3 常见踩坑点与调试技巧

  1. 可视性图不连通:如果某些点对之间因为障碍物阻挡而没有边,会导致TSP距离矩阵出现无穷大值,求解失败。解决办法:

    • 添加辅助点:在钢板边界或图形之间的“走廊”处手动添加辅助点,作为中转站。
    • 放宽碰撞检测:如果题目允许空程穿过已切割图形(即图形被切掉后形成的孔洞),则在构建可视性图时,不应将图形内部视为障碍物,而应将其视为可通行区域。这需要修改is_collision_free函数,只判断连线是否与轮廓线相交,而不是与图形面域相交。
    • 使用近似距离:对于不可达的点对,赋予一个很大的惩罚值(如1e9),让求解器尽量避免选择它,但这可能得不到可行解。
  2. TSP求解时间过长:OR-Tools的搜索参数很关键。FirstSolutionStrategy设置为PATH_CHEAPEST_ARC通常能快速得到一个可行解。LocalSearchMetaheuristic设置为GUIDED_LOCAL_SEARCH可以在给定时间内进行深度优化。如果图形数量超过200,可能需要设置更严格的time_limit,或者考虑使用更快的启发式算法(如LKH算法)的现成实现。

  3. 总路径计算与实际不符:calculate_total_path函数中的切割长度用的是图形周长。这假设了从进入点开始,一次性连续切割整个轮廓。但如果图形有内环(岛屿),或者题目要求特殊的内部切割顺序(如先切内孔再切外轮廓),则需要修改这部分逻辑,为每个图形定义更复杂的内部切割子路径。

  4. 图形重叠或包含:题目数据中图形可能重叠或一个图形包含另一个。这在工业排样中常见。我们的模型需要处理这种情况:

    • 重叠:通常不允许,因为会导致切割冲突。需要在预处理阶段检查并报错,或视为一个合并图形。
    • 包含:内部图形必须在外部图形被切割之前切掉,否则内部图形会掉落或移位。这引入了优先级约束。我们的模型需要扩展为带优先级的TSP或GTSP,可以在构建距离矩阵时,将被包含图形到包含图形的距离设为一个极大值,从而强制优先访问内部图形。
  5. 起点与终点不同:有些题目要求从起点开始,在最后一个图形结束,无需返回起点。这在OR-Tools中可以通过设置routing.SetArcCostEvaluatorOfAllVehicles并调整终点约束来实现,或者更简单地在计算总路径时,不添加最后返回起点的空程。

这个问题的魅力在于它没有一个“标准答案”,它考察的是你将一个模糊的实际问题抽象为清晰数学模型的能力,以及根据约束和规模灵活选择、组合、调整算法的工程实践能力。上面的思路和代码框架提供了一个坚实的起点,但真正的竞赛中,需要你根据具体的题目描述和数据,对这些模块进行裁剪、强化和重构。记住,清晰的文档、合理的假设说明以及可视化的结果,和算法本身一样重要。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询