尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

基于MILP的运动会赛程优化:Python建模与公平性量化实践

基于MILP的运动会赛程优化:Python建模与公平性量化实践 1. 项目概述与核心问题拆解看到“运动会优化比赛模式探索”这个题目很多刚接触数学建模的同学可能会有点懵觉得这跟传统的物理、经济模型不太一样有点“文科”的感觉。其实恰恰相反这是一个非常典型的运筹优化类问题它考察的核心是如何用数学语言描述现实中的管理决策并寻找最优解。2021年数维杯C题以此为背景对于Python小白来说是一个绝佳的练手项目因为它不涉及过于高深的数学理论但对数据处理、模型构建和编程实现能力有全面的要求。简单来说题目给了一个学校运动会的场景有多种比赛项目如短跑、跳远等每个项目有不同数量的运动员报名需要在有限的场地、时间和裁判资源下安排赛程。核心目标不是简单地排个时间表而是要探索并优化比赛模式。什么叫“优化比赛模式”传统模式可能就是所有项目一轮一轮比完但这样可能耗时很长运动员等待时间久观众也觉得无聊。优化模式可能包括预赛、决赛的分组赛制多项目并行进行引入积分制或淘汰赛等。题目要求我们建立模型去量化评估不同模式的优劣并找到在给定约束下比如总时间不超过2天、裁判数量有限的最优安排方案。这里的关键词是“探索”。它意味着没有标准答案你需要自己定义什么是“好”的比赛模式。是总耗时最短还是运动员的体验最佳等待时间少或是比赛观赏性最强决赛更集中这需要你建立一个包含多个目标的评价体系。对于小白我建议先从总耗时最短和赛程紧凑度这两个最直观、最容易量化的目标入手。先解决“有没有”的问题再考虑“好不好”的问题。2. 核心思路与模型框架设计面对这样一个开放性问题建立一个清晰、可计算的模型框架是成功的第一步。我们不能一头扎进代码里必须先想清楚数学逻辑。2.1 问题抽象与关键定义首先我们把运动会抽象成一个资源受限的项目调度问题。任务Jobs 每一个小项比赛如“男子100米预赛第一组”就是一个任务。注意一个“项目”如男子100米可能根据参赛人数被拆分成多个预赛组和一个决赛任务。资源Resources场地资源 跑道、沙坑等。同一时间一个场地只能进行一项比赛。裁判资源 各项目所需的裁判组。一名裁判不能同时执裁两场比赛。时间资源 总比赛时长如每天8小时共16小时。约束Constraints顺序约束 对于同一项目预赛必须在决赛之前。运动员恢复约束 同一运动员参加两个不同项目中间需要有足够的休息时间如至少30分钟。资源冲突约束 共享同一资源如跑道的比赛不能同时进行。接下来要定义我们追求的“优化”目标。这里我建议构建一个多目标优化模型但为了简化初期的求解难度可以采用加权求和法或主要目标法将其转化为单目标。目标一总赛程时长T_total最小化。这是最核心的效率指标。目标二比赛紧凑度最大化。我们希望比赛安排得满满当当不要有太长的空档期。可以用“所有比赛结束时间的方差”或“时间利用率总比赛时间/总赛程时长”来衡量。目标三公平性指数。这是题目“探索”的深水区。如何定义公平可以从几个维度机会公平 所有运动员的等待时间相对均衡避免有人等很久有人连轴转。竞争公平 决赛的入围选拔机制是否合理如按成绩取前八还是按小组排名。资源公平 各项目分配的优质比赛时段如黄金观赛时间是否均衡。我们可以定义一个公平指数F例如F α * (1 / 运动员平均等待时间标准差) β * 决赛选拔机制评分 γ * 时段分配均衡度。其中α, β, γ是权重需要根据实际情况设定。对于初次建模可以重点考虑第一个维度。2.2 模型选择为什么是混合整数线性规划MILP这是一个典型的带时间窗和资源约束的调度问题。常用的模型有图论模型如关键路径法适合顺序固定的项目但处理资源冲突能力弱。启发式算法如遗传算法、模拟退火适合大规模、复杂约束但解的质量不稳定且需要大量调参。整数规划/混合整数线性规划MILP 能够精确描述各种逻辑约束如“如果…那么…”并通过求解器得到最优解或高质量可行解。对于本题这种规模项目数通常在20-50个MILP是非常合适的选择。MILP的核心是引入0-1决策变量。例如定义决策变量x_{i,t} 1表示比赛任务i在时间片t开始0则表示不是。 这样我们就可以用线性不等式来表达所有约束每个任务只能开始一次∑_t x_{i,t} 1(对所有任务i)。资源容量约束 对于任意时间t和任意资源k如跑道1∑_{i 使用资源k} x_{i,t} ≤ 1。即同一时间使用该资源的任务最多一个。顺序约束 如果任务j必须在任务i之后那么任务j的开始时间必须大于等于任务i的结束时间。这需要引入另一个表示时间的连续变量s_i任务i的开始时间并建立约束s_j ≥ s_i d_id_i是任务i的持续时间。注意 直接使用x_{i,t}和s_i变量会形成大规模的模型。更常见的技巧是使用时间索引法或顺序变量法。对于小白我推荐先使用pulp或ortools这样的Python库它们提供了高级的建模接口可以让你更直观地定义间隔约束而无需手动处理庞大的0-1变量矩阵。2.3 数据准备与预处理题目通常会提供报名表包含运动员ID、姓名、参赛项目列表。我们需要从中提取出关键信息项目列表及每项参赛人数 决定每个项目需要分成多少预赛组。例如规则规定“100米跑超过8人需设预赛每组前2名进决赛”。假设有20人报名则需要ceil(20/8)3个预赛组产生3*26人进入决赛。比赛耗时估算 这是模型的重要输入。你需要为每类比赛估计一个持续时间。例如短跑100米200米 每组比赛时间含准备、起跑、测量约5分钟。中长跑800米1500米 每组约10-15分钟。田赛跳远、铅球 每人次试跳/试投约2分钟一组8人约需30-40分钟。实操心得 务必为每场比赛预留转换时间比如跑道项目上一组清场、下一组运动员上场准备至少需要3-5分钟。这个时间必须计入任务持续时间d_i否则模型排出的赛程在实际中根本无法执行。资源需求矩阵 建立一个表格明确每个比赛任务需要哪些资源如跑道1计时裁判组3人测量裁判组2人。3. 模型构建与Python实现详解理论清晰后我们进入实战环节。这里我选择使用python-mip一个兼容pulp但更快的MILP库结合ortools的 CP-SAT 求解器来演示因为它对整数规划的支持很好。3.1 环境搭建与数据加载首先确保你的Python环境安装了必要的库。pip install mip ortools pandas numpy假设我们有一个data.xlsx文件里面有两个工作表athletes运动员信息和events项目信息。import pandas as pd import numpy as np from mip import Model, xsum, minimize, BINARY, CONTINUOUS # 1. 加载数据 df_athletes pd.read_excel(data.xlsx, sheet_nameathletes) # 列AthleteID, Name, [Event1, Event2, ...] df_events pd.read_excel(data.xlsx, sheet_nameevents) # 列EventID, EventName, Type(Track/Field), BaseDuration(min), ResourceNeeds # 2. 数据预处理生成所有比赛任务 tasks [] task_id 0 # 假设我们有一个函数根据规则将项目拆分为预赛组和决赛 def create_heats(event_id, num_athletes, rule): # rule 示例 {prelim_per_heat: 8, qualify_per_heat: 2} num_heats (num_athletes rule[prelim_per_heat] - 1) // rule[prelim_per_heat] heats [] for h in range(num_heats): heats.append({type: prelim, heat_num: h1}) # 添加一个决赛任务 heats.append({type: final, heat_num: 0}) return heats for _, event in df_events.iterrows(): eid event[EventID] participants df_athletes[df_athletes[Events].str.contains(event[EventName])].shape[0] rule {prelim_per_heat: 8, qualify_per_heat: 2} # 示例规则 if participants rule[prelim_per_heat]: sub_tasks create_heats(eid, participants, rule) else: sub_tasks [{type: final, heat_num: 0}] # 直接决赛 for st in sub_tasks: task_duration event[BaseDuration] if st[type] prelim: task_duration 2 # 预赛转换时间稍短 else: task_duration 5 # 决赛准备时间更长 tasks.append({ TaskID: task_id, EventID: eid, EventName: event[EventName], TaskType: st[type], # prelim or final HeatNum: st[heat_num], Duration: task_duration, Resource: event[ResourceNeeds] # 可能是资源ID列表 }) task_id 1 num_tasks len(tasks) print(f共生成 {num_tasks} 个比赛任务。)3.2 构建MILP模型我们采用时间离散化的方法将一天的时间划分为以15分钟为单位的时段time slots。假设总赛程最多为T96个时段24小时 * 4。from mip import Model, xsum, minimize, BINARY T 96 # 总时段数 M 10000 # 一个很大的数用于线性化逻辑约束 model Model(SportsSchedule) # 决策变量 # x[i][t]: 任务i在时段t开始为0-1变量 x [[model.add_var(var_typeBINARY) for t in range(T)] for i in range(num_tasks)] # s[i]: 任务i的实际开始时间连续变量以时段为单位可转化为分钟 s [model.add_var(var_typeCONTINUOUS, lb0) for i in range(num_tasks)] # C_max: 整个赛程的结束时间即最大完成时间 C_max model.add_var(var_typeCONTINUOUS, lb0) # 目标函数最小化总赛程结束时间 model.objective minimize(C_max) # 约束1每个任务必须在且仅在一个时段开始 for i in range(num_tasks): model xsum(x[i][t] for t in range(T)) 1 # 约束2将0-1变量x与连续开始时间s关联起来 # s[i] sum_{t} (t * x[i][t]) for i in range(num_tasks): model s[i] xsum(t * x[i][t] for t in range(T)) # 约束3定义C_max为所有任务的完成时间最大值 # C_max s[i] duration[i] for all i duration [task[Duration] for task in tasks] # 提前将分钟转换为时段数这里假设duration已是时段数 for i in range(num_tasks): model C_max s[i] duration[i] # 约束4资源冲突约束以跑道为例 # 假设我们有一个资源字典resource_dict[resource_id] [使用该资源的任务ID列表] # 对于每个资源每个时段使用它的任务开始变量之和 1 track_tasks [i for i, t in enumerate(tasks) if Track in t[Resource]] for t in range(T): model xsum(x[i][t] for i in track_tasks) 1 # 同一时间只能有一个跑道任务开始 # 约束5顺序约束同一项目的预赛必须在决赛之前 # 我们需要先建立任务间的先后关系列表 precedence_pairs [(prelim_task_id, final_task_id), ...] precedence_pairs [] for i in range(num_tasks): for j in range(num_tasks): if i ! j and tasks[i][EventID] tasks[j][EventID]: if tasks[i][TaskType] prelim and tasks[j][TaskType] final: precedence_pairs.append((i, j)) for i, j in precedence_pairs: model s[j] s[i] duration[i] # 决赛j的开始时间 预赛i的结束时间 # 约束6避免时间窗冲突例如所有比赛必须在两天内完成 # 这已经由T和C_max隐含约束了。可以添加显式约束 # model C_max T print(模型构建完成变量数, model.num_cols, 约束数, model.num_rows)3.3 求解与结果提取# 设置求解器使用CBC开源且通常够用 model.optimize() if model.num_solutions: print(f最优总赛程时长: {C_max.x()} 个时段 (约 {C_max.x() * 15 / 60:.1f} 小时)) schedule [] for i in range(num_tasks): for t in range(T): if x[i][t].x 0.99: # 判断变量是否为1 start_time t * 15 # 转换为分钟 end_time start_time tasks[i][Duration] * 15 schedule.append({ TaskID: i, Event: tasks[i][EventName], Type: tasks[i][TaskType], Heat: tasks[i][HeatNum], StartTime(min): start_time, EndTime(min): end_time, StartTime: f{start_time//60:02d}:{start_time%60:02d}, EndTime: f{end_time//60:02d}:{end_time%60:02d}, }) break # 按开始时间排序 schedule_df pd.DataFrame(schedule).sort_values(byStartTime(min)) print(schedule_df[[Event, Type, Heat, StartTime, EndTime]].to_string(indexFalse)) # 可以将schedule_df保存为CSV或Excel文件 schedule_df.to_csv(optimized_schedule.csv, indexFalse) else: print(未找到可行解。可能需要放松某些约束或检查数据。)重要提示 上述模型是一个高度简化的示例。在实际中T96意味着有96 * 任务数个0-1变量对于几十个任务变量数可能达到几千求解时间会很长。更实用的方法是使用ortools的 CP-SAT 求解器它对于调度问题有更高效的区间变量IntervalVar和顺序约束NoOverlap原语能极大简化建模并提升求解速度。由于篇幅限制这里不展开但强烈建议你在掌握基本MILP思想后转向学习ortools.sat.python.cp_model。4. 模型评估与“公平指数”的量化实现得到一个赛程表后我们需要评估它。总时长C_max是一个指标但“公平性”呢现在我们来具体实现前面提到的公平指数。4.1 计算运动员等待时间与均衡度我们需要根据赛程表和运动员的报名表计算每个运动员从到达赛场到比完所有项目的时间跨度以及其中的空闲等待时间。def calculate_athlete_schedule(schedule_df, df_athletes): 计算每个运动员的赛程时间线。 schedule_df: 包含TaskID, StartTime(min), EndTime(min), Event df_athletes: 包含AthleteID, Name, Events (字符串如100米,跳远) # 首先建立一个映射项目名 - [该项目的所有TaskID] event_to_task_ids {} for _, row in schedule_df.iterrows(): event_to_task_ids.setdefault(row[Event], []).append(row[TaskID]) athlete_timelines [] for _, athlete in df_athletes.iterrows(): aid athlete[AthleteID] events [e.strip() for e in athlete[Events].split(,)] # 找出该运动员参加的所有任务 my_task_ids [] for ev in events: my_task_ids.extend(event_to_task_ids.get(ev, [])) # 获取这些任务的开始和结束时间 my_tasks schedule_df[schedule_df[TaskID].isin(my_task_ids)] if my_tasks.empty: continue my_tasks my_tasks.sort_values(StartTime(min)) arrival my_tasks.iloc[0][StartTime(min)] - 30 # 假设提前30分钟到场 departure my_tasks.iloc[-1][EndTime(min)] total_span departure - arrival # 计算总比赛时间和等待/空闲时间 total_competition_time (my_tasks[EndTime(min)] - my_tasks[StartTime(min)]).sum() total_waiting_time total_span - total_competition_time # 计算“忙碌”程度相邻比赛间隔 intervals [] for i in range(len(my_tasks)-1): gap my_tasks.iloc[i1][StartTime(min)] - my_tasks.iloc[i][EndTime(min)] intervals.append(gap) avg_interval np.mean(intervals) if intervals else 0 athlete_timelines.append({ AthleteID: aid, Name: athlete[Name], TotalSpan(min): total_span, TotalWait(min): total_waiting_time, AvgInterval(min): avg_interval, TaskCount: len(my_tasks) }) return pd.DataFrame(athlete_timelines) athlete_stats calculate_athlete_schedule(schedule_df, df_athletes) # 计算公平性指标等待时间的标准差越小越公平 wait_std athlete_stats[TotalWait(min)].std() span_std athlete_stats[TotalSpan(min)].std() print(f运动员等待时间标准差: {wait_std:.1f} 分钟 (越小越公平)) print(f运动员总耗时标准差: {span_std:.1f} 分钟 (越小越公平)) # 计算整体时间利用率 total_competition_time schedule_df[Duration].sum() # 假设schedule_df有Duration列 total_schedule_time C_max.x() * 15 # 总赛程分钟数 utilization total_competition_time / total_schedule_time if total_schedule_time 0 else 0 print(f时间利用率: {utilization:.2%})4.2 构建综合公平指数现在我们可以将多个指标合成一个综合的公平指数F。为了使其与总时长目标C_max一起优化我们可以构建一个加权单目标或者采用分层优化先优化C_max再在C_max接近最优的范围内优化F。加权单目标示例# 假设我们已经有了C_max, wait_std, utilization # 归一化处理这里需要预设一个最大值或使用当前解中的值进行缩放 # 假设我们通过几次试探性求解得到C_max大概在500-800分钟wait_std在50-150分钟utilization在0.4-0.7 C_max_norm C_max.x() / 800 # 假设800是参考上界 wait_std_norm wait_std / 150 # 假设150是参考上界 # 对于利用率我们希望它越大越好所以用 1 - utilization 来转化为成本 utilization_cost 1 - utilization # 定义权重 alpha 0.5 # 总时长权重 beta 0.3 # 公平性等待时间均衡权重 gamma 0.2 # 紧凑度权重 composite_objective alpha * C_max_norm beta * wait_std_norm gamma * utilization_cost print(f综合目标函数值: {composite_objective:.3f})然后你可以修改之前的模型将目标函数从minimize(C_max)改为minimize(alpha*C_max_norm beta*WaitStd gamma*UtilizationCost)。但这需要将wait_std和utilization也表示为模型中的变量和约束这非常复杂通常更适用于启发式算法。更可行的方案——两阶段法第一阶段以最小化总时长C_max为目标求解得到一个基准解C_max_opt。第二阶段在模型中增加一个约束C_max C_max_opt * (1 tolerance)比如允许总时长增加10%tolerance0.1。然后将目标函数改为最小化wait_std等待时间标准差。这样我们就在不显著牺牲效率的前提下寻找最公平的赛程。5. 方案对比、可视化与报告撰写5.1 设计不同比赛模式进行对比“探索”意味着比较。你可以设计2-3种典型的比赛模式用同一模型求解并对比。模式A传统串行 严格按项目顺序一个比完再下一个。这可以通过在模型中添加极强的“顺序约束”实现或者直接手动排程。模式B完全并行 允许所有场地同时比赛只受资源约束。这就是我们上面构建的基本模型。模式C分时段集中 例如上午集中进行所有田赛的预赛下午进行径赛预赛晚上进行决赛。这可以通过给任务添加“偏好时间窗”约束来实现。分别运行模型得到各自的赛程表、总时长、公平指数等指标放入一个对比表格。评价指标模式A传统串行模式B完全并行模式C分时段集中总赛程时长小时20.514.216.8时间利用率85%78%88%平均运动员等待时间分钟1859565等待时间标准差分钟1207540公平指数F自定义0.650.820.93优点组织简单不易出错效率最高兼顾效率与公平体验佳缺点耗时过长体验差资源冲突多安排复杂对调度要求高5.2 结果可视化一图胜千言。使用matplotlib绘制甘特图是展示赛程最直观的方式。import matplotlib.pyplot as plt import matplotlib.patches as mpatches plt.figure(figsize(15, 8)) colors {prelim: lightblue, final: salmon} y_pos 0 yticks [] yticklabels [] for _, task in schedule_df.iterrows(): start task[StartTime(min)] / 60 # 转换为小时 duration (task[EndTime(min)] - task[StartTime(min)]) / 60 label f{task[Event]}({task[Type]}{task[Heat]}) plt.barh(y_pos, duration, leftstart, height0.6, colorcolors.get(task[Type], gray), edgecolorblack) plt.text(start duration/2, y_pos, label, hacenter, vacenter, fontsize8) yticks.append(y_pos) yticklabels.append(label) y_pos 1 plt.xlabel(Time (Hours from Start)) plt.ylabel(Tasks) plt.yticks(yticks, yticklabels, fontsize8) plt.title(Optimized Competition Schedule (Gantt Chart)) # 添加图例 legend_handles [mpatches.Patch(colorcolor, labeltype_) for type_, color in colors.items()] plt.legend(handleslegend_handles) plt.grid(axisx, linestyle--, alpha0.7) plt.tight_layout() plt.savefig(schedule_gantt.png, dpi300) plt.show()还可以绘制运动员等待时间的分布直方图直观展示公平性。plt.figure(figsize(10, 6)) plt.hist(athlete_stats[TotalWait(min)], bins20, edgecolorblack, alpha0.7) plt.axvline(athlete_stats[TotalWait(min)].mean(), colorred, linestyle--, labelfMean: {athlete_stats[TotalWait(min)].mean():.1f} min) plt.xlabel(Total Waiting Time (minutes)) plt.ylabel(Number of Athletes) plt.title(Distribution of Athletes\ Waiting Time) plt.legend() plt.grid(axisy, alpha0.3) plt.tight_layout() plt.savefig(waiting_time_dist.png, dpi300) plt.show()5.3 模型灵敏度分析与稳健性讨论一个好的模型不仅要能给出答案还要经得起推敲。在论文或报告中你需要讨论参数敏感性 如果某个项目参赛人数突然增加20%赛程会延长多少你的模型能否快速调整可以通过修改输入数据重新求解来验证。规则变化的影响 如果预赛晋级规则从“每组前2名”改为“最好成绩前8名”你的任务拆分逻辑和模型需要做何调整这会影响公平性吗答会影响因为按成绩排名更公平但需要所有预赛结束后才能确定决赛名单增加了赛程的不确定性。资源瓶颈分析 哪个资源如跑道、跳远场地是最紧张的“瓶颈资源”可以通过查看模型中该资源约束的“影子价格”或“松弛变量”来分析或者简单统计该资源的使用率。模型优缺点优点 将复杂的调度问题数学化能得到精确解或高质量解可以考虑多种约束和目标便于进行“如果-那么”式的政策模拟。缺点 对于超大规模问题如全校所有年级同时比赛变量数爆炸求解困难公平性等软指标难以完美量化模型假设如固定的比赛时长可能与现实有出入。5.4 给Python小白的最后几点实操心得从简单开始逐步复杂化 不要试图一口气构建包含所有约束和目标的完美模型。先建立一个仅包含“每个任务必须安排”和“资源不冲突”的核心模型让它能跑通并输出一个可行的赛程。然后再一步步加入顺序约束、运动员休息约束、公平性目标等。善用现成的库和求解器PuLP,python-mip,ortools是你的好朋友。尤其是ortools的 CP-SAT对于这类调度问题建模更直观求解效率也更高。不要自己从头写搜索算法。数据预处理决定上限 模型再精巧如果输入数据比赛时长、转换时间估计不准结果也是空中楼阁。花时间仔细研究真实的比赛流程甚至去操场掐表计时这些基础工作比调参更有价值。可视化是检验和说服的利器 一个密密麻麻的表格远不如一张清晰的甘特图有说服力。用图表把你的优化成果展示出来无论是写在论文里还是向别人汇报效果都会好很多。理解“优化”的局限性 数学模型求出的“最优解”是在你定义的数学世界里的最优。现实中还需要考虑许多模型无法涵盖的因素比如裁判的疲劳度、天气变化、突发事件等。因此最优解通常作为一个“基准方案”实际执行时需要保留一定的灵活性和缓冲时间。这个项目将Python编程、数学建模和现实问题解决紧密结合。通过它你不仅能学会如何用代码解决一个具体问题更能掌握“将模糊的现实需求转化为清晰的数学模型”这一核心思维能力。这才是数学建模竞赛乃至未来工作中最宝贵的技能。
返回列表