简介面向板式家具制造与算法优化领域这份资源提供了一套带Qt界面的板材下料算法实现与可直接运行的程序帮助学习者和从业者理解矩形件切割优化逻辑适用于生产成本控制、算法教学与二次开发。压缩包共62个文件大小14.21MB核心部分为8个C源文件、7个头文件、2个UI界面并附带22个运行所需的DLL、16个Qt语言翻译模块及exe可执行文件DLL保证程序跨平台运行qm文件提供多语言界面支持解压后无需额外配置即可直接体验。已有2143人学习算法仍具优化空间适合在此基础上继续研究改进。通过研读源码可掌握板材尺寸与部件列表的输入处理、切割方案的评估与选择流程借助Qt界面可直观录入参数并查看排样输出便于将理论算法落地到实际生产场景是一份兼顾代码学习与工程实践的实用资源。1. 板式家具下料算法一张大板里的利润藏在最后那 12% 的废料里一张 1220×2440 的标准板材随手排料可能只有 78% 的利用率同一批零件换个排列顺序利用率能到 86% 以上。板式家具下料算法做的就是这件事在给定标准板尺寸、零件尺寸、锯缝宽度和纹理方向的约束下自动生成一套让废料最少、切得出来、工人也愿意照着干的切割方案。它适合做定制家具拆单、批量柜体生产和小型开料车间的人——新手能拿它跑通第一版排样程序熟手能靠它把每张板的成本再压一压。2. 先选对模型普通矩形排样、一刀切与批量下料的差距很多第一次做下料优化的人会直接把问题当成“矩形排样问题”套一个开源套料库就跑。结果排出来的图形确实好看每块零件都填进去了但车间工人看一眼就摇头锯片是走直线的零件之间那种犬牙交错的咬合机器根本切不出来。这个问题的本质是家具下料用的是推台锯或数控开料锯锯片只能沿一条直线把板材完全切开也就是“一刀切”约束。普通矩形排样允许零件在中间任意放置但锯切工艺要求每个切割动作都从板材的一边延伸到另一边或者至少能从当前余料的边界一直切到对面边界。这样一来能被生产接受的排样方案绝大多数都落在条带排样这个类别里。2.1 目标函数不是“面积利用率”这么简单理论上面积利用率 零件净面积之和 / 所用板材面积之和但这个指标不能单独当目标。现场算成本时还得多看两个数开了多少张板以及每张板切完之后剩的余料能不能回库。开三张板但利用率 88%和开两张板但利用率 84%多数工厂会选后者因为板材是整张买的少开一张板省下的成本往往比那 4% 的利用率差更值钱。所以我把下料目标拆成两层先看板数再看利用率。板数相同的前提下再去比较哪套方案留下的余料更小或者余料区域更适合下一单回用。在实际代码里这个目标一般写作def score(boards, used_boards, total_area): # 第一优先级用板数最少第二优先级可用面积利用率最高 return (used_boards, -total_area_ratio)这里我刻意把“面积利用率”取负号方便后面做局部搜索和遗传算法时的最小值比较。你要是想让算法优先控制切割刀数也可以把刀数插到第一优先级后面变成(used_boards, cuts, -area_ratio)这取决于车间里是板材贵还是人工和机器工时贵。2.2 三种常见路线整数规划、Shelf 条带排样、启发式搜索我把做这块经常遇到的路线整理成一张对比表方便你对号入座路线求解速度是否天然满足一刀切适用规模落地成本整数规划精确求解零件一多就极慢需要额外加切割约束单板零件小于 20 个高依赖求解器Shelf 条带贪心毫秒级天然满足日常拆单足够低纯手写可行元启发式搜索GA/SA/局部搜索秒级到分钟级取决于底层排样器适合批量优化中需调参整数规划模型看起来最“正确”但它要表达“一刀切”本身就麻烦。你得为每一条可能的切割线建变量零件数量超过 30 个之后求解时间就很容易从秒级膨胀到小时级。对定制家具这种每天几十张订单、每张订单零件数几百上千的场景这条路基本走不通除非你有专门的求解器许可和耐心。启发式搜索则反过来它不直接搜“零件放在哪个坐标”而是搜“零件以什么顺序进入排样器”。真正决定几何位置的是底层那个能保证一刀切的条带排样器搜索算法只负责找出更好的零件顺序。这样既保住了切割可行性又把搜索空间缩小成“排列序列”实现难度一下就降下来了。2.3 板式家具和钣金下料的本质差别一刀切约束钣金激光切割的套料可以做得非常满零件能放成互相咬合的形态因为激光头可以走任意曲线切完之后零件直接从板材上拿下来就行。木板板材不行锯片切出来的是直的锯缝而且锯片本身有厚度切一刀就是 3 到 4 毫米的损耗。这就是为什么板式家具的下料算法最后几乎都会落回 Shelf 条带排样把板材沿纵向分成若干条带每个条带里的零件横向排开。条带高度由这个条带里最高的零件决定条带和条带之间沿纵向叠放。只要条带内部所有零件都能放进可用宽度并且条带总高度不超过板高这套方案在锯床上就是可执行的。你看算法不需要操心那些复杂的几何咬合因为排版结构本身就是按照锯切路径设计的。很多工厂老师傅手工排料时实际上也在做同样的事只不过他们靠经验和尺子算法靠排序和规则。所以我的建议很直接第一个版本不要贪心去搞不规则排样先把 Shelf 条带排样写通。它够用、能切、容易解释而且给后面上遗传算法或局部搜索留下了一个非常干净的评分函数。3. 用 Python 跑通最小可用下料程序Shelf 排样与代码拆解目标是用纯 Python 标准库写一个最小版本不需要装任何第三方依赖放在任何一台电脑上都能跑。这一版解决的问题是给定一份零件清单和板型尺寸输出用了多少张板、每张板排了多少条带、整单利用率是多少。做完这一步你对“板式家具下料算法”这个标题就有了一个能摸到的实体而不是停留在概念层。3.1 先确定输入模型零件、板型与锯缝参数下料程序的输入结构比一般人想得更简单就是零件名、宽、高、数量和是否允许旋转。但我建议一开始就把“是否允许旋转”这个字段做进去因为柜体侧板这类零件通常需要纹理方向一致不允许 90 度旋转而层板、背板这类无所谓。板材尺寸按实际车间用料设置。常见标准板是 1220×2440但不同供应商会有 1830×2440 或 1220×2800 这类尺寸我一般把板宽和板高都定义成文件开头的常量方便后面直接改。锯缝宽度同样作为常量常用开料锯的锯片厚度在 3.2mm 左右带贴面精修锯的可能会到 4mm。板边留白是为防止锯切时边缘崩边一般取 5mm 到 10mm精度要求不高的车间也可以设为 0。3.2 Shelf 排样核心代码能跑通的最小版本import random from dataclasses import dataclass # 板材与锯切参数 PLATE_W 2440.0 # 板材宽度mm PLATE_H 1220.0 # 板材高度mm SAW_KERF 3.2 # 锯缝宽度mm EDGE_GAP 5.0 # 板边留白mm dataclass class Part: name: str w: float # 零件宽度mm h: float # 零件高度mm qty: int # 数量 rotate: bool True # 是否允许旋转 def pack_shelf(sequence, kerfSAW_KERF, edgeEDGE_GAP): Shelf 条带排样按给定零件顺序 把每个零件放入当前板中可用宽度最紧的条带 放不下就开新条带整板放不下就开新板。 返回值 boards 的每个元素代表一块板 每块板由若干条带组成条带格式为 [已用宽度, 条带高度, 零件列表]。 usable_w PLATE_W - 2 * edge # 去掉左右留白后的可用宽度 usable_h PLATE_H - 2 * edge # 去掉上下留白后的可用高度 # 先把零件展开成单个实例宽高都加锯缝 items [] for p in sequence: for _ in range(p.qty): w, h p.w kerf, p.h kerf if p.rotate and w usable_w: w, h h, w items.append((p.name, w, h)) boards [] # 完成排样的板 cur_shelves [] # 当前正在排的板里面是若干条带 def flush_board(): # 把当前板固定到 boards并清空工作区 nonlocal cur_shelves if cur_shelves: boards.append(cur_shelves) cur_shelves [] for name, w, h in items: best_shelf None # 先在当前板的已有条带里找能放下的优先选剩余宽度最小的条带 for s in cur_shelves: if s[0] w usable_w and s[1] h: if best_shelf is None or s[0] best_shelf[0]: best_shelf s if best_shelf is not None: best_shelf[0] w best_shelf[2].append((name, w, h)) continue # 当前板里没有合适条带尝试在当前板底部新开一条带 used_h sum(s[1] for s in cur_shelves) if used_h h usable_h: cur_shelves.append([w, h, [(name, w, h)]]) continue # 当前板放不下开新板 flush_board() cur_shelves.append([w, h, [(name, w, h)]]) flush_board() return boards def calc_utilization(boards, kerfSAW_KERF, edgeEDGE_GAP): 计算整单利用率零件净面积 / 可用板材面积 part_area 0.0 plate_area 0.0 for board in boards: plate_area (PLATE_W - 2 * edge) * (PLATE_H - 2 * edge) for s in board: for _, w, h in s[2]: part_area (w - kerf) * (h - kerf) return part_area / plate_area if __name__ __main__: parts [ Part(柜体侧板, 400, 600, 4, rotateFalse), Part(层板, 400, 360, 8, rotateTrue), Part(背板条, 120, 800, 6, rotateTrue), Part(抽屉面板, 300, 150, 10, rotateFalse), ] boards pack_shelf(parts) print(使用板数:, len(boards)) print(利用率: %.1f%% % (calc_utilization(boards) * 100)) for i, b in enumerate(boards, 1): h_used int(sum(s[1] for s in b)) print(f板 {i}: {len(b)} 个条带高度合计 {h_used}mm)这段代码的排样逻辑是每个零件先尝试放进当前板里已有的条带优先挑“剩余宽度最紧”的那个这样能把条带内部的空隙压小如果都放不下就在当前板底部新开一条带条带高度等于当前第一个零件的高度如果整块板都放不下就开下一张板。整个过程严格保持“条带内部横向排、条带之间纵向叠”的结构所以锯切路径一定是一刀接一刀切到底工人拿到就能按条带开料。关于旋转的判断我只处理了“宽超过可用宽度必须旋转”的情况。更好的做法是同时考虑纹理约束和宽度平衡比如旋转后能让条带高度更低时也可以转这部分留给下一章的局部搜索扩展。3.3 运行结果怎么解读利用率之外还要看开板数跑完这个最小程序你会得到三样东西用了多少张板、总利用率、每块板的条带分布。先说板数它直接决定订单的板材成本比利用率更值得盯着看。再说利用率这个数低于 75% 时先别急着优化算法大概率是零件尺寸本身和 2440 宽度不匹配或者旋转开关没开。最后看条带高度合计如果某张板的条带高度总和只有 800mm而板高 1220mm说明这块板尾部剩了 400mm 高的整条余料这种余料最值得回库下一单优先配给高度相近的零件。我跑这个样例时第一次结果通常在 78% 到 82% 之间板数在 3 张左右。对真实订单这个基线已经够用了。要是你想继续压问题很快就变成“到底按什么顺序给零件排队”这就是下一章要解决的核心问题。4. 把利用率从 78% 拉到 88%排序策略、局部搜索与参数整定同样的零件集合喂给同一个 Shelf 排样器只改变进入顺序结果能差出好几个百分点。这是因为 Shelf 算法的每一个决策都依赖当前状态先放高零件条带高度就被抬高了后面的矮零件填不满这个条带浪费就产生了先放矮零件条带高度低后面突然来个高零件又不得不为它单独新开一条带。所以排序策略其实就是下料算法的第一层优化空间。4.1 排序比算法更重要面积降序、宽度降序与随机顺序的差距我试过的最省事的策略是“面积降序”也就是大的先放小的后放让所有条带的高度分层更明显。还有一个常见策略是“宽度降序”优先把宽零件排进同一条带里减少条带内部剩余宽度。两者在不同订单下各有胜负但它们都比随机顺序稳定。排序策略适用场景典型效果随机顺序做基线对比用不稳定70%~82%面积降序零件大小差异大稳定 78%~84%宽度降序条带内塞宽板需求多稳定 79%~85%局部搜索优化后任意订单82%~88% 常见你可能会问为什么不直接用“高度降序”因为 Shelf 条带高度由当前条带第一个放入的零件高度决定先放高零件会导致整条带被抬高矮零件放进来的空间收益不高。实际操作中我一般先用面积降序跑一版再用局部搜索去微调顺序。4.2 局部搜索交换、逆转与精英保留一旦接受了“序列决定排样结果”这个设定剩下的就是经典的排列优化问题从当前序列开始随机交换两个零件的位置如果新序列能少开一张板或提高利用率就保留这个变化否则放弃。这个叫爬山法实现简单但对板式家具下料这种规模足够用。def expand_parts(parts): # 把带数量的零件列表展开成单个零件序列 seq [] for p in parts: for _ in range(p.qty): seq.append(Part(p.name, p.w, p.h, 1, p.rotate)) return seq def local_search(parts, iters3000, seed20240601): rng random.Random(seed) seq expand_parts(parts) def score(seq): boards pack_shelf(seq) # 优先板数少其次利用率高 return (len(boards), -calc_utilization(boards)) best_seq seq[:] best_score score(best_seq) for _ in range(iters): cand best_seq[:] a rng.randrange(len(cand)) b rng.randrange(len(cand)) cand[a], cand[b] cand[b], cand[a] # 交换两个零件的位置 s score(cand) if s best_score: # 元组比较(板数, -利用率) best_seq cand best_score s return best_seq, best_score注意这里我把“板数”和“负利用率”组合成一个元组来比较板数少就是绝对优势板数相同时才看利用率。这个设计很关键因为少数情况下放弃一点利用率把订单压到更少的板数上总成本反而是降的。一个订单跑 3000 次迭代毫秒级时间就完成了哪怕每个班次跑一次都毫无压力。4.3 遗传算法参数表种群、迭代、变异率怎么定零件数量超过 60 个之后单纯交换两个位置的局部搜索容易陷入局部最优这时候就得换成遗传算法用一组序列当种群每一代让好的序列两两交叉再让部分序列随机变异。代码骨架和局部搜索很接近关键是参数不能拍脑袋。参数建议范围说明种群大小40~120零件多取大值但计算时间会线性增长迭代代数200~800每代跑完当前种群所有个体交叉率0.6~0.9取两个父序列的前半段后半段按相对顺序补全变异率0.05~0.2太高等同于随机搜索太低会原地踏步精英保留2~5 个每代最优的几条直接进下一代防退化工程上更实用的做法是加一个“连续 50 代无改进就停”的终止条件。很多初学者写遗传算法跑完固定代数后发现结果不如局部搜索原因就是变异率过高把精英解冲掉了。我在现场调试时先固定精英保留数量再调变异率每一步只动一个参数这样翻车时能立刻定位是哪个设置在作怪。5. 板式家具下料必踩的坑锯缝、纹理、余料与搜索退化的排查清单这一章里写的全是实际跑下料程序时会遇到、但文档里基本不会告诉你的事。每一条我都按现象、原因、解决的顺序来总结你按顺序排查能省下大量试错时间。5.1 图纸利用率很高工人却切不出来没考虑一刀切现象算法输出的排版图利用率很好看零件也排得密密麻麻但工人拿着图纸比划半天发现锯片根本走不出那个路径最后只能对照着零件尺寸手工重排。原因底层的排样算法允许零件在任意位置放置没有限制锯切路径必须是整条直线。结果就是排出了“看起来满切不了”的咬合方案。解决把排样器换成 Shelf 条带结构或者在任意放置算法外面加一个剪切可行性校验。我的习惯是直接用条带排样器因为它的输出天生就是按条带组织的工人一看就知道先切哪条横缝、再切哪条竖缝。5.2 尺寸越切越小锯缝被重复计算或漏算现象程序算出来零件尺寸正好但实际切出来的板子窄了 3 毫米多切几刀误差更大到最后一块板直接报废。原因两个位置容易出错。一是零件之间的间距直接用了 0忘了把锯缝宽度加进去二是同一个锯缝在相邻两个零件的尺寸里各算了一遍等于把 3.2mm 的锯缝重复计算了两次。解决在数据进入排样器之前就把每个零件的宽高都加上锯缝和适当的加工余量计算利用率时再统一减掉。我通常在代码开头定义一个SAW_KERF常量所有地方引用同一个值避免各处写死数字导致改漏。另外建议在零件边到边的情况下单独加一个最小间距参数防止板材边缘崩边。5.3 饰面纹理方向翻车旋转开关的代价现象一批柜门零件排样时利用率很高结果装到家具上发现木纹方向横竖不一整个批次要重做。原因为了追求利用率程序自动把部分零件旋转了 90 度这些旋转后的零件纹理方向跟着转了。解决在 Part 模型里加上rotate字段木纹方向敏感的零件强制设为 False只允许旋转方向无关的零件参与自动优化。这里要狠一点宁可多开半张板也不要让项目整体跟不上进度。程序里检查一下最终的旋转数量占比如果超过阈值就说明这单的旋转约束没生效。5.4 遗传算法越搜越差精英丢失与随机种子抖动现象遗传算法前 50 代结果一路变好到 80 代后反而比第 30 代还差而且同一份订单每次跑出来的最优解都不一样车间没法用。原因最常见的坑是每代生成新种群时没有保留精英个体最优解在某一次交叉后被破坏掉后面再也没找回来。另一个是随机种子没固定导致每次跑优结果不同可复现性为零。解决每一代强制保留前 2 到 5 个最优个体直接进入下一代再对剩余个体做交叉和变异。调试阶段用固定随机种子比如代码里写random.Random(20240601)确认算法稳定后再放开。车间交付时直接把最优序列存成文件避免二次运算产生的抖动。5.5 余料复用过度切割刀数和搬运动作也是成本现象程序为了把利用率从 85% 提到 86%把一块不足 200mm 的小余料也匹配进了当前订单结果额外多切了三刀工人搬料、对刀、清废料的时间比省下的板材还贵。原因余料面积不够大时回库、找料、上料的成本远高于它本身的价值算法却只盯利用率完全不管人工成本。解决给余料设置一个最小面积阈值比如低于 0.15 平方米的余料不再参与回库匹配。同时把“切割刀数”加进评分函数让算法在利用率提升不足 0.5% 时主动放弃可能增加刀数的方案。这个参数没有固定推荐值和车间的人工费、板材单价直接相关建议先按当前成本结构算一笔账。6. 验证你的算法随机订单基准、SVG 可视化与可交付的落地方案一套下料算法值不值得正式上线不能只看一两个订单的漂亮数字得有一套能反复跑、能对比、能看排布图的验证流程。我习惯先构造一批固定的随机订单把种子定死确保任何时间重跑都得到同样结果。这样不管是自己改参数还是让同事帮忙复核都有一个统一的标尺。rng random.Random(20240601) bench_orders [] for _ in range(20): parts [] for i in range(rng.randint(8, 20)): w rng.choice([120, 180, 240, 300, 360, 400, 450, 600]) h rng.choice([150, 180, 240, 300, 360, 450, 600, 800]) parts.append(Part(fP{i}, w, h, rng.randint(1, 4))) bench_orders.append(parts)跑基准时我会对每一份订单先算面积降序的基线利用率再跑局部搜索最后记录“平均板数下降”和“平均利用率提升”两个数。如果提升不足 2%先检查排序和参数如果超过 5%也要警惕是不是旋转约束被放宽了。光学数字还是不够直观车间师傅要的是能看懂的图。把排样结果导成 SVG 是成本最低的可视化方式def export_svg(boards, pathlayout.svg): scale 0.15 total_w PLATE_W * scale total_h PLATE_H * scale y_cursor 0 items [] for board in boards: x_cursor 0 for s in board: for name, w, h in s[2]: items.append( frect x{x_cursor*scale:.1f} y{y_cursor*scale:.1f} fwidth{(w)*scale:.1f} height{(h)*scale:.1f} ffill#e8e8e8 stroke#333/ ftext x{(x_cursorw/2)*scale:.1f} fy{(y_cursorh/2)*scale:.1f} font-size6 ftext-anchormiddle{name}/text ) x_cursor w y_cursor s[1] svg ( fsvg xmlnshttp://www.w3.org/2000/svg fwidth{total_w:.0f} height{total_h:.0f} .join(items) /svg ) open(path, w, encodingutf-8).write(svg)生成的 SVG 用浏览器打开就能看到每块板的条带分布和零件位置矩形之间有没有重叠、缝隙大不大、哪块板尾料特别高一眼就能判断。我每次跑完算法都会把板数、利用率、条带高度总和和 SVG 图一起截图发到项目群里让加工师傅直接看图提意见。这套流程跑顺了下料方案就不再是黑匣子了——算法把方案摆出来人工做最后一道减法这才是现场真正愿意用的状态。希望帮到你。本文还有配套的精品资源点击获取