尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

数学建模竞赛中的钢板切割路径优化:两阶段算法与Python实现

数学建模竞赛中的钢板切割路径优化:两阶段算法与Python实现 1. 项目概述与问题核心五一数学建模竞赛的A题“钢板最优切割路径问题”本质上是一个经典的工业优化问题但它在经典之上叠加了现实生产中的复杂约束。简单来说就是给你一块大钢板上面预先标记好了若干个需要切割下来的小零件轮廓你的切割机从起点出发需要依次走完所有轮廓线进行切割最后回到起点。目标很明确在完成所有切割任务的前提下让切割机空跑也就是不进行切割的移动即“空程”的总距离最短。这直接关系到生产效率、能耗和成本。我参加过多次数学建模竞赛并担任指导这类路径规划问题年年有但年年都能挖出新东西。对于参赛队伍而言这道题的关键不在于提出一个惊世骇俗的新算法而在于如何系统性地将实际问题转化为数学模型并选择或组合恰当的优化工具进行求解最后用清晰、有说服力的论文和稳定的代码呈现出来。题目通常会提供钢板尺寸、各零件轮廓的几何信息可能是坐标点序列、切割起点等信息。难点往往隐藏在细节里比如切割顺序的排列组合是阶乘级爆炸的比如从一个零件的终点移动到下一个零件的起点这段空程路径是否可以穿过已经切割下来的零件空洞通常不允许因为零件已掉落切割同一个零件轮廓时是必须一刀连续切完还是可以中途抬起刀头空程跳到轮廓的另一处再继续切这些工艺细节直接决定了模型的复杂度和求解策略。2024年的这道题从网络上的讨论热度来看大家普遍关注如何高效处理“空程”优化这正好是连接数学建模与实际价值的核心点。2. 核心思路拆解与模型构建面对这样一个最优切割路径问题我们的思路不能一上来就钻到算法里必须分层次、结构化地拆解。整个建模过程可以看作一个“翻译”工作把工程语言翻译成数学语言再翻译成算法语言。2.1 问题抽象与关键定义首先我们需要对问题进行精确的抽象。我们可以将每一个需要切割的零件轮廓视为一个“任务”或“子图”。每个任务本身包含一系列连续的线段由坐标点定义切割机必须完整遍历这些线段。任务与任务之间的移动就是需要优化的“空程”。这里有几个核心定义必须厘清节点Node与边Edge一种常见的建模方法是将每个零件轮廓的起点和终点有时可以是轮廓上任意一点定义为“节点”。而“边”则有两种一种是零件轮廓本身的切割边其长度固定且必须被遍历另一种是连接不同零件节点之间的空程边其长度取决于两点间的欧氏距离或考虑避障的路径长度这是我们优化的对象。空程Idle Path指切割机刀头抬起、不进行切割时的移动路径。优化目标就是最小化所有空程边的总和。切割约束这是模型是否贴近实际的关键。通常假设轮廓连续性一个零件的轮廓必须被连续切割完成中途不能跳到另一个零件去。空程避障空程移动时不能穿过已经切割下来的零件区域因为零件可能已掉落但可以穿过尚未切割的零件区域。这引入了动态的障碍物环境大大增加了难度。许多简化模型会忽略这一点假设空程可以走直线这虽然易于求解但会损失模型保真度。基于以上抽象该问题可以初步归类为一个广义旅行商问题GTSP或车辆路径问题VRP的变体。不同于经典TSP访问城市点这里我们需要访问的是一系列“子图”零件轮廓并且必须在每个子图中完成一条特定的“哈密顿路径”走完整个轮廓。2.2 数学模型构建我们可以尝试建立一个混合整数线性规划MILP模型这是数学建模中表达清晰、逻辑严谨的常用方法。集合定义V: 所有节点的集合。每个零件i有两个特殊节点入口节点s_i和出口节点t_i可以设为同一点即轮廓起点。A: 所有弧有向边的集合。包括零件内部的切割弧和零件间的空程弧。K: 所有零件任务的集合。决策变量x_{ij}: 二进制变量如果弧(i, j)被选中在路径中则为1否则为0。u_i: 辅助变量用于消除子回路MTZ约束可以理解为节点i在路径中的访问顺序。参数c_{ij}: 弧(i, j)的长度切割长度或空程距离。M: 一个足够大的正数。目标函数最小化总路径成本Minimize Z Σ_{(i,j)∈A} c_{ij} * x_{ij}约束条件流量平衡约束对于每个节点i进入的弧等于离开的弧。Σ_{j: (j,i)∈A} x_{ji} Σ_{j: (i,j)∈A} x_{ij} 1(对于所有节点i除了虚拟的起点/终点)任务完成约束对于每个零件k必须有一条路径从其入口节点s_k出发遍历其所有内部切割边到达出口节点t_k。这需要一组约束来确保零件k的所有内部边被连续访问。一种简化方式是预先将每个零件的轮廓处理为一条必须按顺序走的路径然后将整个零件视为一个“超级节点”其内部成本固定优化的是连接这些超级节点的边。子回路消除约束MTZ形式u_i - u_j M * x_{ij} M - 1(对于所有i, j,i ≠ j) 这个约束确保路径形成一个整体回路而不是多个小圈。起点终点约束指定路径从给定的起点开始并最终返回起点或指定终点。注意这是一个高度简化的模型框架。实际模型中“任务完成约束”是最复杂的一部分。如何用数学公式描述“必须连续遍历一个给定点集序列零件轮廓”需要巧妙的建模技巧。常见做法是引入额外的决策变量来记录轮廓上的访问顺序或者使用网络流思想将零件轮廓建模为一条必须满流的链。2.3 求解策略选型分析直接求解上述MILP模型对于零件数量稍多比如超过15个的情况可能就会因为计算复杂度太高而无法在比赛时间内得到最优解。因此我们必须设计高效的求解策略。策略通常分为两类精确算法和启发式算法。精确算法如分支定界法、动态规划等。适用于小规模问题零件数10可以保证找到全局最优解。在建模比赛中即使用精确算法只能求解小规模实例也可以作为验证启发式算法有效性的基准。启发式/元启发式算法这是解决该问题的主流和实用选择。两阶段法这是最直观、最常用的策略。第一阶段任务排序。忽略零件内部的详细几何形状将每个零件抽象为一个点如其重心或轮廓上的一个代表点。然后求解一个在这些点之间的TSP问题确定访问各个零件的顺序。这一步可以使用经典的TSP启发式算法如最近邻法、遗传算法、模拟退火、蚁群算法等。第二阶段入口点选择。在确定了零件访问顺序后对于相邻的两个零件需要在前一个零件的轮廓上选择一个“退出点”在后一个零件的轮廓上选择一个“进入点”使得这两点之间的空程最短。这就变成了一个在两条闭合曲线上找最近点对的问题可以通过计算几何的方法如计算两组点之间的最小欧氏距离来求解。融合改进的智能算法将排序和选点融合在一个算法框架内进行优化。例如在遗传算法的染色体编码中不仅编码零件的访问顺序还编码每个零件上选择的切入/切出点。适应度函数直接计算总空程。这种方法搜索空间更大但潜力也更大需要精心设计交叉、变异算子。考虑避障的路径规划如果考虑空程不能穿过已切割区域问题就升级为动态环境下的路径规划。可以在两阶段法的基础上在第二阶段用简单的路径搜索算法如A*算法在“动态地图”已切割区域为障碍物上搜索两点间最短可行空程。但这会极大增加计算量需要权衡模型复杂度和求解时间。对于数学建模竞赛我推荐采用两阶段法因为它结构清晰易于实现和解释而且通过巧妙设计第一阶段和第二阶段的方法完全可以得到高质量的解。我们的核心创新点可以放在第二阶段入口点选择的优化上或者对第一阶段TSP求解算法的改进上。3. 模型实现与核心代码解析我们选择Python作为实现语言因为它有丰富的科学计算库NumPy, SciPy和优化算法库。我们将采用两阶段法第一阶段用模拟退火算法SA解决任务排序问题第二阶段用几何计算确定最优入口点。3.1 数据预处理与几何表示首先我们需要解析题目数据。假设数据以文本文件给出包含钢板尺寸和每个零件的轮廓点坐标列表。import numpy as np import math import random import matplotlib.pyplot as plt def load_data(file_path): 加载数据 假设数据格式 第一行钢板长度 钢板宽度 后续行零件ID 后续为x1,y1,x2,y2,... 轮廓点坐标 parts [] with open(file_path, r) as f: lines f.readlines() plate_size list(map(float, lines[0].strip().split())) for line in lines[1:]: data list(map(float, line.strip().split())) part_id int(data[0]) points np.array(data[1:]).reshape(-1, 2) # 重塑为N行2列的数组 # 确保轮廓闭合首尾点相同 if not np.allclose(points[0], points[-1]): points np.vstack([points, points[0]]) parts.append({ id: part_id, points: points, centroid: np.mean(points[:-1], axis0) # 计算重心用于第一阶段排序 }) return plate_size, parts # 计算轮廓上任意两点间的折线距离沿着轮廓 def contour_distance(points, idx1, idx2): 计算轮廓points上从索引idx1到idx2沿着轮廓的路径长度。 假设轮廓是闭合的点序列为[p0, p1, ..., pn, p0]。 n len(points) - 1 # 去掉重复的闭合点 # 确保索引在有效范围内 i1, i2 idx1 % n, idx2 % n if i1 i2: return 0.0 # 计算正向距离 forward_dist 0.0 for i in range(i1, i2): forward_dist np.linalg.norm(points[i1] - points[i]) # 计算反向距离轮廓另一方向 reverse_dist 0.0 for i in range(i2, i1 n): reverse_dist np.linalg.norm(points[(i1)%n] - points[i%n]) # 返回较短的距离理论上沿着轮廓走两个方向距离之和等于周长 # 实际上对于闭合轮廓从一个点到另一个点有两条路径我们取短的那条。 # 更严谨的做法是总周长 - forward_dist 另一条路径长取min total_perimeter forward_dist reverse_dist # 这里reverse_dist计算的是另一条路 return min(forward_dist, total_perimeter - forward_dist)3.2 第一阶段基于模拟退火的零件排序优化第一阶段我们将零件的重心作为代表点用模拟退火算法寻找访问这些重心点的最短回路顺序。def total_distance(order, centroids): 计算给定访问顺序下访问重心点的总距离空程的近似 dist 0.0 num len(order) for i in range(num): from_idx order[i] to_idx order[(i1) % num] dist np.linalg.norm(centroids[to_idx] - centroids[from_idx]) return dist def simulated_annealing(centroids, init_orderNone, T_start1000, T_end1e-3, alpha0.95, iter_per_T100): 模拟退火算法求解TSP centroids: 各零件的重心坐标列表 num_parts len(centroids) if init_order is None: current_order list(range(num_parts)) random.shuffle(current_order) else: current_order init_order.copy() current_dist total_distance(current_order, centroids) T T_start best_order current_order.copy() best_dist current_dist while T T_end: for _ in range(iter_per_T): # 产生新解采用2-opt邻域操作随机交换两个位置 new_order current_order.copy() i, j random.sample(range(num_parts), 2) new_order[i], new_order[j] new_order[j], new_order[i] new_dist total_distance(new_order, centroids) delta new_dist - current_dist # Metropolis准则 if delta 0 or random.random() math.exp(-delta / T): current_order new_order current_dist new_dist if current_dist best_dist: best_order current_order.copy() best_dist current_dist T * alpha # 降温 return best_order, best_dist # 使用示例 plate_size, parts load_data(cutting_data.txt) centroids [p[centroid] for p in parts] best_order, approx_dist simulated_annealing(centroids) print(f第一阶段优化完成零件访问顺序: {best_order}) print(f基于重心近似的空程估计: {approx_dist:.2f})实操心得模拟退火中的参数初始温度T_start、降温系数alpha、每个温度的迭代次数iter_per_T对结果影响很大。T_start需要足够高以允许跳出局部最优alpha通常在0.9到0.99之间太接近1降温慢耗时太小容易陷入局部最优。在实际比赛中可以先用小规模数据调试参数观察目标函数下降曲线。3.3 第二阶段轮廓入口点选择优化确定了零件访问顺序后我们需要为顺序中相邻的两个零件选择最佳的“退出点”和“进入点”。假设一个零件轮廓有m个点另一个有n个点暴力枚举所有m*n种组合计算距离是可行的因为单个零件的轮廓点数不会太多通常几十到上百个。def find_best_connection_points(contour1, contour2): 在两个轮廓contour1和contour2上找到一对点(p1, p2) 使得从contour1的p1到contour2的p2的欧氏距离最短。 返回 (最佳距离, contour1上的点索引, contour2上的点索引) best_dist float(inf) best_i, best_j -1, -1 # 注意轮廓点数组的最后一个点是重复的起点我们计算时不包含它 for i in range(len(contour1)-1): p1 contour1[i] for j in range(len(contour2)-1): p2 contour2[j] dist np.linalg.norm(p2 - p1) if dist best_dist: best_dist dist best_i i best_j j return best_dist, best_i, best_j def calculate_total_idle_path(parts, order): 根据给定的零件访问顺序order计算总空程。 假设切割每个零件轮廓的起点和终点就是我们找到的最佳连接点。 total_idle 0.0 num len(order) # 还需要考虑从起点到第一个零件入口点的空程以及从最后一个零件出口点回到起点的空程 # 这里假设起点为(0,0)可根据题目修改 start_point np.array([0.0, 0.0]) prev_exit_point start_point detailed_path [] # 记录详细的路径点用于可视化 for idx in range(num): part_idx order[idx] next_part_idx order[(idx1) % num] part parts[part_idx] next_part parts[next_part_idx] # 如果是最后一次移动是回到起点 if idx num - 1: next_contour [start_point] next_contour_idx 0 else: next_contour next_part[points] # 找到从上一个退出点到当前零件轮廓的最佳进入点 # 这里我们简化将上一个退出点视为一个“虚拟轮廓”只包含一个点 dummy_contour np.array([prev_exit_point]) _, _, enter_idx find_best_connection_points(dummy_contour, part[points]) enter_point part[points][enter_idx] # 计算这段空程 total_idle np.linalg.norm(enter_point - prev_exit_point) detailed_path.append(prev_exit_point) detailed_path.append(enter_point) # 计算当前零件内部的切割路径必须从enter_point开始走完整个轮廓回到enter_point # 这里需要计算沿着轮廓从enter_point走一圈回到原点的路径长度即轮廓周长。 # 我们先计算轮廓总长 contour_len 0.0 contour_points part[points] n len(contour_points) - 1 for k in range(n): contour_len np.linalg.norm(contour_points[k1] - contour_points[k]) # 注意如果切割起点和终点必须是同一点那么切割长度就是轮廓周长。 # 如果可以从轮廓上一点开始在另一点结束则切割长度可能小于周长但需要额外的决策。 # 此处采用简化模型从enter_point开始走完整条闭合轮廓终点也是enter_point。 # 因此切割长度就是轮廓周长。 # 但实际路径需要从enter_point出发沿着轮廓走一圈。我们需要确定行走方向顺时针/逆时针使得路径连续。 # 更简单的处理在计算总成本时我们只关心空程切割长度是固定成本可以最后加上。 # 所以我们暂时不将切割路径加入detailed_path只记录切割长度。 # 确定当前零件的退出点即下一个零件的进入点所对应的本零件上的点 # 我们需要找到本零件轮廓上与下一个零件最佳进入点配对的那个点 if idx num - 1: # 最后一个零件退出点就是回到起点的点我们设定为enter_point因为切完一圈回来了 exit_point enter_point next_enter_point start_point else: _, exit_idx, next_enter_idx find_best_connection_points(part[points], next_part[points]) exit_point part[points][exit_idx] next_enter_point next_part[points][next_enter_idx] # 更新prev_exit_point为当前零件的退出点用于下一轮计算 prev_exit_point exit_point # 加上从最后一个零件退出点回到起点的空程在循环中最后一个零件退出点就是enter_point且下一目标为起点已计算 # 循环逻辑已处理 return total_idle, detailed_path # 计算总空程 total_idle, path_points calculate_total_idle_path(parts, best_order) total_cutting_length sum([np.sum(np.linalg.norm(p[points][1:] - p[points][:-1], axis1)) for p in parts]) total_path_length total_idle total_cutting_length print(f优化后总空程: {total_idle:.2f}) print(f总切割长度固定: {total_cutting_length:.2f}) print(f预估总路径长度: {total_path_length:.2f})注意事项上述第二阶段代码是一个高度简化的版本。它假设从一个零件退出后直接直线移动到下一个零件的进入点。并且它假设零件切割的起点和终点是同一个点即必须走闭合轮廓。在实际问题中切割起点和终点可以是轮廓上不同的点这构成了一个更复杂的优化问题不仅要确定零件顺序还要为每个零件确定切割起始点。这可以通过扩展算法在优化排序的同时将每个零件的起始点也作为决策变量进行优化。3.4 可视化与结果分析将优化前后的路径可视化能极大提升论文的说服力。def plot_solution(parts, order, path_points, plate_size): 绘制钢板、零件轮廓和切割路径 plt.figure(figsize(12, 8)) # 绘制钢板边框 plt.plot([0, plate_size[0], plate_size[0], 0, 0], [0, 0, plate_size[1], plate_size[1], 0], k-, linewidth2) # 绘制所有零件轮廓 colors plt.cm.tab20(np.linspace(0, 1, len(parts))) for i, part in enumerate(parts): contour part[points] plt.plot(contour[:, 0], contour[:, 1], -, colorcolors[i], alpha0.6, labelfPart {part[\id\]}) # 标注重心 plt.scatter(part[centroid][0], part[centroid][1], colorcolors[i], s50, markero) # 绘制空程路径 path_points np.array(path_points) if len(path_points) 0: # 将路径点按顺序连接 for i in range(0, len(path_points)-1, 2): start_pt path_points[i] end_pt path_points[i1] plt.plot([start_pt[0], end_pt[0]], [start_pt[1], end_pt[1]], r--, linewidth1.5, alpha0.8) plt.scatter(path_points[:, 0], path_points[:, 1], colorred, s30, markers, labelIdle Path Points, zorder5) # 绘制访问顺序重心连线 centroid_list [parts[i][centroid] for i in order] centroid_list.append(centroid_list[0]) # 闭合 centroid_arr np.array(centroid_list) plt.plot(centroid_arr[:, 0], centroid_arr[:, 1], b:, linewidth1, alpha0.5, labelCentroid Tour) plt.xlabel(X (mm)) plt.ylabel(Y (mm)) plt.title(Optimal Cutting Path Planning Result) plt.legend(locupper left, bbox_to_anchor(1.05, 1)) plt.grid(True, alpha0.3) plt.axis(equal) plt.tight_layout() plt.savefig(cutting_path_result.png, dpi300) plt.show() # 调用可视化函数 plot_solution(parts, best_order, path_points, plate_size)4. 模型评估、优化与常见问题完成初步建模和求解后我们需要对模型和结果进行批判性分析这是论文拿高分的关键。4.1 模型优缺点分析优点思路清晰易于实现两阶段法将复杂问题分解降低了建模和编程难度。可解释性强零件排序和入口点选择两个步骤物理意义明确评委和读者容易理解。灵活性高第一阶段和第二阶段可以替换不同的算法。例如TSP排序可以用遗传算法、蚁群算法入口点选择可以用更精细的局部搜索。缺点与改进方向解的质量两阶段法是“先排序后选点”排序时基于重心距离但重心距离最短并不一定意味着实际轮廓点之间的最短空程。这可能导致次优解。改进方法是在排序时引入更精确的“轮廓间距”估计或者在迭代中反馈第二阶段信息到第一阶段如迭代改进。切割起点/终点固定我们假设每个零件必须从某点开始并回到同一点切割。若允许起点终点不同则每个零件多了一个决策变量切割方向可以在第二阶段用动态规划求解每个零件轮廓上的最优切割起止点。忽略空程避障这是模型与实际情况最大的差距。引入避障后空程距离不再是直线距离需要用路径搜索算法如A*计算计算量激增。一个折中方案是先按无避障模型求解得到路径后再对可能穿过已切割区域的空程线段进行碰撞检测并局部修正路径如绕行这是一种“先优化后修复”的策略。全局最优性启发式算法不能保证全局最优。可以在论文中讨论算法的收敛性并用小规模实例的精确解如调用Gurobi、CPLEX求解MILP模型进行对比验证启发式算法的有效性。4.2 高级优化技巧与算法改进要让你的解决方案脱颖而出可以考虑以下进阶策略融合优化算法设计一个融合的元启发式算法如遗传算法。染色体编码采用两部分编码。第一部分是零件排列顺序置换编码。第二部分是每个零件上选择的切割起始点索引整数编码。适应度函数直接计算该编码对应的总空程 总切割长度。计算适应度时需要解码根据顺序和起始点依次计算零件内切割路径和零件间空程。遗传操作顺序部分采用OX、PMX等交叉算子起始点部分采用均匀交叉或单点交叉。变异算子可随机交换两个零件顺序或随机改变某个零件的起始点。这种方法能同时优化顺序和起止点搜索能力更强但编程更复杂计算更耗时。局部搜索强化在两阶段法得到初始解后使用局部搜索策略进行改进。2-opt对零件访问顺序进行2-opt交换尝试优化。节点重定位固定顺序对每个零件的入口点在其轮廓上进行局部微调寻找更优的连接点。大规模邻域搜索随机移除一小部分零件然后用插入法重新安排它们的位置评估是否改进。考虑切割工艺约束热变形如果切割顺序不合理先切割内部小孔可能导致钢板局部热量集中变形影响后续切割精度。可以在目标函数中加入一个惩罚项对可能引起热聚集的切割顺序进行惩罚这需要简化的热传导模型估计。引入“引线”实际切割中为了避免在零件轮廓起点留下疤痕有时会从钢板废料区先切一段“引线”再切入轮廓。这可以在模型中加入虚拟的“引线”段并优化引线的连接点。4.3 常见问题与调试技巧在实现过程中你肯定会遇到各种问题。以下是一些常见坑点及解决方法算法陷入局部最优效果不好问题模拟退火或遗传算法很快收敛到一个解不再改善。排查检查降温速率是否过快alpha太小检查初始温度T_start是否足够高允许接受劣解增加每个温度下的迭代次数iter_per_T。技巧多次运行算法不同随机种子取最好结果。记录每次迭代的最优解变化曲线直观判断收敛情况。计算时间过长问题当零件数量多50或轮廓点数多时第二阶段入口点选择的双重循环计算量很大。优化在find_best_connection_points函数中如果两个轮廓相距很远可以先用它们的外接矩形或重心距离做一个快速判断如果距离大于当前最优解的某个倍数则跳过详细计算。使用KD-Treescipy.spatial.KDTree对每个轮廓的点进行空间索引加速最近点查询。对轮廓进行采样用更少的点来代表轮廓进行粗略优化然后在精细阶段对候选区域进行密集计算。可视化路径交叉或混乱问题画出来的空程路径看起来杂乱无章甚至明显绕远路。排查首先检查零件访问顺序best_order是否合理。可以单独绘制重心点的TSP路径图看是否是一个较合理的环路。其次检查calculate_total_idle_path函数中prev_exit_point和enter_point的更新逻辑是否正确确保路径是连续的。调试打印出每一步计算的空程距离和连接点坐标人工核对几个关键步骤。结果不稳定问题每次运行程序得到的总路径长度差异较大。原因启发式算法的随机性导致。模拟退火和遗传算法都对初始解和随机操作敏感。对策这是正常现象。在论文中应报告算法多次运行如30次后的最好解、最差解、平均解和标准差以说明算法的鲁棒性。最终提交的结果自然是多次运行中的最好解。如何与论文写作结合图表务必使用可视化结果。至少应包括零件布局图、优化前后的空程路径对比图、算法收敛曲线图。灵敏度分析改变算法关键参数如模拟退火的初始温度、降温系数观察对结果的影响并分析原因。这能体现你对模型的理解深度。对比实验设计不同规模零件数不同的算例对比你的算法和基准算法如最近邻法、随机排序的结果。用表格清晰展示对比数据。模型检验如果可能用商业优化软件如LINGO、Gurobi求解小规模问题的精确解与你的算法结果对比计算误差百分比证明你的启发式算法是有效的。最后记住数学建模竞赛比拼的不仅是结果更是解决问题的过程、模型的创新性、实现的完整性以及论文表述的清晰度。将你的思考过程、模型建立、算法选择、结果分析和优化建议有条理地、图文并茂地展现在论文中比单纯追求一个更低的数字更重要。这份代码和思路为你提供了一个坚实可靠的起点你可以在此基础上深入挖掘形成自己团队的特色解决方案。
返回列表