1. 从一张订单说起为什么下料问题让人头疼最近在帮一个做金属加工的朋友优化他们的板材切割方案他们接了个不大不小的订单需要从一批标准尺寸的大板材上切割出几十种不同尺寸的小零件。老板的要求很简单省料省到极致。车间老师傅拿着图纸和计算器算了半天排了七八个方案总觉得还能再挤一挤但又说不出个所以然。这其实就是典型的“下料问题”Cutting Stock Problem一个在制造业、印刷业、木材加工等行业里让无数计划员和老师傅们挠头的经典优化难题。它的核心矛盾在于你手头有固定长度或面积的大材料比如12米长的钢管、2440x1220mm的木板、成卷的布料客户的需求是各种各样长度或形状的小零件。目标就是用最少的“大材料”满足所有“小零件”的需求同时还要考虑切割过程中的工艺约束比如每次切割产生的废料锯缝、切割方向、以及是否允许零件旋转拼接等。手动排列组合对于超过三五种零件的情况基本就靠经验和运气了。这时候就需要把问题交给计算机用数学规划的方法来寻找最优解。整数线性规划Integer Linear Programming, ILP正是解决这类离散组合优化问题的利器。它把“用多少张大板”、“每张大板怎么切”这些决策变成一系列只能取整数的变量然后在一个由各种限制条件比如零件需求总量、板材尺寸构成的“可行域”里寻找那个使总成本通常就是使用板材的总张数或总长度最小的点。而Python凭借其丰富的科学计算库如PuLP、ortools、scipy成为了我们实现这一求解过程的绝佳工具。这篇文章我就以这个金属加工的实际案例为引子带你一步步用Python建模并求解一个基础的一维下料问题比如切割钢管、钢筋让你不仅能得到答案更能理解背后的“为什么”。2. 把现实问题翻译成数学模型定义变量与约束在写任何代码之前我们必须先把车间里的问题用数学语言清晰地描述出来。这一步至关重要模型建错了求解器再强大也白搭。我们以最简单的一维下料为例假设我们有一种无限供应的标准原材料长度为L例如6000mm。我们需要切割出m种不同长度的小零件第i种零件的长度为l_i需求数量为d_i。2.1 核心思路模式Pattern与决策变量直接去思考每一根原材料上具体怎么排布零件变量会非常复杂。一个更聪明的办法是引入“切割模式”的概念。一个切割模式就是指在一根标准长度L的原材料上切割出各种零件的一个可行组合。例如模式P1: [零件A x 2, 零件B x 1]表示用一根原材料切出2个A零件和1个B零件。假设我们枚举出了所有可能的切割模式当然可能模式数量巨大我们稍后讨论如何处理共有n种模式。那么我们的决策变量就很简单了x_j 表示采用第j种切割模式的原材料的数量。这是一个非负整数变量这也是“整数”规划中“整数”二字的直接体现。我们不能说用半根原材料。2.2 构建目标函数与约束条件我们的目标是最小化使用的原材料总根数。所以目标函数非常直观最小化 Z x_1 x_2 ... x_n接下来是约束条件必须满足所有零件的需求需求约束 对于每一种零件ii1, 2, ..., m所有切割模式生产出的该零件总数必须大于等于其需求量d_i。 假设模式j能切割出a_ij个零件i那么约束可以写成a_1i * x_1 a_2i * x_2 ... a_ni * x_n d_i (对于所有 i)这里用“大于等于”而不是“等于”是允许生产过剩因为有时候为了节省原材料总数多生产几个零件可能更划算。在实际中这多出来的部分可以作为库存或允许少量损耗。非负整数约束x_j 0 且为整数 (对于所有 j)2.3 一个具体的数值例子为了让模型更具体我们设定一个简单的场景原材料长度 L 6000 mm需要3种零件零件1: 长度 l1 2200 mm, 需求 d1 20 根零件2: 长度 l2 1800 mm, 需求 d2 15 根零件3: 长度 l3 1500 mm, 需求 d3 10 根我们先手动枚举几个可行的切割模式注意零件组合的总长度不能超过6000mm模式1 切2根零件1 (2200*24400) 剩余1600无法利用余料1600mm。模式2 切1根零件1和2根零件2 (22001800*25800) 余料200mm。模式3 切1根零件1和1根零件3 (220015003700) 余料2300mm这种模式显然很差浪费严重但数学上是可行的。模式4 切3根零件2 (1800*35400) 余料600mm。模式5 切4根零件3 (1500*46000) 余料0mm完美利用。模式6 切2根零件2和1根零件3 (1800*215005100) 余料900mm。现在我们的模型就可以用这些模式来构建了。决策变量是x1到x6分别代表使用对应模式的原材料的根数。目标 Min Z x1 x2 x3 x4 x5 x6约束对于零件1 2x1 1x2 1x3 0x4 0x5 0x6 20对于零件2 0x1 2x2 0x3 3x4 0x5 2x6 15对于零件3 0x1 0x2 1x3 0x4 4x5 1x6 10x1, x2, ..., x6 为非负整数。这样一个现实的下料问题就被我们“翻译”成了一个标准的整数线性规划模型。下一步就是如何让Python来帮我们求解这个模型。3. Python求解实战从枚举模式到调用求解器在Python中我们通常不手动枚举所有模式对于零件种类多、尺寸复杂的情况模式数量是组合爆炸的而是借助专门的库来动态生成模式并求解。这里我介绍两个最常用的工具链PuLP建模友好和OR-Tools性能强大。我们先使用PuLP来完成上面那个枚举模式的例子再探讨更通用的方法。3.1 使用PuLP求解已知模式的模型首先确保安装了PuLPpip install pulpimport pulp # 定义问题 指定求最小值 prob pulp.LpProblem(Cutting_Stock_Problem, pulp.LpMinimize) # 定义决策变量 (模式1到模式6的使用数量) 指定为整数 x1 pulp.LpVariable(x1, lowBound0, catInteger) x2 pulp.LpVariable(x2, lowBound0, catInteger) x3 pulp.LpVariable(x3, lowBound0, catInteger) x4 pulp.LpVariable(x4, lowBound0, catInteger) x5 pulp.LpVariable(x5, lowBound0, catInteger) x6 pulp.LpVariable(x6, lowBound0, catInteger) # 定义目标函数最小化总根数 prob x1 x2 x3 x4 x5 x6, Total_Rods # 定义需求约束 prob 2*x1 1*x2 1*x3 20, Demand_Part1 prob 2*x2 3*x4 2*x6 15, Demand_Part2 prob 1*x3 4*x5 1*x6 10, Demand_Part3 # 求解问题 prob.solve(pulp.PULP_CBC_CMD(msgFalse)) # 使用CBC求解器关闭求解过程输出 # 打印结果 print(f求解状态: {pulp.LpStatus[prob.status]}) print(f最小需要原材料根数: {pulp.value(prob.objective)}) print(\n各模式使用数量:) for v in prob.variables(): print(f{v.name} {v.varValue})运行这段代码你可能会得到一个解。但请注意我们手动枚举的模式可能并不全得到的不一定是全局最优解。它只是在我们给定的这6种模式下的最优组合。这引出了下料问题求解的一个关键如何高效地找到有价值的切割模式3.2 列生成法Column Generation思想与OR-Tools的应用对于大规模问题预先枚举所有模式不可行。业界和学术界最常用的方法是列生成法。它的核心思想是先从一个很小的、可行的模式集合开始比如每种模式只切一种零件直到放不下为止求解这个“限制主问题”。然后通过求解一个“子问题”通常是一个背包问题来寻找一个能降低当前总成本的新切割模式即“列”加入到主问题中。如此迭代直到找不到更优的模式为止。幸运的是我们不需要自己实现复杂的列生成算法。Google的OR-Tools库为其提供了高度优化的求解器。我们可以使用ortools.linear_solver.pywraplp中的SCIP或CBC求解器并利用其建模功能。下面我们展示如何用OR-Tools直接求解它内部会处理模式生成等复杂问题。首先安装pip install ortoolsfrom ortools.linear_solver import pywraplp def solve_cutting_stock(L, lengths, demands): 使用OR-Tools求解一维下料问题 Args: L: 原材料长度 lengths: 零件长度列表 如 [2200, 1800, 1500] demands: 零件需求列表 如 [20, 15, 10] # 创建求解器 使用SCIP支持整数规划 solver pywraplp.Solver.CreateSolver(SCIP) if not solver: print(创建求解器失败) return None # 获取零件种类数 num_items len(lengths) # 计算一个简单的上界最浪费的切法每根原材料只切一种最小的零件 # 这给出了可能需要原材料数量的最大值 max_patterns_per_item [ (demand (L // length) - 1) // (L // length) for demand, length in zip(demands, lengths) ] upper_bound sum(max_patterns_per_item) # 创建决策变量 x[pattern_index] 表示使用第pattern_index种模式的数量 # 注意这里我们并不知道有多少种模式所以先创建一个足够大的变量列表。 # 更高级的做法是动态添加列生成但为了简化我们假设一个足够大的上界。 # 实际上OR-Tools的求解器在内部处理整数规划时会进行类似的搜索。 # 我们这里采用一种更直接的建模方式创建多个“匿名”的原材料并决定每根上怎么切。 # 但这并不是最高效的方法。对于教学和中小规模问题我们可以用另一种思路 # 直接建模每根原材料上的切割方案但这需要引入大量二元变量。 # 鉴于篇幅和复杂性我们展示一个利用OR-Tools“带权集覆盖”思路的简化版本。 # 实际上对于标准下料问题建议使用OR-Tools的Bin Packing或特定示例。 print(注意OR-Tools的标准接口需要更复杂的建模来精确描述下料问题。) print(对于生产环境建议参考OR-Tools官方示例中的bin_packing或cutting_stock相关代码。) print(下面提供一个概念性更强的简化示例说明如何调用求解器进行整数规划求解。) # 为了保持文章的连贯性和实操性我们切换回一个更可控的示例 # 假设我们已经通过其他方法如简单枚举或启发式生成了一组候选模式。 # 我们将使用OR-Tools来求解这个“已知模式”的模型类似于PuLP的例子但展示OR-Tools的语法。 # 假设我们生成了和之前一样的6种模式用矩阵patterns表示 # patterns[j][i] 表示模式j切割出零件i的数量 patterns [ [2, 0, 0], # 模式1: 2个零件1 [1, 2, 0], # 模式2: 1个零件1, 2个零件2 [1, 0, 1], # 模式3: 1个零件1, 1个零件3 [0, 3, 0], # 模式4: 3个零件2 [0, 0, 4], # 模式5: 4个零件3 [0, 2, 1], # 模式6: 2个零件2, 1个零件3 ] num_patterns len(patterns) # 创建变量每种模式使用的次数 x [] for j in range(num_patterns): x.append(solver.IntVar(0, solver.infinity(), fx[{j}])) # 创建目标最小化总根数 objective solver.Objective() for j in range(num_patterns): objective.SetCoefficient(x[j], 1) # 每种模式使用一次成本为1一根原材料 objective.SetMinimization() # 添加需求约束 for i in range(num_items): constraint solver.Constraint(demands[i], solver.infinity(), fdemand_{i}) for j in range(num_patterns): constraint.SetCoefficient(x[j], patterns[j][i]) # 求解 print(正在求解...) status solver.Solve() # 输出结果 if status pywraplp.Solver.OPTIMAL: print(找到最优解) print(f最小原材料根数 {objective.Value()}) total_waste 0 for j in range(num_patterns): if x[j].solution_value() 0.5: # 忽略接近0的值 used int(round(x[j].solution_value())) # 计算该模式的余料 pattern_length sum(patterns[j][i] * lengths[i] for i in range(num_items)) waste L - pattern_length total_waste waste * used print(f 模式{j1} (切割方案{patterns[j]}) 使用 {used} 次 单根余料 {waste}mm) print(f总余料长度 {total_waste}mm) else: print(未找到最优解。状态码:, status) # 运行示例 L 6000 lengths [2200, 1800, 1500] demands [20, 15, 10] solve_cutting_stock(L, lengths, demands)这个示例虽然绕了一圈但核心展示了OR-Tools建模和求解整数规划的基本流程。对于真正的下料问题你需要集成一个模式生成器例如用动态规划求解背包问题来生成新列并与主问题求解循环迭代。OR-Tools的官方示例库中提供了更完整的实现。注意在实际项目中直接使用pulp或ortools的初级接口可能无法应对大规模下料问题。工业级解决方案通常会调用专门的优化求解器如Gurobi, CPLEX并结合列生成、分支定价等高级算法。但对于大多数中小规模问题零件种类20需求数量适中使用pulp配合合理的模式枚举例如只生成利用率高于某个阈值如85%的模式已经能得到非常不错的结果。4. 超越基础模型扩展与实际问题处理我们之前讨论的模型是一个高度简化的版本。现实中的下料问题要复杂得多需要考虑各种工艺和业务约束。下面我们来探讨几个常见的扩展方向。4.1 从一维到二维板材切割问题一维问题只考虑长度二维问题则要考虑长和宽。这瞬间将问题复杂度提升了好几个数量级。零件是矩形有长和宽可能允许旋转90度。原材料也是矩形板材。目标是最小化使用的板材总面积或总张数。建模方法从简单的整数规划变为复杂的混合整数规划MILP并大量引入二元变量来表示“某个零件是否被放置在某个板材的某个特定位置”。求解这类问题通常需要借助专门的二维排样算法如基于左下角放置Bottom-Left的启发式算法、贪心算法或者更高级的基于约束规划和线性规划的精确算法如用Gurobi求解。Python库如rectpack提供了一些启发式算法的实现。# 示例使用rectpack进行简单的矩形排样启发式非最优 import rectpack as rp # 定义板材和零件 bins [(600, 400)] * 10 # 10张600x400的板材 rectangles [(150, 100)] * 5 [(200, 80)] * 3 [(120, 120)] * 4 # 各种零件 packer rp.newPacker() # 添加板材 for b in bins: packer.add_bin(*b) # 添加零件 for r in rectangles: packer.add_rect(*r) # 执行打包算法 packer.pack() # 输出结果 for i, abin in enumerate(packer): print(f板材 {i1}:) for rect in abin: print(f 零件 {rect.rid} 放置在 ({rect.x}, {rect.y}), 尺寸 {rect.width}x{rect.height})4.2 处理工艺约束切割损耗与切割次数切割损耗锯缝宽度每次切割锯片会损耗掉一定宽度如3mm的材料。这在模型中必须考虑。一种方法是在计算零件总长度时将锯缝损耗等效地加到零件长度上。例如如果一个模式要切出k个零件会产生k-1次内部切割那么该模式的有效占用长度就是零件总长 (k-1) * 锯缝宽度。这个值必须小于等于原材料长度L。切割次数限制某些切割设备对单根原材料上的切割次数有限制因为切割次数过多影响效率或精度。这可以在生成切割模式时作为一个过滤条件只生成切割次数小于阈值的模式。4.3 多规格原材料与余料价值现实中仓库里可能有好几种不同长度或价格的原材料。这时决策变量需要扩展为x_{k,j}表示使用第k种规格的原材料的第j种模式的数量。目标函数变为最小化总成本而不仅仅是总根数。成本可以是原材料的价格也可以是其长度如果按长度计价。此外产生的余料如果足够大可能具有再利用价值比如可以用于切割更小的零件。这可以通过引入“余料库存”变量和更复杂的多阶段模型来处理但模型会变得非常庞大。实践中更常见的做法是设定一个余料阈值如小于200mm的料视为废料只对大于阈值的余料考虑在后续排料中复用。4.4 求解策略与性能优化当问题规模变大时直接求解完整的整数规划模型可能非常耗时甚至不可行。这时候需要一些策略模式生成策略不要枚举所有模式只生成“好”的模式。例如使用动态规划求解一个背包问题来寻找单根原材料上利用率最高即剩余长度最小的切割组合。只将这些高效模式加入主问题。启发式与精确算法结合先用启发式算法如首次适应递减法FFD、最佳适应递减法BFD快速得到一个较好的可行解将这个解作为整数规划求解器的初始解可以大大加快求解速度。设置求解时间限制与容忍间隙对于大规模问题可以接受接近最优的解。设置一个最大求解时间例如300秒和一个最优间隙容忍度例如Gap1%让求解器在有限时间内找到一个满足间隙要求的满意解。分解与滚动计划如果订单量巨大可以按时间或产品类别将问题分解成若干个子问题分别求解然后再进行整体协调。5. 避坑指南从模型到代码的常见问题在实际用Python实现下料问题求解的过程中我踩过不少坑。这里总结几个关键点希望能帮你节省时间。5.1 模型构建的陷阱忽略整数约束与方向性最经典的错误就是忘记设置变量的整数约束直接当成线性规划LP来解。LP的解通常是分数比如“用2.5根原材料”这在实际中毫无意义。一定要用catIntegerPuLP或IntVarOR-Tools来定义变量。对于二维问题零件的方向性是否允许90度旋转是一个容易被忽略但影响巨大的因素。如果允许旋转在建模时需要为同一个零件创建两个不同的“版本”原始方向和旋转后的方向或者引入额外的二元变量来控制方向选择。5.2 数据输入的精度与单位统一长度数据一定要统一单位全部用mm或全部用cm。特别是从CAD图纸或Excel表格导入数据时要仔细检查。一个小数点的错误可能导致整个排样方案无效。另外对于切割损耗要明确是“切一刀损耗多少”还是“每个零件分摊多少”并在计算中保持一致。5.3 求解器选择与配置PuLP默认的CBC求解器对于中小型整数规划问题足够好用而且是开源的。但如果问题规模很大变量成千上万CBC可能会很慢。这时候可以考虑安装更强大的商业求解器如Gurobi、CPLEX的学术许可版并通过PuLP的接口调用。在OR-Tools中SCIP也是一个功能强大的开源求解器。调用求解器时合理设置参数很重要。例如可以设置timeLimit来防止程序无限制运行设置gapRel相对间隙或gapAbs绝对间隙来接受近似最优解。# PuLP中设置求解时间限制和间隙 prob.solve(pulp.PULP_CBC_CMD(timeLimit60, gapRel0.01)) # 限时60秒 相对间隙1% # OR-Tools中设置参数 solver.SetTimeLimit(60*1000) # 毫秒为单位 solver.params.relative_gap_tolerance 0.01 # 相对间隙1%5.4 结果解读与验证求解器输出“Optimal”并不一定意味着你得到了完美的实际方案一定要验证检查整数解确保所有变量值都是整数可能会有极小的浮点误差如1e-6应四舍五入。验证需求满足将解出的模式使用数量代入需求约束手动计算每种零件的总产量确保大于等于需求量。验证长度约束对于每一个被使用的切割模式计算其所有零件长度加上切割损耗之和确保不超过原材料长度。检查实际可切割性数学模型可能给出一个在数字上可行但在实际切割机上无法物理实现的方案比如零件排列顺序导致无法下刀。对于复杂二维排样需要用专门的排样软件进行可视化检查和NC代码生成。5.5 性能瓶颈识别如果你的程序跑得很慢可以从以下几点排查模式数量这是最主要的瓶颈。尝试限制模式生成只保留利用率高的模式例如余料小于最短零件长度的模式。变量和约束数量使用prob.numVariables()和prob.numConstraints()PuLP查看问题规模。如果太大考虑问题分解。求解器日志打开求解器输出信息msgTrue观察“节点数”、“目标值下降情况”。如果目标值很早就停滞不前但求解器还在大量分支可能需要调整启发式参数或提供初始解。6. 一个完整的、可复现的案例脚本最后我将整合以上要点提供一个相对完整的一维下料问题Python求解脚本。它使用了动态规划来生成高效切割模式然后用PuLP求解主问题。这个脚本可以直接用于解决类似问题。import pulp from typing import List, Tuple def generate_cutting_patterns(stock_length: int, piece_lengths: List[int], kerf: int 0) - List[List[int]]: 使用动态规划生成所有利用率较高的切割模式。 这里为了简化我们生成所有总长度 stock_length 且剩余长度小于最小零件长度的模式。 这是一种简单的枚举对于零件种类多的情况可能组合爆炸。生产环境应用列生成中的子问题求解。 min_piece_len min(piece_lengths) num_pieces len(piece_lengths) patterns [] # 这是一个简化的递归/回溯生成仅适用于零件种类很少的情况例如5。 # 对于更多零件应采用背包问题DP求解。 def dfs(current_pattern: List[int], remaining_length: int, start_idx: int): current_pattern: 当前模式中各种零件的数量列表 remaining_length: 剩余可用长度已考虑锯缝 start_idx: 从第几种零件开始尝试添加避免重复模式 # 将当前模式加入结果只要不是空模式 if sum(current_pattern) 0: # 计算当前模式的总长度占用包括内部锯缝 total_cut_count sum(current_pattern) if total_cut_count 0: total_kerf (total_cut_count - 1) * kerf used_length sum(current_pattern[i] * piece_lengths[i] for i in range(num_pieces)) total_kerf if used_length stock_length: patterns.append(current_pattern.copy()) # 尝试添加新的零件 for i in range(start_idx, num_pieces): # 尝试添加一个零件i new_remaining remaining_length - piece_lengths[i] - kerf # 添加一个零件和一次切割损耗 if new_remaining 0: current_pattern[i] 1 dfs(current_pattern, new_remaining, i) # 注意仍然从i开始允许重复添加同种零件 current_pattern[i] - 1 # 回溯 # 初始化一个全零的模式 init_pattern [0] * num_pieces dfs(init_pattern, stock_length kerf, 0) # 初始剩余长度加上一个kerf方便计算 # 去重虽然dfs通过start_idx减少了重复但可能仍有长度相同的不同排列这里简单去重 unique_patterns [] seen set() for p in patterns: # 将模式转换为元组以便哈希 key tuple(p) if key not in seen: seen.add(key) unique_patterns.append(p) # 过滤掉利用率太低的模式剩余长度大于最小零件长度说明还能再放一个 filtered_patterns [] for p in unique_patterns: total_pieces sum(p) used_length_no_kerf sum(p[i] * piece_lengths[i] for i in range(num_pieces)) if total_pieces 0: continue total_kerf (total_pieces - 1) * kerf used_length used_length_no_kerf total_kerf waste stock_length - used_length if waste min_piece_len: # 剩余空间放不下最小的零件认为是一个“好”模式的候选 filtered_patterns.append(p) print(f生成了 {len(filtered_patterns)} 种有效切割模式。) return filtered_patterns def solve_cutting_stock_pulp(stock_length: int, piece_lengths: List[int], demands: List[int], kerf: int 0): 主求解函数 # 1. 生成切割模式 patterns generate_cutting_patterns(stock_length, piece_lengths, kerf) if not patterns: print(未生成任何有效切割模式请检查输入数据。) return num_patterns len(patterns) num_pieces len(piece_lengths) # 2. 建立PuLP模型 prob pulp.LpProblem(1D_Cutting_Stock, pulp.LpMinimize) # 决策变量每种模式使用的次数 x_vars [pulp.LpVariable(fx_{j}, lowBound0, catInteger) for j in range(num_patterns)] # 目标函数最小化原材料使用根数 prob pulp.lpSum(x_vars) # 需求约束 for i in range(num_pieces): prob pulp.lpSum(patterns[j][i] * x_vars[j] for j in range(num_patterns)) demands[i], fDemand_Piece_{i} # 3. 求解 solver pulp.PULP_CBC_CMD(timeLimit30, gapRel0.02, msgTrue) # 设置30秒时限和2%的gap prob.solve(solver) # 4. 输出结果 print(f\n求解状态: {pulp.LpStatus[prob.status]}) if pulp.LpStatus[prob.status] Optimal: print(f理论最小原材料根数: {pulp.value(prob.objective):.2f}) total_used 0 total_waste 0 print(\n详细的切割方案:) for j in range(num_patterns): usage x_vars[j].varValue if usage and usage 0.5: usage_int int(round(usage)) total_used usage_int # 计算该模式的构成和余料 pattern_desc [] used_length_no_kerf 0 for i in range(num_pieces): if patterns[j][i] 0: pattern_desc.append(f{piece_lengths[i]}mm x {patterns[j][i]}) used_length_no_kerf patterns[j][i] * piece_lengths[i] total_pieces_in_pattern sum(patterns[j]) used_kerf (total_pieces_in_pattern - 1) * kerf if total_pieces_in_pattern 1 else 0 used_length used_length_no_kerf used_kerf waste stock_length - used_length total_waste waste * usage_int print(f 模式 {j1}: 使用 {usage_int} 根原材料) print(f 切割方案: { .join(pattern_desc)}) print(f 单根利用率: {used_length/stock_length*100:.1f}% (余料 {waste}mm)) print(f\n总计使用原材料: {total_used} 根) print(f总余料: {total_waste} mm) # 验证需求 print(\n需求满足情况验证:) for i in range(num_pieces): total_produced sum(patterns[j][i] * int(round(x_vars[j].varValue())) for j in range(num_patterns) if x_vars[j].varValue() 0.5) print(f 零件{i1}({piece_lengths[i]}mm): 需求 {demands[i]}, 生产 {total_produced}, 超出 {total_produced - demands[i]}) else: print(未找到最优解。尝试调整求解参数或检查模型。) # 案例运行 if __name__ __main__: # 输入数据 STOCK_LENGTH 6000 # 原材料长度单位mm KERF 3 # 锯缝宽度单位mm PIECE_LENGTHS [2200, 1800, 1500, 1100] # 零件长度列表 DEMANDS [20, 15, 10, 25] # 零件需求列表 print(一维下料问题求解案例) print(f原材料长度: {STOCK_LENGTH}mm) print(f锯缝宽度: {KERF}mm) print(f零件规格: {list(zip(PIECE_LENGTHS, DEMANDS))}) print(- * 50) solve_cutting_stock_pulp(STOCK_LENGTH, PIECE_LENGTHS, DEMANDS, KERF)这个脚本提供了一个从模式生成到求解验证的完整流程。你可以修改STOCK_LENGTH、KERF、PIECE_LENGTHS和DEMANDS来求解自己的问题。需要注意的是generate_cutting_patterns函数中的DFS枚举方法只适用于零件种类很少比如3-5种的情况。对于更多零件你需要替换为一个更高效的背包问题求解器来生成模式或者直接使用OR-Tools等库内置的更高级算法。通过这个完整的探索我们从理解问题本质开始一步步构建数学模型用Python实现求解并深入讨论了实际应用中的扩展和陷阱。下料问题是一个迷人的优化问题领域它完美地体现了数学如何赋能传统工业将老师的经验转化为可计算、可优化的精确方案。当你下次看到一堆待切割的材料时希望你能想到背后可能正运行着一行行Python代码在无声地寻找着最节省的那条切割线。