简介:一份针对2021年华为杯F题(机组排班优化问题)的思路与代码包,面向参加中国研究生数学建模竞赛的在校研究生,尤其适合需要快速上手、参考完整求解流程的备赛者。包内含赛题思路笔记、数据处理脚本与分步求解代码,覆盖数据预处理、模型构建及结果分析等环节。压缩包共16个文件,以9个Python脚本和4个CSV数据文件为主,另有Word赛题文档、txt注释说明及Markdown思路文档,整体仅211KB,轻量精炼,便于对照学习。目前已有135人学习浏览。借助该思路包,读者可清晰理解F题的排班建模逻辑,并参考各题代码中的详细注释(特别是第2问)进行复现与扩展,节省从零摸索的时间,是一份实用价值较高的竞赛参考资料。
1. F题不是“排班题”,是“拿奖题”
每年9月中旬的研究生数模竞赛,F题永远以“看起来很像运筹学”的面目出现。2021年的F题是航空公司机组优化排班,很多队拿到题目就慌了——机组、航班、休息期,数据一堆,以为要用专门的调度软件。可你把题目拆开看,本质就是三道递进的整数规划:先让你把每个航班“盖”上一组机组,再让你把“机组连续休息”这类规则写进约束,最后让你在成本、均衡、疲劳度里挑一个目标做优化。用Matlab的intlinprog完全能顶住,难点在建模而不是算法。下面按“考什么→哪三问→怎么建模→易错哪→怎么答题”给你过一遍。适合目标省一及以上的队伍,尤其是不想卷信号题、无人机题的选手。
2. 考点分布与赛题拆解:F题的三个小问到底在考什么
2.1 研赛F题的整体结构:数据、问题、代码三位一体
拿到题先看文件目录。这份资源里给了Data文件夹,里面有A-Crew.csv、A-Flight.csv、B-Crew.csv、B-Flight.csv,还有problem文件夹下的“航空公司机组优化排班问题.docx”,code文件夹下分第1题、第2题、第3题,并且第3题代码的详细注释放在第2问中。这说明题目不是只有一套数据,而是分为A、B两组数据,对应不同的机队或航线网络。比赛时多数队伍会栽在第一步:不建索引,直接用Excel打开CSV,看到时间格式就懵。正确做法是先把CSV读进Matlab,统一时间格式,把航班号和机组ID变成分类变量,然后才开始建模。
| 文件路径 | 内容 | 使用时机 |
|---|---|---|
| Data/A-Flight.csv | 航班表(航班号、起降时间、起降机场等) | 第一问建模输入 |
| Data/A-Crew.csv | 机组组列表(人员组、基地、可用时段) | 第二问约束输入 |
| Data/B-Flight.csv | 另一组航班数据 | 第三问换数据验证 |
| Data/B-Crew.csv | 另一组机组数据 | 第三问换数据验证 |
| problem/航空公司机组优化排班问题.docx | 题目全文与三个小问 | 第一天精读 |
| code/第1题、第2题、第3题 | 对应三问的Matlab实现 | 第二天起对照 |
这类“给数据+给问题文档”的赛题,评分标准一般按三部分走:模型是否完整(40%)、求解与结果是否合理(35%)、论文表达与图表(25%)。资源里单独列出的“思路.md”就是对三小问的逐题分析,配合data里的CSV,能省下很多整理数据的时间。先把这个结构认清楚,后面建模才不会乱。
2.2 三个小问的考点层次与分值预估
第一问通常是基础排班:给定一组航班和机组集合,要求每个航班分配机长和乘务组,满足最基本的覆盖,目标是最少使用机组数或最低成本。这一问考的是“0-1变量+覆盖约束”的建模基本功,属于送分但容易丢分的地方。第二问会加“机组连续休息时间”或“基地出发返回”这类真实规则,这就需要把时间轴上的状态变成约束条件,考的是约束矩阵的构造。第三问一般会升级为多目标或者多基地、多机型的组合优化,考的是降阶、线性化、或者用遗传/粒子群这类启发式算法做近似。
从近几年的分水岭看,大多数队伍能做出第一问,但第二问开始拉开差距:约束少了会被判定“规则未满足”,约束多了模型规模爆炸。第二问的建模水平直接决定你是有省奖还是国家奖。第三问只要给出“把多目标加权化为单目标”的合理方案,就能拿到大半分数,不要求你真的跑出全局最优。所以复习重点应该压在第2问的约束表达和矩阵构造上,而不是盲目追求高级算法。
2.3 这份资源里“思路.md”和“code”的使用顺序
不要拿到资源先翻代码,也不要先看思路。比赛时间只有4天,正确顺序是这样的:第一天上午通读“航空公司机组优化排班问题.docx”,把三个小问的约束逐条抄成清单;下午看“思路.md”,把每问的建模变量和约束对号入座;晚上再打开code文件夹,对照第1题和第2题的注释理解怎么用intlinprog写矩阵。第3题的代码在第2问中带详细注释,说明第三问很可能是在第二问基础上加目标函数或松弛约束,不是独立模型。
这样用的原因在于,代码是别人整理好的“黑匣子”思路,如果你先读代码,很容易照搬而不知道自己改了哪些参数对应什么含义。评委最反感的是“模型很漂亮,但结果对不上约束”。先把问题文档梳理成数据表,再对着思路文档填变量,最后用代码验证,这是最稳的拿奖路径。这也解释了为什么资源要分“思路.md”和“code”——思路管建模,代码管落地,缺一不可。
3. 高频考点:机组排班建模的三大核心
3.1 考点一:0-1变量与航班覆盖约束
机组排班第一关是把“谁飞哪个航班”表示成决策变量。设x(i,j)=1表示第j组机组执行航班i,否则为0。每个航班至少要有一组机组覆盖,所以对航班i有sum_j x(i,j) >= 1。这里有个很多队伍会踩的坑:题目里的“机组”是机长、副驾驶、乘务员的组合,不是一个个体。所以更准确的建模是“机组组”,也就是把同一套人员组合当一个整体。A-Crew.csv和B-Crew.csv里每一行应视为一个可用的机组排班单元。
覆盖约束是整数规划里最基础的约束,但它的坑在“等于”还是“大于等于”。如果允许某个航班由两组机组共同执行(比如长航线需要换班),就要用>=;如果严格要求每个航班恰好一组,则用=。2021年F题大概率是“至少一组”或“恰好一组”,读题时注意“每架航班必须且只能分配一组”的表达。第二个坑是同一机组在同一时间段不能被重复指派,这需要加时间冲突约束,不是单看航班覆盖就能解决的。真正的比赛模型里,x(i,j)前面还要乘上一个“可执行系数”,如果机组组j没有执飞航班i的资质,直接就设x(i,j)=0或从变量集合里剔除。
3.2 考点二:机组休息与基地约束的建模
机组不能连续飞无限长,至少要满足“飞行一定时长后休息N小时”的规则。这种约束通常转换成“如果机组j执行了航班i,则它不能在航班i落地后的N小时内执行另一个航班k”。用数学表达就是x(i,j)+x(k,j)<=1,当航班k的起飞时刻落在航班i落地时刻加休息期之后时,这个约束不触发;落在期内就要触发。实际编码时,可以先算出任意两个航班i和k之间的“最小间隔”,如果间隔小于休息时长,就把这两个航班设成冲突对,加入约束矩阵。
基地约束是指机组从哪个基地出发,最终要回到哪个基地。排班表必须闭合,不能让机组飞到外地过夜(除非题目允许)。处理方法是把“机组所属基地”变成数据的一列,对每个基地的机组集合,统计其执行航班序列的最终落地机场,要求最终落地机场等于基地。在Matlab里,这一部分一般不做成线性约束,而是先生成可行链,再在链的级别做组合优化。这也是为什么第三问难度上升的原因:一旦引入链式排班,0-1变量之间会出现大量组合关系。关键思路是“先过滤,后建模”——先把无法形成闭环的航班对从变量里删掉,再去跑整数规划。
3.3 考点三:目标函数与Matlab的intlinprog求解
目标函数通常有两种考法:最小化机组使用数,或最小化总成本/总飞行时长偏差。如果是最小化机组数,可以设y(j)=1表示机组j被启用,目标就是sum_j y(j)。y与x的关系是:若x(i,j)有任何一个是1,则y(j)必须为1,即y(j) >= x(i,j),同时x(i,j) <= y(j)。这组不等式是整数规划里的经典“激活变量”逻辑,评委看到这一步会直接加分。
Matlab里用intlinprog求解这类混合整数线性规划。假设已经构造好目标向量f、不等式约束Ax<=b、等式约束Aeqx=beq,以及变量上下界和整数变量下标,调用方式如下:
% 变量数量 n = 航班数*机组组数 + 机组组数 % 目标函数:先排x部分,再排y部分,y的系数为启用成本 f = [zeros(flights*groups,1); ones(groups,1)]; % 不等式约束:每个航班覆盖至少一个机组 % A(航班i, 对应x)= -1; b对应-1,用于 >= 转为 <= % 整数变量全部,lb=0, ub=1 [x_opt, fval, exitflag] = intlinprog(f, 1:n, A, b, Aeq, beq, lb, ub);这段代码里,intlinprog默认求解线性规划,第一个参数f是目标系数,第二个参数1:n表示所有变量都是整数变量,A和b是不等式约束,Aeq和beq是用等式表达严格覆盖或多乘务员配置。注意把“>=”约束转成“<=”时,要同时把两边取负号,否则A矩阵符号错会导致无解。exitflag是求解器返回状态:1表示成功,-2表示无解,0表示迭代次数超限。看到-2时不要慌,先检查是不是覆盖约束符号写反了,再检查数据里有没有时间冲突导致所有解都被禁掉。
4. 真题演练:三问的答题思路与Matlab实现
4.1 第1问:基础排班,怎么写出第一版可行解
第1问要求的往往是“在保证每个航班都被覆盖的前提下,用的机组组数最少”。最直接的答题思路是三步:第一,读取A-Flight.csv和A-Crew.csv,把航班按起飞时间排序;第二,定义x(i,j)为航班i由机组组j执行;第三,构造覆盖约束和目标函数。这里要展示给评委的不只是代码,而是“为什么覆盖约束用>=而不是=”。如果题目允许一名机组组在同一天执行多个航班,只要时间不重叠,那么一个机组组可以覆盖多个航班,此时每个航班只需要一组,所以用>=。如果不允许同一组多飞,那就要换成“机组组当日累计飞行时间上限”来组合。
第1问结果通常是一个整数矩阵,每一行是航班,每一列是机组组,1表示匹配。输出表建议包含:航班号、起飞时间、落地时间、机组组编号、该组当日飞行累计时长。评委拿到这张表,一眼能看出你是否真的理解了覆盖关系。代码里不要把所有变量建成稀疏矩阵然后用for循环填约束,那样效率低。用向量化方式:先构造航班与机组组的“时间冲突矩阵”,再把它展开成约束的稀疏矩阵。时间冲突矩阵是n×m的0-1矩阵,1表示该机组可以执行该航班,0表示时间冲突或资质不符。这个矩阵同时也是约束矩阵的主干。
% 构造时间冲突矩阵 available(i,j)=1 表示机组组j可执行航班i available = zeros(flights, groups); for i = 1:flights for j = 1:groups % 判断机组组j的资质与时间是否满足航班i if crew_ability(j, flight_type(i)) && ... dep_time(i) >= crew_available_start(j) && ... arr_time(i) <= crew_available_end(j) available(i,j) = 1; end end end % 将available矩阵作为x变量的可行域:不可行的x强制为0这段代码的目的不是直接求解,而是缩小变量范围。在论文里,你要写出“对于available(i,j)=0的位置,x(i,j)直接置0或从模型中删除”,这是把大规模问题降阶的第一个重要操作。很多队伍忽略了这一步,导致整数规划变量数多到求解器跑不动。
4.2 第2问:加约束后的修正建模
第2问一般会加入“机组连续休息时间不少于N小时”或“每个机组组每天最多飞M小时”。这类约束属于“成对互斥”约束,直接加在x变量上。对任意两个冲突的航班i和k(它们由同一机组执行时违反了休息时间或飞行时长限制),加入x(i,j)+x(k,j)<=1,对所有机组组j成立。如果机组数很多,这个约束的数量会爆炸,但大多数是冗余的。优化技巧:只对“时间间隔小于最小休息时长”的航班对生成约束,不要全连。
答题时用一段伪代码展示冲突航班对的生成逻辑,再给出Matlab里构造稀疏矩阵的方法。这里有个加分点:把“休息规则”设计成可配置参数,比如定义rest_min = 8(小时),飞行时长上限fly_max = 8,然后用这两个参数去筛选冲突航班对。这体现灵敏度分析的意识,评委很吃这一套。实际代码可以写:
% 假设flights表有dep_time和arr_time两个列(使用datenum格式) % crew_groups是机组组列表 % 冲突矩阵 conflict(i,k) = 1 表示航班i与k由同一组执行会违反休息规则 rest_min = 8; % 小时 for j = 1:groups % 取机组组j可执飞的航班列表 flight_list = find(available(:,j)==1); for a = 1:length(flight_list) for b = a+1:length(flight_list) i1 = flight_list(a); i2 = flight_list(b); gap = dep_time(i2) - arr_time(i1); % 单位是天 if gap < rest_min / 24 % 转成天 % 加入约束 x(i1,j)+x(i2,j) <= 1 end end end end这段代码里最容易被忽略的是时间单位换算。CSV里的时间通常是字符串,比如“2021-08-01 08:30”,Matlab读进来要用datenum转成数值,相减得到“天”。休息8小时就得写8/24,不是写8。很多队伍最后结果算不出,就是卡在单位换算上。另一方面,循环三层虽然直观,但数据量大时会很慢。比赛时可以用“先按机组组可飞航班列表排序,再用差分时间”加速。如果第2问数据量不大,直接三层循环也够用。
4.3 第3问:多目标或更大规模的降阶处理
第3问常见两种升级方向:一是目标函数变成“总成本 + 机组使用不平衡惩罚”的多目标;二是把A/B两组数据合起来或要求跨基地闭环。应对多目标的套路是线性加权,把两个目标归一化后相加。比如总成本C1,最大最小机组工作量差C2,目标写为min (w1C1 + w2C2),权重w1,w2需要做灵敏度分析,列出几组取值下的结果对比。这个表格在论文里非常提分。
如果是规模爆炸,不要硬跑整数规划。先用“分时段的贪心”或“机组组优先级排序”给出一个可行解作为初值,再在初值周围用启发式搜索,比如遗传算法的变异算子来改进。第3题代码里“详细注释在第2问中”暗示第3问很可能是直接调用第2问的模型,换一组数据或换一个目标函数。所以答题时要写清楚:第3问的模型是第2问模型的扩展,不是新模型。具体地,扩展点要么在等式约束上加了新的覆盖要求,要么在目标函数里加了惩罚项。评委希望看到你“复用既有模型并解释改动”,而不是推翻重来。
第3问的Matlab实现建议用函数封装:把“数据读入”“冲突矩阵构造”“目标函数计算”“约束矩阵生成”分别写成function,这样第2问函数第3问可以复用。代码组织得好,论文里贴核心段就够了,不用把几百行的主程序全贴上去。这也是资源里code文件夹分“第1、第2、第3”的原因,比赛代码要分层。拿到代码后,你只需要改参数和读入文件名,就能完成第三问的验证。
5. 易错点:这5处失分最多
5.1 易错一:时间格式错乱导致冲突计算失败
错在哪:直接用字符串去减,或者把“8小时”写成8。为什么错:Matlab里字符串不能相减,datenum得到的是天,8小时是8/24天。如果gap直接和8比较,相当于把8天和8小时混为一谈,所有约束都会失效。怎么答对:读CSV后用datenum统一转成数值,写一个time_to_hours函数,所有休息时长的比较用hours = (dep_time - arr_time)*24。在论文里明确写“所有时间统一为天,小时数通过乘以24换算”,这一句就是考点。
5.2 易错二:覆盖约束写成“=1”,没考虑一个机组组飞多班
错在哪:把“每个航班必须且只能被一个机组组执行”理解为sum_j x(i,j)=1,忽略了机组组可以跨航班执行。为什么错:当机组组j执行航班i又执行航班k时,x(i,j)=1, x(k,j)=1,每个航班单独看都是“被一个组执行”,但“每个航班恰好一个组”并不等于“一个组只飞一个航班”。所以用“sum_j x(i,j) >= 1”更合理。怎么答对:读题时看有没有“每名机组当日只能执行一个航班”,没有就写>=,并在论文里说明为什么可以大于1——因为同一个机组组在不同时间段执行不同航班是被允许的,只要时间不冲突。
5.3 易错三:机组组资质过滤遗漏
错在哪:直接把所有机组组放给所有航班,没有看A/B组数据里的舱位资质、执飞资格。为什么错:有些机组组只能飞某种机型或某个舱位配置,如果忽略这一点,求出的“最优解”在真实规则下不可行,评委用题目条件一验证就扣分。怎么答对:在构造变量x(i,j)前,先做“可执行矩阵”,用数据里的字段筛掉不匹配的组合,再开始建立约束。这一步放在数据预处理阶段,不要混在约束矩阵里。
5.4 易错四:intlinprog的参数写反或变量数不对
错在哪:把不等式约束的A和b构造反了,或者整数变量下标写成1:groups而不是1:n。为什么错:intlinprog要求所有整数变量必须明确列出,把>=转成<=时如果忘了乘-1,约束方向就反了。变量数量不匹配时,Matlab会直接报错“Subscript indices must either be real positive integers”。怎么答对:先在模型里列出所有变量并编号(哪些是x,哪些是y),再构造f、A、b。建议用变量名注释清楚,跑出结果前用size检查矩阵维度,比如size(A,2)必须等于n。
5.5 易错五:只报目标函数值,不交付排班表
错在哪:论文里只给了最优值,但没有给出哪个航班由哪组机组执行的明细。为什么错:评委没法验证你的解是否满足约束,也没法核对数据。很多队伍把目标值算出来就收工,结果评委看不到x矩阵的值。怎么答对:把x矩阵的1的位置提取出来,输出成“航班号、机组组号、起飞时间、落地时间”的表,放在附录。这也是第1问最容易拿分的动作,还能让评委相信你确实跑出了可行解。
6. 答题技巧与押题:把建模过程变成加分项
6.1 论文里必须出现的三张表和一页图
三张表:变量定义表、参数取值表、结果对比表。变量定义表让评委不用猜你的符号;参数取值表说明休息时长、权重系数这些数值是怎么定的;结果对比表列出不同权重或不同约束松紧下的目标值和机组使用数。一页图是指排班甘特图,横轴时间,纵轴机组组,用条形表示每次飞行任务。这张图能直观展示时间冲突是否被避开,是评委会看的第一个视觉信息。画图用Matlab的barh或gantt的自定义绘制,半小时搞定。
6.2 灵敏度分析这样做:调一个参数,记录三个指标
押题方向大概率是“调整休息时长N后,所需机组数如何变化”或“成本权重w变化后,排班均衡性如何变化”。答题时不要只放一个曲线,而是做一张参数-指标表:取N=6、8、10、12小时,记录机组数、总飞行时长方差、求解时间。结论写“随着N增大,所需机组数从X增加到Y,说明休息规则是约束瓶颈”之类。这种话术是拿奖的快捷键,它证明你不仅会跑代码,还理解模型行为。权重分析也类似,列三组w1/w2取值,看目标值变化,说明你做了权衡。
6.3 我的血泪习惯:每一版模型先跑小样例再上全量数据
我从那次比赛后,每次建模都强制先构造一个5个航班、3组机组的玩具样例,把全部约束和代码跑通,再换全量数据。因为全量数据一旦报错,你根本不知道是数据脏还是约束错。玩具样例可以手工验证,10分钟内定位问题。这个习惯救了我好几次,特别是第二问加冲突约束时,小样例能让你看清约束矩阵是不是漏了某对角块。另一个习惯是每次改完变量数,就重新打印一次size(A,2)核对,避免低级错误。希望帮到你,F题拿奖没有捷径,但把上面这些板子搭好,省奖起步是稳的。
本文还有配套的精品资源,点击获取