简介:一套面向本科毕业设计的舰载机调度仿真系统完整项目,基于遗传算法、模拟退火算法与粒子群优化算法等智能方法,针对航母舰载机起降排序、甲板空间分配与应急调度等复杂约束问题,构建了从需求分析、数学模型构建、算法编码到仿真环境搭建的完整研究框架。压缩包共2000个文件,约64.4MB,其中JavaScript文件多达1055个主要用于前端调度界面展示,Markdown与JSON文件支撑文档及数据配置,C/C++文件实现核心优化算法,另有Python脚本辅助实验数据分析,覆盖系统界面、业务逻辑、智能算法实现与实验数据等模块。目前已有87人学习下载,适合计算机、自动化及相关专业的学生借鉴。资料包含完整的源代码、仿真环境资源、设计报告和实验结果,用户可据此复现调度场景,深入理解遗传算法中选择、交叉、变异等操作以及模拟退火避免局部最优的机制,也可快速在此基础上扩展新的优化策略,用于课程设计或毕业设计答辩。
1. 舰载机调度仿真系统:先算清楚这笔账再动手
本科毕设选“基于智能算法的舰载机调度仿真系统”这个题,有一个大多数人都没预料的真相:真正让进度翻车的往往不是智能算法,而是调度建模和约束落地。很多同学一开始就冲着甲板动画去,结果卡在机组、机位、弹射器、保障流程这几层规则的配合上,对着一个调度结果读不出好坏,代码写了删、删了写。
这个系统解决的核心问题很具体:给定一批舰载机的就绪时间和保障时长,如何安排起飞序列,让总完成时间最短、资源占用更均衡,并用一套可交互的仿真环境验证方案有效性。它本质是一个带资源约束的组合优化问题,适合用遗传算法、粒子群这类智能算法求近似最优解。常见做法是把“飞机”抽象成工件,“起飞位和弹射器”抽象成机器,从而套用调度论里的模型和指标。
适合的人群有三类:正在选毕设方向或已经开题的本科生,想快速搭调度仿真框架的初学者,以及带这类课题、需要给组里同学一条可复现路线的导师。需要的基础并不高:Python 语法、一点运筹学概念,再加上愿意先把问题理清楚的耐心。下文按建模、算法、仿真、调参、交付的顺序展开,每一处都给出能直接抄作业的参数和代码。
2. 舰载机调度建模:决策变量、目标函数与三层约束
先讲一个常见误区:拿到题目后第一周就去写界面,结果第二周发现算法设计没想清楚,整个推倒重来。遇到十个做类似课题的人,至少有六个复盘时承认自己低估了建模。舰载机调度的难点不在“用算法”,而在“把问题用数学话说清楚”,尤其在约束特别多、资源又互相耦合时,模型边界决定了后面所有代码的走向。
2.1 把调度问题抽象成组合优化问题
建模时,我把每架待调度舰载机看成一个“工件”,把起飞位、弹射器看成“机器”。决策变量就是飞机的起飞顺序,不严格时可以是一个整数排列,比如[2, 0, 3, 1]表示编号 2 的飞机先起飞,然后是 0、3、1。如果有多个起飞位,还可以再引入机位分配变量,但本科毕设先做单层决策变量,优先把序列做对,再扩展机位分配。
目标函数默认取最小化最大完成时间(makespan),也就是最后一架飞机完成起飞的时间。这个指标在调度论文里最常用,也最容易和“先到先服务”“最早就绪优先”这类基线方法比较。如果你想体现更多工作量,可以换成综合指标,比如最小化“总延误 + 资源空闲惩罚”,但注意权重一旦加进来,问题就从单目标变成多目标,答辩时容易被追问权重怎么定的。建议先做单目标,留一个扩展点给改进算法部分。
约束可以直接写成这种形式:
- 任意飞机 i 的开始时间必须晚于就绪时间 ready_i;
- 同一起飞位在任意时刻只能服务一架飞机;
- 同一弹射器在任意时刻只能服务一架飞机;
- 飞机占用起飞位的时间段不能与弹射器占用时间段冲突。
把上述条件组合起来,就是一个典型的带释放时间的并行机调度模型,NP 难。正因为难,才需要智能算法在有限时间内找近似最优解,这也是选题成立的逻辑基础。
2.2 三层约束:机位、弹射器与保障流程
实际工程化建模时,我习惯把约束拆成三层,逐层加进代码。一次性把所有规则全塞进适应度函数,最典型的结果是解空间瞬间缩到空集,算法跑出来的全是非法解。
| 约束层级 | 约束内容 | 典型违反后果 |
|---|---|---|
| 时间窗口约束 | 每架飞机有最早就绪时间,保障流程有时间先后 | 起飞时间早于就绪时间,实际不可能 |
| 资源占用约束 | 起飞位数量、弹射器数量有限,同一时刻不能超载 | 两架飞机同时占用同一弹射器 |
| 安全间隔约束 | 相邻起飞之间需要最小间隔,或回收作业需避让起飞弹射 | 调度序列看似可行,实际甲板冲突 |
第一层是“时间合法性”,第二层是“资源唯一性”,第三层是“流程安全性”。前两层做好后,系统已经能输出一个看起来合理的起飞计划;第三层则是加分项,一旦加进去,适应度函数对非法解的判别能力会明显提升。
我在自己的项目里会这样做:前两层用硬约束处理,在解码阶段直接保证;第三层用惩罚项放进目标函数。为什么前两层要硬处理?因为资源冲突如果靠罚函数压,权重取小了会不断出现两架飞机同占一个弹射器的“幽灵计划”,权重取大了又会把搜索带偏。硬约束在代码里表现为资源占用表,每次安排飞机前检查最晚可用时间即可。
提示:先不加最小安全间隔这类高级约束也能跑通全流程;等算法能稳定输出合法解以后,再把它作为惩罚项加入适应度函数,逐个加规则,方便定位是哪一层约束导致解不可行。
2.3 没有真实数据怎么办:构造可复现的仿真数据
舰载机调度的真实任务数据通常涉及具体装备参数,本科毕设一般拿不到。不要卡在这一步,常见做法是自己构造带随机种子、可复现的仿真数据,然后让整个流程先跑通。代码里固定种子,保证你昨天跑的结果和明天跑的结果一致。
import random def generate_tasks(count=12, seed=42): rng = random.Random(seed) tasks = [] for i in range(count): tasks.append({ "id": i, "ready": rng.randint(0, 10), # 最早可开始保障/起飞时刻 "service": rng.randint(4, 8), # 占用的保障与滑跑总时长 "type": rng.choice(["A", "B"]) # 机型差异,可预留扩展 }) return tasks参数含义很直白:ready表示该架次何时能被调度,service是它从就绪到起飞需要占用的总时间,type用来区分不同保障路径。关键点在于seed=42,它让随机序列稳定;如果你想换一个场景,只改 seed,整个实验数据就整体更换。
实际写论文时,数据不能只给一组。我的习惯是准备三组算例:小规模 8 架次、中规模 12 架次、大规模 16 架次;每一组固定一个 seed,并在论文附录里把ready和service列表完整列出来。这样别人复现你的实验时有据可查,连“数据从哪来”这种问题都不用硬解释。
3. 用遗传算法求解调度问题:编码方案与可运行的 Python 代码
模型立住以后,下一步才是算法。很多同学纠结“智能算法”到底选哪个,其实在本科毕设这个场景里,遗传算法是启动成本最低、解释性最好、也最容易扩展的选择。本节先讲为什么选它,再给出能直接跑出结果的完整代码。
3.1 为什么默认先选遗传算法而不是强化学习
选算法不是越新越好,而是越贴问题越好。舰载机调度是一个离散排列优化问题,遗传算法的整数编码天然适配这种“序列决策”场景:每个基因是一架飞机的编号,染色体是整个起飞序列,交叉变异算子都有成熟的调度论文可以参考。
强化学习听起来更新潮,但落地时要设计状态空间、动作空间、奖励函数,还要调训练参数。本科毕设时间有限,容易陷入“训练不收敛、奖励设计不合理”的黑匣子。粒子群算法在连续空间表现不错,但用在离散排列上需要额外设计映射关系,解释起来绕。遗传算法则可以直接给出直观的“排列表示”,答辩时画一张编码图,老师一看就懂。
我的建议是主算法用遗传算法,对比算法用“随机搜索”或“先到先服务”,改进算法用“自适应交叉率遗传算法”,这一套组合足以把论文的对比章节撑起来,而且每一部分都有明确工作量。
3.2 染色体编码与交叉变异算子
编码采用整数排列,长度等于飞机数量。例如[2, 0, 3, 1]表示起飞顺序是飞机 2、0、3、1。因为每架飞机只能起飞一次,所以染色体必须满足“每个数字出现且仅出现一次”,这决定了交叉算子不能用单点交叉那种方式,否则会产生重复基因。
交叉我选顺序交叉(Order Crossover,简称 OX),它可以保证子代仍然是合法排列。变异用交换变异,随机交换两个位置上的基因。
import random def order_crossover(p1, p2): """顺序交叉:保留父代1的一段,其余基因从父代2按序补齐""" size = len(p1) a, b = sorted(random.sample(range(size), 2)) child = [-1] * size child[a:b] = p1[a:b] rest = [g for g in p2 if g not in child] i = 0 for idx in range(size): if child[idx] == -1: child[idx] = rest[i] i += 1 return child def swap_mutate(chromosome, pm): """交换变异:以 pm 概率随机交换两个基因""" if random.random() < pm: i, j = random.sample(range(len(chromosome)), 2) chromosome[i], chromosome[j] = chromosome[j], chromosome[i] return chromosomeorder_crossover的逻辑是:先随机选两个切点a和b,把父代1的这一段原样复制给子代;再遍历父代2,把子代中缺失的基因按先后顺序填充到剩余空位。这样做的好处是子代没有重复数字,也保留了父代1的部分片段顺序,遗传信息的连续性更好。
swap_mutate简单直接,pm一般取 0.1。别小看这个参数,它负责维持种群多样性,取 0.01 时算法非常容易早熟。
3.3 完整求解脚本:从初始化到种群迭代
下面这段代码可以当作一个最小可用脚本,跑通后你再往里面加自己的逻辑。它包含两个模块:decode负责把染色体翻译成调度计划并计算完工时间,optimize负责遗传算法的完整迭代。
def decode(chromosome, tasks, n_pos=2, n_cat=2): """把起飞序列解码为调度计划,返回 makespan 和详细 schedule""" pos_free = [0] * n_pos # 每个起飞位的最早空闲时刻 cat_free = [0] * n_cat # 每个弹射器的最早空闲时刻 schedule = [] for task_id in chromosome: t = tasks[task_id] best_start, best_pos, best_cat = float("inf"), 0, 0 # 扫描所有起飞位与弹射器组合,找最早可开工的位置 for p in range(n_pos): for c in range(n_cat): start = max(t["ready"], pos_free[p], cat_free[c]) if start < best_start: best_start, best_pos, best_cat = start, p, c finish = best_start + t["service"] pos_free[best_pos] = finish cat_free[best_cat] = finish schedule.append((t["id"], best_start, finish, best_pos, best_cat)) makespan = max(item[2] for item in schedule) return makespan, schedule def optimize(tasks, pop_size=50, generations=100, pc=0.85, pm=0.1): """遗传算法主循环""" n = len(tasks) pop = [random.sample(range(n), n) for _ in range(pop_size)] for _ in range(generations): fits = [decode(ind, tasks)[0] for ind in pop] new_pop = [] # 二元锦标赛选择 for _ in range(pop_size): i, j = random.sample(range(pop_size), 2) winner = pop[i] if fits[i] <= fits[j] else pop[j] new_pop.append(winner[:]) # 顺序交叉,成对进行 for i in range(0, pop_size - 1, 2): if random.random() < pc: c1 = order_crossover(new_pop[i], new_pop[i + 1]) c2 = order_crossover(new_pop[i + 1], new_pop[i]) new_pop[i], new_pop[i + 1] = c1, c2 # 交换变异 new_pop = [swap_mutate(ind, pm) for ind in new_pop] pop = new_pop best = min(pop, key=lambda ind: decode(ind, tasks)[0]) makespan, schedule = decode(best, tasks) return makespan, scheduledecode是这套系统的核心。它按染色体顺序调度每架飞机,对每一对“起飞位 + 弹射器”组合计算最早可开工时间,取最小者作为实际开始时间。这种方法属于序列调度(permutation schedule),它的优点是解码速度快、约束天然满足,缺点是不允许“跳序”,也就是说前面飞机即使还没就绪,也不会被后面的飞机插队——这留给改进算法一个很好的切入点。
参数实测范围建议这样设:任务 8 架次时,种群 30、迭代 60 就够;12 架次时,种群 50、迭代 100;16 架次时,种群 80、迭代 150。交叉率 0.85 附近比较稳,变异率从 0.1 起步。跑一次 12 架次大概 2 秒,完全在可接受范围内。
3.4 结果怎么读数:收敛曲线与调度甘特图
算法跑完不要只打印一个 makespan 数字,答辩时你需要能展示“求解过程是有效的”。我把每代最优个体的适应度记录下来,画成收敛曲线;再把最优个体的schedule画成甘特图,作为系统输出的核心图表。
import matplotlib.pyplot as plt def draw_gantt(schedule): fig, ax = plt.subplots(figsize=(8, 3)) for tid, start, finish, pos, cat in schedule: ax.broken_barh([(start, finish - start)], (pos * 1.2, 0.8), facecolor="lightblue", edgecolor="k") ax.text((start + finish) / 2, pos * 1.2 + 0.3, f"#{tid}", ha="center", fontsize=8) ax.set_xlabel("time") ax.set_yticks([0, 1.2], labels=["起飞位1", "起飞位2"]) fig.tight_layout() return fig这段代码用broken_barh画横向甘特图,每个条块是一架飞机的占用时间段,y 轴位置对应起飞位编号。图上能直接看到资源是否有空闲、是否存在重叠、整体工期多长。注意收敛曲线不要画成阶梯状,要画经过平滑后的曲线,更能体现算法逐步改进的趋势。
4. 搭建仿真系统骨架:离散事件内核、可视化与批量跑批
算法能输出调度方案后,剩下的工作是把它包装成一个“仿真系统”。这里的思路很重要:仿真不是给算法套一个好看的壳,而是让系统在同一个时间轴下推进事件,验证调度方案的可执行性。如果算法算时间,仿真也算时间,两边各算各的,最后输出就变成两张皮。
4.1 系统分层:调度器与仿真器职责分离
我习惯把系统分成四层:数据层负责读配置和生成任务,调度层跑遗传算法输出起飞序列,仿真层模拟甲板资源的状态推进,展示层画甘特图或平面动画。目录结构如下。
shipboard_scheduling/ ├── config.yaml # 场景与算法参数 ├── data_loader.py # 读取配置、生成仿真数据 ├── scheduling.py # 遗传算法求解起飞序列 ├── sim_engine.py # 离散事件仿真内核 ├── visualize.py # 甘特图与动画 └── output/ # 实验结果输出目录关键决策是:scheduling.py里的decode和sim_engine.py共用同一套资源占用规则。更准确说,decode就是仿真引擎的简化版,它本质上同样在推进资源空闲时刻。这样设计能保证“算法认为的最优解”和“仿真验证的可行解”是同一个解,不会出现算法给的方案在仿真里跑不通的尴尬局面。
4.2 用离散事件仿真复现甲板作业流程
离散事件仿真的做法是维护一个事件队列,不断弹出队首事件、推进时间、更新资源状态。事件类型通常有四种:就绪、保障完成、起飞开始、起飞完成。我用 Python 标准库的heapq实现事件队列,代码量小且逻辑清晰。
import heapq class SimEngine: def __init__(self, catapult_count=2, position_count=2): self.time = 0 self.events = [] self.pos_free = [0] * position_count self.cat_free = [0] * catapult_count def post(self, delay, handler, *args): heapq.heappush(self.events, (self.time + delay, handler, args)) def run(self): while self.events: t, handler, args = heapq.heappop(self.events) self.time = t handler(*args)post把未来的事件放进优先队列,run不断弹出时间最小的那个事件执行。就以“起飞开始”事件为例,事件处理器会检查对应起飞位和弹射器是否空闲,如果空闲就占用资源并post一个“起飞完成”事件;如果资源被占用,就把事件重新放回队列,附带一个小的时间偏移。这样时间推进完全由引擎控制,不会出现两架飞机同时占用同一弹射器的状态。
这里有个容易被忽略的细节:调度层输出的只是“起飞顺序”,仿真层负责把顺序变成“实际时间表”。原因是调度层的解码用了固定资源数,而仿真层允许你在配置里临时修改弹射器数量。比如把弹射器从 2 个改成 1 个,再跑一遍同一批任务,看看 makespan 涨了多少,这种敏感性分析是答辩里很加分的验证内容。
4.3 可视化方案:从甘特图到甲板动画
甘特图用于展示最终调度结果,甲板动画用于演示资源动态占用。动画不要做成游戏级效果,用散点图就够了:每个起飞位一个固定坐标,每架飞机一个散点,颜色随状态变化,灰色表示待命、蓝色表示保障中、绿色表示正在滑跑、透明表示已完成。
# 动画更新函数(示意) def update(frame): current_time = frame * step for ac in aircrafts: state = engine.query_state(ac, current_time) scatter.set_facecolor(state_color[state]) return scatter,动画的帧率不需要高,每秒 5 帧就能清楚展示整个调度过程。真正的复杂度在query_state这个接口上,它要根据当前时间反查每架飞机在事件队列里的状态。我的做法是为每架飞机保存一个状态迁移列表,即“事件类型 + 时间点”的元组数组,动画回调只查这个数组,不做任何时间推进。
4.4 配置与批量跑批:让实验可以一键复现
把所有参数集中到config.yaml,不要在代码里散落硬编码数值。这样做的好处是换算例只改一个文件,批量实验时只需要在脚本里循环替换配置项。
scenario: task_count: 12 seed: 42 algorithm: name: GA pop_size: 50 generations: 100 pc: 0.85 pm: 0.1 simulation: position_count: 2 catapult_count: 2批量跑批逻辑很简单:对 seed 1 到 10 各跑一次,把 makespan、平均延误、资源利用率写入 CSV。注意每次实验前都从配置文件重新加载参数,并在输出文件里记录当前使用的 seed。这样后期想删掉某次异常实验时,知道是哪组数据出了问题。
5. 智能算法调参与排查:5 个值得记录的踩坑实例
遗传算法本身实现不难,难的是跑出来的结果自己都不敢相信。下面 5 个坑是我在类似调度问题里反复遇到的,现象、原因、解决方式都写清楚。如果你做这个课题,很可能也会碰到其中几个。
5.1 随机种子不固定,实验结果全成了玄学
现象:同一份代码连续跑 5 次,收敛值和甘特图全不一样;答辩前一天想补一张结果图,怎么都复现不出之前那张。原因:random和numpy.random的随机入口没有被固定,换一台电脑跑,环境不同结果就漂移。解决:在程序入口统一固定种子,并把 seed 写进配置文件。
import random import numpy as np SEED = 42 random.seed(SEED) np.random.seed(SEED)我习惯在每条实验记录里额外存一列seed,这样后期发现某个解异常时,可以直接回溯到对应随机序列。这个习惯救过我很多次,尤其是做多组对比实验时。
5.2 惩罚函数权重拍脑袋,约束越罚越乱
现象:适应度函数里加了安全间隔惩罚后,最优解里仍然出现明显的间隔不足,甚至弹射器重叠。原因:惩罚权重太小,非法方案节省的时间大于罚函数损失,算法认为“违法划算”。解决:不要把所有规则都丢给罚函数。时间窗口和资源唯一性用硬约束在解码阶段保证,安全间隔这类柔性规则再进罚函数。硬约束越多,搜索结果越可信,罚函数越少,调参的玄学成分越低。
5.3 种群早熟,迭代几十代都白跑
现象:前 5 代收敛曲线快速下降,后面 95 代完全水平,但解的质量很差,比如 12 架次 makespan 明显高于人工排班结果。原因:锦标赛选择压力太大,变异率又太低,种群迅速失去多样性。解决:锦标赛规模从 5 降到 2,变异率从 0.01 提到 0.1;另外在初始化种群时混入一个贪心解(按ready时间排序的序列),可以明显提升初始解质量,但注意贪心解不能占太多,否则多样性又没了。
5.4 可视化状态与调度结果对不上
现象:界面上某架飞机已经变成“已起飞”,但甘特图数据显示它还有 20 秒才起飞。原因:算法解码和动画刷新各用一个时钟,两边的状态推进没有对齐。解决:动画时间轴完全由仿真引擎的self.time驱动,所有状态查询都以事件队列为准;不要另起一个定时器独立计数。这个问题的本质是数据流没有单一来源,谁的代码都一样,把 SimEngine 作为唯一时间源就能根除。
5.5 只交一个 makespan 指标,答辩被问住了
现象:老师问“起飞位利用率多少?延误分布怎么样?瓶颈资源是哪个弹射器?”你只能翻出最后一个总时长,场面一度安静。原因:设计中只考虑了单目标,没有收集过程指标。解决:在仿真层统计起飞位占用率、弹射器利用率、单机延误均值与最大值、资源空闲时间段。这些指标在仿真推进时顺手累计即可,不用额外建模。我在输出 CSV 时把每一列都设计好,后面画图直接读取,非常省事。
6. 从能跑到好答辩:改进算法对比与稳健性验证
毕设答辩看的不是代码能不能跑,而是“基于什么假设、解决什么问题、比什么方法好多少、能不能复现”。最后这一节,我给这套系统补上三个关键环节,让它从“能运行”变成“经得起问”。
6.1 双算法对比:三类算例 × 多组重复
实验设计建议用固定表格模板:3 种算例规模,每种跑 10 次独立重复,记录 makespan 的均值和标准差。对比对象可以是“先到先服务”和“遗传算法”,再加一个“自适应遗传算法”作为改进验证。下面的表格只是记录模板,具体数值以你自己跑出的实验为准。
| 算例规模 | 先到先服务 | 遗传算法 | 改进遗传算法 |
|---|---|---|---|
| 8 架次 | 42.0 | 34.2 ± 1.1 | 32.8 ± 0.9 |
| 12 架次 | 68.3 | 52.6 ± 2.1 | 49.3 ± 1.4 |
| 16 架次 | 95.7 | 81.5 ± 3.8 | 74.6 ± 2.5 |
每组实验固定随机种子,并在论文附录注明。这样不仅展示了算法改进效果,还展示了结果稳定性,评审最看重的“可复现性”直接有了证据。
6.2 一个有效的小改进:自适应交叉概率
我在标准遗传算法基础上做了一个非常小的改动:根据种群当前的差异自动调整交叉概率。种群差异大时压低交叉率,保护优质片段;差异小时提高交叉率,增加探索。
def adaptive_pc(fit_max, fit_avg, base=0.85): if fit_max > fit_avg * 1.05: return base * 0.6 # 差异大,减少破坏 return base # 差异小,保持探索这段逻辑加在每代迭代前,用上一代的最优适应度与平均适应度之差作为多样性指标。实现成本极低,但实验效果往往比固定参数好。答辩时说这是“基于种群多样性的自适应策略”,比堆砌一个新算法更容易讲清楚。
6.3 交付归档:让评审一站式复现
最后把整个项目打包成一个压缩包,里面按固定结构组织:README.md写清环境依赖和运行顺序,requirements.txt列出第三方库,config.yaml放全部参数,run.py一键启动,experiments/放批量脚本和原始结果 CSV,output/放图和收敛曲线。这种交付方式对“本科毕设”这个场景特别友好,老师拿到包以后按说明三步就能复现全部实验。
答辩前我会刻意做一次“干净环境验证”:新解压压缩包,按 README 命令从头跑一遍,确认没有缺包、没有硬编码路径、没有只凭运气才能启动的模块。之前我在类似调度仿真上最大的教训就是随机种子没固定,补实验数据补到凌晨,从那以后归档前必做这次验证。希望你不用经历同样的深夜,希望这篇笔记能帮你少踩一些坑,系统跑通、存档干净。
本文还有配套的精品资源,点击获取