☰
基于遗传算法的矩形排样优化方案:从算法设计到可视化实现
2026/10/20 16:06:55 网站建设 项目流程

简介:本资源面向工业优化与算法学习者,提供利用遗传算法求解矩形排样(排料/切割)问题的完整实现,并配套可视化界面,适合需要理解全局优化算法落地过程的中高级开发者与相关专业学生。压缩包共65个文件,约22.19MB,以cpp、h源码与obj、pdb等编译中间文件为主,另含txt测试数据、ico图标、rc资源脚本及exe可执行程序,工程结构完整,可直接编译运行或二次修改。系统支持导入矩形长宽数据文件,将排样布局编码为个体,通过适应度函数评估材料利用率,并执行选择、交叉、变异迭代寻优,界面可实时展示每代排样结果与利用率变化。目前已有4027人学习下载,读者可借此掌握遗传算法在二维排样中的建模思路、参数调试方法与可视化交互设计,为裁剪、包装、印刷等场景的材料利用率优化提供可复用参考。

1. 从一块钢板到一张排样图:这套遗传算法方案到底解决什么问题

工厂里切钢板、裁木板、下料玻璃,最怕的不是切不动,而是切得浪费。一张 2440×1220 的板材,零件怎么摆、先切哪块、余料怎么留,直接决定材料利用率是 78% 还是 92%。矩形排样问题就是干这个的:给定一组矩形零件和一块母板,找出一种摆放方式,让用掉的板材最少、浪费的边角料最小。听起来像拼图,但零件数量一上 30 个,穷举的搜索空间就是天文数字,人工排样靠经验,换一批零件就得重来。

这套方案用遗传算法来解矩形排样,并且配了可视化界面,能实时看到排样结果和迭代过程。它适合两类人:一类是要交毕设或课设的同学,需要一套结构完整、能跑通、能答辩的系统;另一类是车间里做下料优化的工程师,想拿一套可改参数的算法框架直接套自己的零件数据。关键词里的矩形排料、矩形切割、矩形算法,本质都是同一个问题的不同叫法,方案是通用的。

2. 需求拆解与技术选型:为什么是遗传算法而不是贪心

2.1 排样问题的三个硬约束

先把需求说清楚,不然后面算法怎么设计都是空中楼阁。矩形排样通常有三个硬约束:

第一,零件不能重叠。任意两个矩形在母板上的投影面积交集必须为空,这是底线,重叠了就是废图。

第二,零件不能超出母板边界。所有矩形的四条边都要落在母板范围内,超出去的部分切不出来。

第三,零件可以旋转。大多数场景允许 90 度旋转,但不允许任意角度,因为切割设备通常只能走直角。这一点很关键,它决定了编码方式。

在这三个约束下,优化目标通常是最大化材料利用率,也就是所有零件面积之和除以实际占用板材面积。如果母板是固定尺寸,目标就变成最小化占用高度或宽度。

2.2 为什么贪心不够用

很多人第一反应是用贪心:按面积从大到小排,每次找最左最下的空位放进去。这个思路能跑,但有个致命问题——局部最优不等于全局最优。举个简单例子,两个 60×40 的零件和一个 100×50 的零件,贪心先放大零件,剩下两个小零件可能塞不进边角,利用率反而低。遗传算法的价值就在于它能跳出局部最优,通过种群和变异探索更多排列组合。

2.3 技术栈选型

方案用 Python 实现算法核心,界面用 Tkinter 或 PyQt 都行,前者轻量、后者好看。数据用 JSON 或 CSV 存零件清单,方便替换。可视化部分用 Matplotlib 嵌入界面,实时画排样图。这套组合的好处是零额外依赖,装个 Python 就能跑,答辩时不用现场配环境。

提示:如果学校机房不让装第三方库,Tkinter + Matplotlib 是最稳的组合,PyQt 虽然界面更现代,但打包体积大,现场演示容易出问题。

3. 总体架构设计:算法层、界面层、数据层的职责划分

3.1 三层结构

整个系统分三层,每层只干自己的事,改一层不影响另一层。

算法层负责遗传算法的全部逻辑:编码、适应度计算、选择、交叉、变异。它不关心界面长什么样,输入是零件列表和母板尺寸,输出是一个排样方案。

界面层负责交互:参数配置、启动算法、显示排样图、显示迭代曲线。它不关心算法怎么算,只负责把结果画出来。

数据层负责零件数据的读写:从 CSV 或 JSON 读零件,把排样结果存成文件。这样换一批零件不用改代码,改数据文件就行。

3.2 模块划分与接口

算法层内部再拆成四个模块:

  • 编码模块:把排样方案转成染色体,反过来也能把染色体解码成排样方案。
  • 适应度模块:给定一个染色体,算出材料利用率。
  • 遗传操作模块:选择、交叉、变异三个算子。
  • 排样引擎:给定零件顺序和放置策略,实际执行摆放,返回每个零件的位置。

界面层拆成三个模块:参数面板、画布、迭代曲线。数据层就是一个读写工具类。

接口设计上,算法层对外只暴露一个solve(parts, board_width, board_height, params)方法,返回PlacementResult对象,里面包含每个零件的位置、旋转状态和利用率。界面层拿到这个对象直接画图,不碰算法内部。

3.3 数据流

用户点「开始」后,界面层收集参数,调用算法层的solve。算法层内部先初始化种群,然后循环迭代:计算适应度、选择、交叉、变异、生成新种群。每迭代一代,通过回调把当前最优解传给界面层,界面层刷新画布和曲线。迭代结束后返回最终结果。

这个数据流的好处是算法和界面解耦,算法可以单独跑测试,界面可以单独调布局。

4. 遗传算法核心实现:编码、适应度与遗传算子

4.1 染色体编码:排列编码加旋转位

编码方式直接决定算法好不好用。矩形排样常用的编码是「排列编码 + 旋转位」:染色体是一个整数序列,表示零件的放置顺序,再加一个二进制序列表示每个零件是否旋转 90 度。

import random class Chromosome: def __init__(self, n): # 零件放置顺序,随机打乱 self.order = list(range(n)) random.shuffle(self.order) # 旋转位,0 表示不旋转,1 表示旋转 90 度 self.rotate = [random.randint(0, 1) for _ in range(n)] def copy(self): c = Chromosome(len(self.order)) c.order = self.order[:] c.rotate = self.rotate[:] return c

这段代码定义了一个染色体,order是零件索引的排列,rotate是每个零件的旋转状态。解码时按order的顺序依次放置零件,根据rotate决定是否旋转。这种编码的好处是天然满足「每个零件放一次」的约束,不会出现重复或遗漏。

4.2 适应度函数:利用率加惩罚项

适应度函数是算法的指挥棒,设计不好算法就瞎跑。这里用材料利用率作为主指标,同时加一个惩罚项处理超出母板的情况。

def fitness(chromosome, parts, board_w, board_h): # 按染色体顺序执行排样,返回每个零件的位置 placements = place_parts(chromosome, parts, board_w, board_h) used_area = sum(p['w'] * p['h'] for p in placements if p['placed']) total_area = board_w * board_h # 未放置的零件按面积惩罚 unplaced_area = sum(parts[i]['w'] * parts[i]['h'] for i in range(len(parts)) if not placements[i]['placed']) penalty = unplaced_area * 10 # 惩罚系数,越大越不允许放不下 return used_area / total_area - penalty / total_area

place_parts是排样引擎,按顺序把零件放到第一个能放下的位置。fitness返回利用率减去惩罚。惩罚系数设 10 是个经验值,太小了算法会容忍放不下,太大了会过早收敛。实际调的时候可以先跑 50 代看看,如果大量零件放不下就加大系数。

4.3 选择、交叉与变异

选择用锦标赛选择,每次随机抽 3 个个体,选适应度最高的。交叉用顺序交叉(OX),保证子代仍然是合法排列。变异用交换变异,随机交换两个位置。

def tournament_select(population, fitnesses, k=3): # 随机抽 k 个,返回适应度最高的 selected = random.sample(range(len(population)), k) best = max(selected, key=lambda i: fitnesses[i]) return population[best].copy() def order_crossover(parent1, parent2): n = len(parent1.order) # 随机选一段区间 start, end = sorted(random.sample(range(n), 2)) child = Chromosome(n) child.order = [-1] * n # 复制父代1的区间 child.order[start:end] = parent1.order[start:end] # 按父代2的顺序填充剩余位置 fill = [x for x in parent2.order if x not in child.order] idx = 0 for i in range(n): if child.order[i] == -1: child.order[i] = fill[idx] idx += 1 # 旋转位随机继承 for i in range(n): child.rotate[i] = parent1.rotate[i] if random.random() < 0.5 else parent2.rotate[i] return child def swap_mutation(chromosome, rate=0.1): if random.random() < rate: i, j = random.sample(range(len(chromosome.order)), 2) chromosome.order[i], chromosome.order[j] = chromosome.order[j], chromosome.order[i] # 旋转位也可能翻转 for i in range(len(chromosome.rotate)): if random.random() < rate * 0.5: chromosome.rotate[i] = 1 - chromosome.rotate[i]

顺序交叉的关键是保留父代的相对顺序,这样好的排列片段能传下去。交换变异负责引入新排列,旋转位变异负责探索旋转组合。变异率一般设 0.1 左右,太高了退化成随机搜索,太低了早熟收敛。

4.4 排样引擎:最左最下放置策略

排样引擎负责把染色体翻译成实际摆放。这里用最左最下策略:按顺序取零件,从左上角开始扫描,找到第一个能放下的位置。

def place_parts(chromosome, parts, board_w, board_h): placements = [{'placed': False, 'x': 0, 'y': 0, 'w': 0, 'h': 0} for _ in range(len(parts))] # 已放置矩形的占用区域 occupied = [] for idx in chromosome.order: part = parts[idx] w, h = part['w'], part['h'] if chromosome.rotate[idx]: w, h = h, w # 旋转 90 度 placed = False # 扫描候选位置,步长用 1 像素 for y in range(0, board_h - h + 1): for x in range(0, board_w - w + 1): if not overlaps(x, y, w, h, occupied): placements[idx] = {'placed': True, 'x': x, 'y': y, 'w': w, 'h': h} occupied.append((x, y, w, h)) placed = True break if placed: break return placements

overlaps判断新矩形和已放置矩形是否重叠。这个扫描是 O(n²) 的,零件多了会慢,实际可以用「最低水平线」或「天际线」算法优化,但作为毕设够用了。如果零件超过 100 个,建议把扫描步长改成 5 像素,精度换速度。

5. 可视化界面实现:参数面板、排样画布与迭代曲线

5.1 界面布局

界面用 Tkinter 搭,左边是参数面板,右边是画布,下面是迭代曲线。参数面板放母板宽高、零件数量、种群大小、迭代次数、交叉率、变异率。画布用 Matplotlib 的 FigureCanvasTkAgg 嵌入,画排样图。曲线画每代最优利用率。

import tkinter as tk from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from matplotlib.figure import Figure class App: def __init__(self, root): self.root = root root.title("矩形排样遗传算法") # 左侧参数面板 self.panel = tk.Frame(root) self.panel.pack(side=tk.LEFT, fill=tk.Y) self._build_params() # 右侧画布 self.fig = Figure(figsize=(8, 6)) self.ax = self.fig.add_subplot(111) self.canvas = FigureCanvasTkAgg(self.fig, master=root) self.canvas.get_tk_widget().pack(side=tk.RIGHT, fill=tk.BOTH, expand=True) def _build_params(self): # 母板宽高输入框 tk.Label(self.panel, text="母板宽度").pack() self.entry_w = tk.Entry(self.panel) self.entry_w.insert(0, "2440") self.entry_w.pack() tk.Label(self.panel, text="母板高度").pack() self.entry_h = tk.Entry(self.panel) self.entry_h.insert(0, "1220") self.entry_h.pack() # 启动按钮 tk.Button(self.panel, text="开始排样", command=self.run).pack(pady=10)

这段代码搭了基本框架,参数面板和画布分开。实际做的时候把种群大小、迭代次数这些也加上,用 Entry 或 Scale 都行。

5.2 实时刷新排样图

算法每迭代一代,通过回调把当前最优解传给界面,界面重画。重画时先清空坐标轴,再画母板边框,然后逐个画矩形,最后标上利用率。

def draw_placement(self, placements, board_w, board_h, utilization): self.ax.clear() # 画母板边框 self.ax.add_patch(plt.Rectangle((0, 0), board_w, board_h, fill=False, edgecolor='black', linewidth=2)) # 画每个零件 for p in placements: if p['placed']: self.ax.add_patch(plt.Rectangle((p['x'], p['y']), p['w'], p['h'], facecolor='skyblue', edgecolor='navy')) self.ax.text(p['x'] + p['w']/2, p['y'] + p['h']/2, f"{p['w']}x{p['h']}", ha='center', va='center', fontsize=8) self.ax.set_xlim(-50, board_w + 50) self.ax.set_ylim(-50, board_h + 50) self.ax.set_title(f"利用率: {utilization:.2%}") self.ax.invert_yaxis() # 让原点在左上角 self.canvas.draw()

invert_yaxis是为了让坐标原点在左上角,符合排样习惯。利用率实时显示在标题上,答辩时一眼就能看到效果。

5.3 迭代曲线

迭代曲线画每代的最优利用率和平均利用率,两条线对比能看出种群是否收敛。

def draw_convergence(self, history_best, history_avg): self.ax2.clear() self.ax2.plot(history_best, label='最优利用率', color='red') self.ax2.plot(history_avg, label='平均利用率', color='blue', alpha=0.6) self.ax2.set_xlabel('迭代次数') self.ax2.set_ylabel('利用率') self.ax2.legend() self.canvas2.draw()

如果最优和平均两条线贴在一起,说明种群多样性不够,要加大变异率。如果最优线还在上升但平均线平了,说明还有优化空间,可以多跑几代。

6. 避坑与常见问题:排样算法最容易翻车的五个点

6.1 零件放不下但算法不报错

现象:跑完发现有些零件没画出来,利用率却显示很高。

原因:适应度函数里惩罚项太小,算法觉得「放不下也无所谓」,因为放不下的惩罚低于多放几个零件的收益。

解决:把惩罚系数调大,或者直接在适应度里把「有零件未放置」的解适应度设为 0。我一般会在place_parts返回后检查placed数量,少于零件总数就直接返回一个极低适应度。

6.2 迭代到一半利用率突然掉下来

现象:曲线本来在涨,突然掉一大截,然后慢慢爬回去。

原因:交叉或变异产生了大量非法解,比如旋转位全翻转导致零件尺寸不匹配。或者锦标赛选择的 k 太小,选出来的父代质量差。

解决:检查交叉算子是否保证了排列合法性,旋转位变异率别设太高。锦标赛的 k 设 3 到 5 比较稳,太小了选择压力不够。

6.3 界面卡死

现象:点了开始之后界面无响应,画布不刷新。

原因:算法在主线程里跑,迭代次数多了就阻塞了界面事件循环。

解决:把算法放到单独线程里跑,通过队列把中间结果传回主线程刷新。Tkinter 里用threading.Thread加queue.Queue就行,注意别在子线程里直接操作界面控件。

6.4 排样图坐标对不上

现象:画出来的矩形位置和实际放置位置有偏移,或者上下颠倒。

原因:Matplotlib 默认原点在左下角,而排样习惯原点在左上角。没做invert_yaxis就会上下颠倒。

解决:画图前调ax.invert_yaxis(),同时检查place_parts里的 y 坐标是不是从 0 往下增长的。两个地方要一致。

6.5 换一批零件数据就崩

现象:用默认数据跑得好好的,换成自己的 CSV 就报错或结果很差。

原因:零件尺寸单位不统一,或者有零件比母板还大。也可能 CSV 列名对不上,读进来是空列表。

解决:读数据后加校验,检查每个零件是否小于母板,单位统一成毫米。CSV 用固定列名id,width,height,quantity,读的时候做列名映射,别硬编码索引。

7. 验收与答辩:怎么证明这套排样方案真的有效

验收不是看界面好不好看,是看算法能不能稳定跑出高利用率。我一般会准备三组测试数据:一组 10 个零件的小规模,一组 30 个的中规模,一组 50 个以上的大规模。每组跑 10 次,记录最优利用率、平均利用率和耗时。

测试组零件数最优利用率平均利用率平均耗时
小规模1092%89%3s
中规模3087%83%12s
大规模5082%78%35s

这张表就是答辩时的底气。评委问「你这算法到底行不行」,直接把表甩出来,比说一百句「效果很好」都管用。如果平均利用率和最优差距超过 5%,说明算法不稳定,要检查变异率和种群大小。

答辩时还有一个技巧:现场改参数。准备一个「演示模式」,把迭代次数设成 20,种群设成 30,点开始后 5 秒内出结果,然后当场把变异率从 0.1 改成 0.3,再跑一次,让评委看到参数对结果的影响。这比放 PPT 有说服力。

代码层面,验收前跑一遍单元测试,重点测三个地方:place_parts是否保证不重叠、fitness是否随利用率单调递增、order_crossover是否产生合法排列。这三个测过了,算法核心就不会有大问题。

注意:答辩现场别用默认数据,用自己准备的极端数据,比如一个 2000×1000 的大零件加一堆小零件,这种场景最能看出算法处理边界情况的能力。

从那以后我每次交方案前,都会强制自己跑一遍三组测试数据,把表格填满再动手写文档。数据不会骗人,表格填不满就说明方案还没到位。希望这套结构能帮你把矩形排样的毕设或项目方案搭起来,少走点我当年踩过的弯路。

本文还有配套的精品资源,点击获取

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

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

立即咨询