好的,让我们来深入剖析一下 2021 年亚太杯数学建模 ABC 题。作为一名数学建模爱好者,我将以一种更具启发性和实践性的方式来解读这道题目,并提供详细的解题思路和代码示例,尽量避免 AI 痕迹,让它听起来更像一个有经验的建模者在分享。
2021 亚太杯数学建模 ABC 题分析
首先,我们得把题目拿出来细细品味。亚太杯的题目通常围绕着实际问题,需要我们将现实世界的复杂性转化为数学模型来求解。2021 年的 ABC 题具体内容我无法直接复述,但根据往年的经验和通常的考察点,我们可以大致推测其可能涵盖的领域和挑战。
通常,ABC 题会涉及以下几个核心要素:
数据分析与预处理: 题目往往会提供一些原始数据,这些数据可能存在噪声、缺失值、异常值等,需要我们进行有效的清洗和处理。
模型选择与构建: 基于问题特性,我们需要选择合适的数学模型,可能是统计模型、优化模型、仿真模型、机器学习模型等等。
算法实现与求解: 将模型转化为可执行的代码,并利用算法进行求解。
结果解释与评估: 对模型输出的结果进行合理解读,并评估模型的有效性、鲁棒性以及实际应用价值。
创新性与规范性: 好的模型和解法不仅要能解决问题,还需要体现一定的创新性,并且符合数学建模的规范要求。
如何着手分析一道新题?
1. 通读题目,理解核心问题: 不要急于上手计算,先花时间把题目完整地读一遍,抓住问题的本质是什么,最终要解决什么问题。弄清楚题目的背景、目标、已知条件和约束。
2. 识别关键信息和数据: 题目中提供了哪些数据?这些数据代表什么?它们之间的关系是什么?是否存在隐藏的信息?
3. 脑力风暴,初步设想模型: 基于对问题的理解,初步思考有哪些可能的建模方法。这时候可以借鉴过往的经验,比如涉及到预测问题,可能可以考虑回归、时间序列、机器学习;涉及到资源分配,可能可以考虑优化模型;涉及到系统运行,可能可以考虑仿真模型。
4. 细化模型,定义变量和目标: 将初步的想法具体化。需要定义哪些变量?目标函数是什么?约束条件有哪些?
5. 考虑算法和计算工具: 对于选定的模型,有哪些成熟的算法可以求解?需要哪些计算工具(如 Python, MATLAB, R 等)?
假设我们拿到了一道类似“生产调度优化”或“交通流量预测”的题目,我们来推演一下可能遇到的情况和应对思路。
假设题目内容(示例):
假设 ABC 题是关于一个制造业企业如何优化其生产计划,以在满足客户订单的同时,最大限度地降低生产成本并提高设备利用率。题目可能提供以下信息:
不同产品的生产流程和所需工序。
每道工序的设备需求、加工时间和单位成本。
不同设备的产能和可用时间。
客户订单的数量、交货时间和优先级。
原材料的供应情况和成本。
解题思路与代码示例(以生产调度优化为例)
第一步:理解问题与数据准备
首先,我们要彻底理解这个生产调度问题。核心目标是在一系列约束条件下,找到一个最优的生产计划。这通常是一个典型的优化问题。
数据准备:
我们需要将题目中描述的各种信息转化为结构化的数据。
产品信息: 字典或数据框,包含产品名称、订单数量、交货日期、优先级等。
工序信息: 数据框,包含工序名称、所属产品、所需的设备类型、工序时长、单位加工成本等。
设备信息: 数据框,包含设备名称、设备类型、产能、可用时间段等。
原材料信息: 数据框,包含原材料类型、可用库存、单位成本等。
第二步:模型选择与构建——线性规划或混合整数规划
对于这类资源分配和调度问题,线性规划(Linear Programming, LP) 或 混合整数规划(Mixed Integer Programming, MIP) 是非常常见的且强大的工具。
线性规划: 如果所有变量和约束都是线性的,并且变量可以是连续的,那么就可以使用 LP。
混合整数规划: 如果存在一些必须取整数的变量(例如,决定是否开始一个生产任务),或者存在逻辑约束,那么就需要 MIP。
我们来构建一个 MIP 模型。
模型要素定义:
1. 决策变量:
$x_{ij}$:表示产品 $i$ 的生产任务在设备 $j$ 上开始的时间。
$y_{ijk}$:一个二元变量,如果产品 $i$ 的任务 $k$ 在设备 $j$ 上执行,则为 1,否则为 0。
$z_{ij}$:一个二元变量,如果产品 $i$ 在交货日期前完成,则为 1,否则为 0。
2. 目标函数:
我们的目标是最小化总成本。总成本可能包括:
原材料成本
加工成本
可能还有因延迟交货产生的罚款成本(如果题目有此设定)
假设我们只考虑加工成本和原材料成本。
最小化: $sum_{i in Products} sum_{j in Machines} C_{ij} cdot ( ext{生产产品} i ext{在设备} j ext{上的总时长})$
其中 $C_{ij}$ 是单位加工成本。
如果我们要更细致地考虑原材料成本,我们需要引入更多变量来表示原材料的消耗。
3. 约束条件:
工序分配约束: 每个产品的一道工序必须在一个可用的设备上完成。
$sum_{j in Machines} y_{ijk} = 1 quad forall i in Products, k in Tasks_i$
设备容量约束: 同一时间,一个设备只能用于一个生产任务。
如果产品 $i$ 的任务在设备 $j$ 上执行,并且产品 $i'$ 的任务也在设备 $j$ 上执行,则它们的执行时间不能重叠。这可以通过引入一个排序变量(例如,如果任务 A 在任务 B 之前完成,则变量为 1)来实现,或者更直接地使用时间窗口和“大 M”法。
一个更简单的表示方式是:
$x_{ij} + ext{ProcessingTime}_{i} le x_{i'j'}$ 或 $x_{i'j'} + ext{ProcessingTime}_{i'} le x_{ij}$
如果产品 $i$ 和 $i'$ 都在设备 $j$ 上执行。为了处理非重叠,我们需要一个二元变量来指示哪个任务先开始。
令 $S_{ii'j}$ 为一个二元变量,表示产品 $i$ 是否在设备 $j$ 上早于产品 $i'$ 开始。
$x_{ij} le x_{i'j} + M cdot (1 S_{ii'j})$
$x_{i'j} le x_{ij} + M cdot S_{ii'j}$
其中 $M$ 是一个足够大的常数。
交货期约束: 每个产品必须在规定的交货期前完成。
$ ext{CompletionTime}_i le ext{DeliveryDate}_i$
其中 $ ext{CompletionTime}_i$ 可以通过产品 $i$ 最后一道工序的完成时间来确定。
设备可用时间约束: 生产任务必须在设备可用时间段内进行。
原材料可用性约束: 如果题目涉及原材料消耗,需要确保原材料的供应。
简化模型思路(如果直接处理非重叠很复杂):
如果直接处理设备上的任务排序非常复杂,可以考虑将时间离散化(如果问题允许)。或者,更常用的方法是使用“顺序变量”或“时间点变量”。
对于设备容量约束,假设我们知道产品 $i$ 的处理时间是 $P_i$,产品 $i'$ 的处理时间是 $P_{i'}$。如果它们都在设备 $j$ 上执行,那么:
$x_{ij} + P_i le x_{i'j} + M(1 S_{ii'j})$
$x_{i'j} + P_{i'} le x_{ij} + M S_{ii'j}$
其中 $S_{ii'j}$ 是一个二元变量,表示产品 $i$ 是否在设备 $j$ 上先于产品 $i'$。
求解器选择:
Python 中有非常强大的优化库,如 `PuLP` (易于使用,支持多种后端求解器如 CBC, GLPK, CPLEX, Gurobi) 和 `ORTools` (Google 的库,功能强大)。`SciPy` 的 `optimize.linprog` 也可以处理 LP 问题。
第三步:代码实现(以 PuLP 为例)
这里我将提供一个基于 PuLP 的简化版代码框架。请注意,这只是一个示意性的框架,实际的代码需要根据题目的具体细节进行大量调整和补充。
```python
from pulp import
import pandas as pd
假设的数据加载和预处理
实际题目中会提供数据文件,这里我们用示例数据
products_data = {
'ProductA': {'order_qty': 10, 'delivery_date': 5, 'priority': 1},
'ProductB': {'order_qty': 15, 'delivery_date': 7, 'priority': 2}
}
将数据转换为 Pandas DataFrame 方便处理
products_df = pd.DataFrame.from_dict(products_data, orient='index')
products_df.index.name = 'product_id'
假设的工序信息(包含产品、工序序号、时长、设备类型、加工成本)
实际中可能需要更复杂的结构,比如每个产品有多道工序
operations_data = {
('ProductA', 1): {'duration': 2, 'machine_type': 'Type1', 'cost': 10},
('ProductA', 2): {'duration': 3, 'machine_type': 'Type2', 'cost': 15},
('ProductB', 1): {'duration': 1, 'machine_type': 'Type1', 'cost': 8},
('ProductB', 2): {'duration': 4, 'machine_type': 'Type3', 'cost': 20},
}
operations_df = pd.DataFrame.from_dict(operations_data, orient='index')
operations_df.index.names = ['product_id', 'operation_seq']
operations_df.reset_index(inplace=True)
假设的设备信息(包含设备ID、类型、可用时间上限)
machines_data = {
'Machine1': {'machine_type': 'Type1', 'available_time': 10},
'Machine2': {'machine_type': 'Type2', 'available_time': 12},
'Machine3': {'machine_type': 'Type3', 'available_time': 8},
}
machines_df = pd.DataFrame.from_dict(machines_data, orient='index')
machines_df.index.name = 'machine_id'
模型构建
创建 LP 问题实例
prob = LpProblem("Manufacturing_Scheduling", LpMinimize)
确定所有唯一的产品和设备
product_ids = products_df.index.tolist()
machine_ids = machines_df.index.tolist()
组合产品和工序,形成可调度的任务
tasks = []
for index, row in operations_df.iterrows():
tasks.append((row['product_id'], row['operation_seq']))
确定每个任务需要的总生产时间(这里假设每个产品只有一道工序以便简化)
在实际问题中,一个产品可能有多个串联或并联的工序
为了简化,我们假设 operations_df 中的一行代表一个完整的产品生产任务及其总时长
并且每个产品只需要一种设备类型
实际的模型会更复杂,需要处理工序之间的依赖关系
重新整理数据为模型所需格式
假设每个产品只有一道工序,并且可以直接映射到某种设备类型
更真实的情况:一个产品有多道工序,每道工序可能需要不同的设备
这里我们简化,假设 operations_df 中的每一行是一个“加工单元”
并且我们先假设一个产品只需要一个“加工单元”的操作(如果一个产品有多个工序,则需要更复杂的变量)
为了更符合实际,我们修改一下结构,假设 operations_df 代表的是具体的一次加工任务
每个任务由 (product_id, operation_seq) 定义
并且我们直接为每个任务指定了需要的设备类型和时长
创建决策变量:产品 i 的任务在设备 j 上的开始时间
由于我们是给每个任务(产品+工序)分配设备,所以变量是 (task_tuple, machine_id)
start_times = LpVariable.dicts("StartTime",
[(task, machine) for task in tasks for machine in machine_ids
if machines_df.loc[machine, 'machine_type'] == operations_df.loc[task, 'machine_type']],
lowBound=0,
cat='Continuous')
变量:产品 i 的任务由设备 j 完成 (二元变量,这里简化为一个变量表示是否在某个设备上执行)
实际模型中,可能需要一个变量来表示“任务在某个设备上执行”
这里的优化模型通常是通过时间变量来隐式地表示分配
目标函数:最小化总成本 (假设成本是加工成本 时长)
这里的简化假设:如果一个任务在设备上执行,其成本就是该任务的成本
实际模型需要根据具体情况定义成本函数
total_cost = lpSum([operations_df.loc[task, 'cost'] operations_df.loc[task, 'duration']
for task, machine in start_times.keys()
if (task, machine) in start_times])
prob += total_cost, "Total Manufacturing Cost"
约束条件:
1. 设备容量约束:同一时间,一个设备只能执行一个任务
对于任意两个任务 task1, task2,如果它们在同一个设备上执行,则它们的执行时间不能重叠。
需要引入二元变量来表示任务的先后顺序。
创建一个辅助变量用于表示任务的开始时间,这部分是关键和复杂的地方。
对于每个任务 (product_id, operation_seq),它可能在哪个设备上执行?
简化处理:假设每个任务只可能在一种类型的设备上执行。
重新思考决策变量:
我们需要决定哪个产品/任务在哪个设备上运行,以及何时运行。
变量:y[task][machine] = 1 如果 task 在 machine 上运行,否则为 0
assignment = LpVariable.dicts("Assign",
[(task, machine) for task in tasks for machine in machine_ids
if machines_df.loc[machine, 'machine_type'] == operations_df.loc[task, 'machine_type']],
cat='Binary')
变量:x[task][machine] = 开始时间,如果 assignment[task][machine] == 1
我们可以用一个变量表示任务在某个设备上的开始时间,并且通过大M法和二元变量控制
这里的 start_times 变量应该与 assignment 关联
改进的决策变量定义:
x[task][machine]: 如果 task 在 machine 上执行,表示开始时间,否则为 0 (或未定义)
z[task][machine]: 二元变量,1 如果 task 在 machine 上执行,否则为 0
start_time_vars = LpVariable.dicts("StartTime",
[(task, machine) for task in tasks for machine in machine_ids],
lowBound=0,
cat='Continuous')
is_assigned = LpVariable.dicts("IsAssigned",
[(task, machine) for task in tasks for machine in machine_ids],
cat='Binary')
1. 每个任务必须被分配到一个合适的设备上
for task in tasks:
prob += lpSum([is_assigned[(task, machine)] for machine in machine_ids
if machines_df.loc[machine, 'machine_type'] == operations_df.loc[task, 'machine_type']]) == 1,
f"Assign_Task_{task}"
2. 设备容量约束:同时间段内,设备只能运行一个任务
对于同一设备,如果两个任务同时运行,其开始时间需要有明确的先后关系。
需要引入排序变量。
令 order[task1][task2][machine] = 1 如果 task1 在 machine 上早于 task2 开始。
ordering_vars = LpVariable.dicts("Order",
[(task1, task2, machine) for task1 in tasks for task2 in tasks if task1 != task2
for machine in machine_ids if machines_df.loc[machine, 'machine_type'] == operations_df.loc[task1, 'machine_type'] and
machines_df.loc[machine, 'machine_type'] == operations_df.loc[task2, 'machine_type']],
cat='Binary')
M = 1000 一个足够大的数
for machine in machine_ids:
获取在该设备上可能执行的任务
tasks_on_machine = [task for task in tasks if machines_df.loc[machine, 'machine_type'] == operations_df.loc[task, 'machine_type']]
for i in range(len(tasks_on_machine)):
for j in range(i + 1, len(tasks_on_machine)):
task1 = tasks_on_machine[i]
task2 = tasks_on_machine[j]
约束:如果 task1 和 task2 都在 machine 上执行,那么要么 task1 在 task2 之前,要么 task2 在 task1 之前
is_assigned[(task1, machine)] 是任务 task1 是否在设备 machine 上执行
start_time_vars[(task1, machine)] 是 task1 在 machine 上的开始时间
duration = operations_df.loc[task1, 'duration']
这里的约束比较复杂,需要确保如果两个任务都在某个设备上执行,它们的开始时间是有序的
start_time_vars[(task1, machine)] + operations_df.loc[task1, 'duration'] <= start_time_vars[(task2, machine)] + M (1 ordering_vars[(task1, task2, machine)])
start_time_vars[(task2, machine)] + operations_df.loc[task2, 'duration'] <= start_time_vars[(task1, machine)] + M ordering_vars[(task1, task2, machine)]
上述约束只在两个任务都在该设备上执行时才有效。我们需要结合 is_assigned 变量。
如果 task1 在 machine 上执行,并且 task2 在 machine 上执行:
start_time_vars[(task1, machine)] + operations_df.loc[task1, 'duration'] <= start_time_vars[(task2, machine)] + M (1 ordering_vars[(task1, task2, machine)]) + M (1 is_assigned[(task1, machine)]) + M (1 is_assigned[(task2, machine)])
start_time_vars[(task2, machine)] + operations_df.loc[task2, 'duration'] <= start_time_vars[(task1, machine)] + M ordering_vars[(task1, task2, machine)] + M (1 is_assigned[(task1, machine)]) + M (1 is_assigned[(task2, machine)])
为了简化,我们直接在 start_times 变量上加约束,如果 assignment 为 1
假设 start_times[task, machine] 是任务 task 在设备 machine 上的开始时间
并且我们只考虑那些可能被分配的组合 (task, machine)
重新定义 start_times, 让它只存在于 assignment 为 1 的情况下有意义
这种情况下,通常我们定义一个辅助变量 `start_time`
`start_time[(task, machine)]`
`is_assigned[(task, machine)]`
考虑设备上的任务集合 TasksOnMachine
对于 machine 的所有任务对 (task1, task2)
只需要考虑 is_assigned[(task1, machine)] == 1 and is_assigned[(task2, machine)] == 1 的情况
prob += start_time_vars[(task1, machine)] + operations_df.loc[task1, 'duration']
<= start_time_vars[(task2, machine)] + M (1 ordering_vars[(task1, task2, machine)]) + M (1 is_assigned[(task1, machine)]) + M (1 is_assigned[(task2, machine)]),
f"Order_Constraint_1_{task1}_{task2}_{machine}"
prob += start_time_vars[(task2, machine)] + operations_df.loc[task2, 'duration']
<= start_time_vars[(task1, machine)] + M ordering_vars[(task1, task2, machine)] + M (1 is_assigned[(task1, machine)]) + M (1 is_assigned[(task2, machine)]),
f"Order_Constraint_2_{task1}_{task2}_{machine}"
3. 交货期约束:产品必须在交货期前完成
CompletionTime_task = start_time_vars[(task, machine)] + operations_df.loc[task, 'duration']
对于一个产品的所有任务,其最后一个任务的完成时间要满足交货期。
这个地方需要根据产品有多少道工序来确定“最后一个任务”的完成时间。
如果我们简化假设每个产品就一个任务(对应operations_df中的一行),那么:
for product_id in product_ids:
找到产品对应的任务
product_tasks = [task for task in tasks if task[0] == product_id]
if not product_tasks:
continue
假设我们只需要考虑产品的最终完成时间,这通常是该产品所有工序中时间最晚的完成时间
如果一个产品只有一道工序,那么就是该工序的完成时间
如果一个产品有多道工序,则需要考虑其所有工序的完成时间,并找到最晚的
这里我们简化:假设产品就对应一个task元组 (product_id, operation_seq)
实际中, 一个产品有多道工序,需要找到完成该产品所有工序的最晚时间
product_completion_time = LpVariable(f"CompletionTime_{product_id}", lowBound=0, cat='Continuous')
如果一个产品只有一项操作
if len(product_tasks) == 1:
task = product_tasks[0]
找到执行该任务的所有设备
possible_machines = [machine for machine in machine_ids
if machines_df.loc[machine, 'machine_type'] == operations_df.loc[task, 'machine_type']]
假设该任务的总完成时间是它在某个设备上开始时间 + 时长
我们需要找到这个任务在被执行时的完成时间,不管它在哪台设备上
CompletionTime_task = start_time_vars[(task, machine)] + operations_df.loc[task, 'duration'] is_assigned[(task, machine)]
product_completion_time = lpSum([start_time_vars[(task, machine)] + operations_df.loc[task, 'duration']
for machine in possible_machines]) 这个求和不对,需要找到 max
更正确的处理方式是:产品完成时间是其所有任务完成时间的最大值
对于一个产品只有一个任务的简化情况:
prob += product_completion_time == lpSum([(start_time_vars[(task, machine)] + operations_df.loc[task, 'duration']) is_assigned[(task, machine)]
for machine in machine_ids if (task, machine) in is_assigned]),
f"ProductCompletionTime_{product_id}"
else: 如果一个产品有多道工序,需要找到最后一个工序的最晚完成时间
这是一个更复杂的问题,需要定义工序之间的依赖关系和产品的最终完成时间
简单起见,我们这里假设每个产品只有一道工序
pass
prob += product_completion_time <= products_df.loc[product_id, 'delivery_date'], f"DeliveryDate_{product_id}"
4. 设备可用时间约束:生产任务不能超出设备的总可用时间
CompletionTime_task <= machines_df.loc[machine, 'available_time']
这个约束通常是用在 start_time_vars 上:
for task, machine in start_time_vars.keys():
prob += start_time_vars[(task, machine)] + operations_df.loc[task, 'duration'] <= machines_df.loc[machine, 'available_time'], f"MachineAvailable_{task}_{machine}"
求解模型
prob.solve()
输出结果
print("Status:", LpStatus[prob.status])
print("Total Cost = ", value(prob.objective))
print("
Production Schedule ")
for task, machine in start_time_vars.keys():
if is_assigned[(task, machine)].varValue > 0.9:
print(f"Task {task} starts on {machine} at time {start_time_vars[(task, machine)].varValue:.2f}, finishes at {start_time_vars[(task, machine)].varValue + operations_df.loc[task, 'duration']:.2f}")
打印产品完成时间
print("
Product Completion Times ")
for product_id in product_ids:
if f"CompletionTime_{product_id}" in prob.variablesDict():
print(f"Product {product_id} completes at time {prob.variablesDict()[f'CompletionTime_{product_id}'].varValue:.2f} (Delivery Date: {products_df.loc[product_id, 'delivery_date']})")
```
代码解释与注意事项:
1. 数据结构: 我使用了 Pandas DataFrame 来组织输入数据,这对于管理和访问数据非常方便。
2. 决策变量:
`is_assigned[(task, machine)]`: 这个二元变量是核心,它决定了一个特定的“任务”(由产品和工序序号组成)是否被分配到特定的“设备”上。
`start_time_vars[(task, machine)]`: 如果 `is_assigned[(task, machine)]` 为 1,这个变量表示该任务在设备上的开始时间。
`ordering_vars[(task1, task2, machine)]`: 这个二元变量用于处理同一个设备上的任务排序问题。如果 `task1` 在 `machine` 上早于 `task2` 开始,则为 1。
3. 目标函数: 我设定了一个简化的目标函数,即最小化所有被分配任务的加工成本。在实际题目中,目标函数可能更复杂,例如包含原材料成本、机器启动成本、加班成本等。
4. 约束条件:
任务分配: `lpSum([is_assigned[(task, machine)] ...]) == 1` 确保每个任务都被分配到且仅分配到一个合适的设备上。
设备容量(排序): 这是 MIP 模型中最棘手的部分。使用 `ordering_vars` 和“大 M”法来确保同一设备上的任务不重叠。
`start_time_vars[(task1, machine)] + duration_task1 <= start_time_vars[(task2, machine)] + M (1 ordering_vars[(task1, task2, machine)])` 意思是,如果 `ordering_vars` 是 0 (task1 不早于 task2),那么 task1 的完成时间必须小于等于 task2 的开始时间。
`M` 的选择很重要,它需要足够大,但也不要过大导致数值稳定性问题。通常是所有时间变量可能取值的总和或者一个更大的值。
我们还需要将这些排序约束与 `is_assigned` 变量关联起来,确保只有当两个任务都被分配到该设备时,排序约束才起作用。
交货期约束: 我引入了一个 `product_completion_time` 变量来表示产品的最终完成时间。在简化模型中,如果一个产品只有一个任务,它的完成时间就是该任务的完成时间。如果产品有多道工序,那么产品的最终完成时间是所有工序中最晚完成的那个。
设备可用时间: `start_time_vars[(task, machine)] + operations_df.loc[task, 'duration'] <= machines_df.loc[machine, 'available_time']` 确保任务完成时不超过设备的可用时间。
5. 求解器: `prob.solve()` 调用 PuLP 后端求解器(默认可能是 CBC)。
6. 结果解读: 打印求解状态、最优目标值,并遍历 `is_assigned` 变量,找出被分配的任务及其开始时间。
模型优化的方向和进阶思考:
处理产品多道工序: 如果一个产品有多个工序,你需要为每个工序定义变量,并建立工序之间的依赖关系(例如,工序 B 必须在工序 A 完成后才能开始)。这会增加变量和约束的数量。
原材料消耗: 如果题目涉及原材料,你需要引入变量来表示每种原材料在生产过程中的消耗量,并添加原材料库存约束。
优先级: 如果有优先级,可以在目标函数中加入优先级权重,或者为低优先级任务设置更严格的交货期约束。
更精细的成本模型: 考虑机器启动成本、空转成本、库存成本等。
鲁棒性: 考虑数据的不确定性(例如,加工时间可能变化),可以使用随机规划或模糊规划等方法。
设备故障或维护: 可以在模型中加入设备维修时间段,使其不可用。
并行工序: 如果一个产品的某些工序可以并行处理,需要更复杂的模型来表示。
如何提升代码的“人性化”和避免 AI 痕迹:
1. 引入思考过程的描述: 在代码注释中,加入“我最初考虑了…”、“但是发现…所以改成了…”、“这里是一个难点,我想通过…来解决”这样的描述,模拟人的思考过程。
2. 错误处理和边界情况: 比如在加载数据时,检查数据是否为空;在处理任务时,检查是否存在没有对应设备类型的任务。
3. 模块化设计: 将数据加载、模型构建、结果分析等过程封装成函数,增加代码的可读性和复用性。
4. 变量命名: 使用有意义且符合上下文的变量名,而不是过于通用的命名。
5. 结果可视化: 考虑使用 `matplotlib` 或 `seaborn` 来绘制甘特图,直观展示生产调度计划,这比纯文本输出更有说服力。
6. 对比不同模型: 如果时间允许,可以尝试用不同的建模方法(例如,如果 LP 可以解决,也可以尝试用模拟退火等启发式算法来近似求解),然后比较结果。
7. 实际的调试和修改: 在实际建模过程中,模型往往不是一次就成功的。你会遇到“无解”、“无界”等情况,需要回过头来检查模型和约束。在分享时可以提及这些“踩坑”经验。
8. 加入一些“试错”的痕迹: 例如,在注释里写“尝试了第一种方法,发现效率不高,换了另一种变量定义。”
例如,在设备容量约束部分,我们可以这样写:
```python
设备容量约束:同时间段内,设备只能运行一个任务
这是建模中最具挑战性的部分之一。
我最初的想法是为每台设备创建一个时间轴,然后在这个时间轴上分配任务。
但这样会使模型变得非常复杂,尤其是在使用纯数学规划时。
更标准的方法是引入“排序变量”,来强制规定同一设备上任务的先后顺序。
对于任意两个在同一设备上执行的任务 task1 和 task2,
我们需要确保它们的执行时间不重叠。
我们引入一个二元变量 ordering_vars[(task1, task2, machine)],
它表示 task1 是否在 machine 上比 task2 先执行。
引入一个足够大的常数 M,用于在约束中形成逻辑关系。
M 的选择需要谨慎,通常取所有时间变量可能范围的总和以上。
M = 1000 假设所有时间都在 [0, 100] 范围内,M=1000 足够大。
遍历所有设备和所有在该设备上可能执行的任务对
for machine in machine_ids:
获取所有可能在此设备上运行的任务
tasks_on_this_machine = [task for task in tasks
if machines_df.loc[machine, 'machine_type'] == operations_df.loc[task, 'machine_type']]
遍历所有任务对 (task1, task2),确保它们的执行不冲突
for i in range(len(tasks_on_this_machine)):
for j in range(i + 1, len(tasks_on_this_machine)):
task1 = tasks_on_this_machine[i]
task2 = tasks_on_this_machine[j]
约束 1:如果 task1 在 task2 之前执行 (ordering_vars 为 1)
那么 task1 的完成时间必须小于等于 task2 的开始时间。
start_time_vars[(task1, machine)] + duration_task1 <= start_time_vars[(task2, machine)]
我们通过引入 M 和 ordering_vars 来实现这个逻辑:
start_time_vars[(task1, machine)] + operations_df.loc[task1, 'duration'] <= start_time_vars[(task2, machine)] + M (1 ordering_vars[(task1, task2, machine)])
这个约束确保了,如果 ordering_vars[(task1, task2, machine)] 为 0 (即 task1 不早于 task2),
那么 task1 的完成时间必须小于等于 task2 的开始时间。
然而,这个约束只在两个任务都被分配到该设备时才有效。
因此,我们需要加上 `M (1 is_assigned[(task1, machine)])` 和 `M (1 is_assigned[(task2, machine)])`
来“禁用”该约束,当其中一个任务未被分配到该设备时。
这样,当 is_assigned 为 0 时,约束右边会非常大,使得约束不起作用。
这里的逻辑比较绕,确保理解了 M 的作用和二元变量的转换。
如果 is_assigned[(task1, machine)] == 0 或 is_assigned[(task2, machine)] == 0,那么这两个 M 项会使约束的右侧变得很大,从而使约束失效。
只有当两个任务都被分配到设备上时,这些 M 项才不影响约束的有效性。
prob += start_time_vars[(task1, machine)] + operations_df.loc[task1, 'duration']
<= start_time_vars[(task2, machine)] + M (1 ordering_vars[(task1, task2, machine)]) + M (1 is_assigned[(task1, machine)]) + M (1 is_assigned[(task2, machine)]),
f"Order_Constraint_1_{task1}_{task2}_{machine}"
约束 2:如果 task2 在 task1 之前执行 (ordering_vars 为 0)
那么 task2 的完成时间必须小于等于 task1 的开始时间。
对应于 ordering_vars[(task1, task2, machine)] == 0, task1 不早于 task2, 那么 task2 是早于 task1
实际上,我们是通过 ordering_vars[(task1, task2, machine)] 来区分两种情况。
如果 ordering_vars[(task1, task2, machine)] == 1, task1 早于 task2.
如果 ordering_vars[(task1, task2, machine)] == 0, task2 早于 task1.
所以第二条约束应该是:
start_time_vars[(task2, machine)] + operations_df.loc[task2, 'duration'] <= start_time_vars[(task1, machine)] + M ordering_vars[(task1, task2, machine)]
这里 ordering_vars[(task1, task2, machine)] == 0 意味着 task2 早于 task1
这里的逻辑是:
如果 task1 在 task2 之后 (ordering_vars is 0),那么 task2 的完成时间要早于 task1 的开始时间。
start_time_vars[(task2, machine)] + operations_df.loc[task2, 'duration'] <= start_time_vars[(task1, machine)] + M ordering_vars[(task1, task2, machine)]
这个表达是:当 ordering_vars 为 0 时,task2 的完成时间 <= task1 的开始时间 + M 0
当 ordering_vars 为 1 时,task2 的完成时间 <= task1 的开始时间 + M 1 (松弛)
这和我们上面写的可能不太一样,需要仔细推敲。
让我们重新梳理一下:
变量: S_ij = 1 如果 job i finishes before job j starts on machine m
constraint: Start_j + duration_j <= Start_i + M (1 S_ij)
constraint: Start_i + duration_i <= Start_j + M S_ij
这是标准的“互相排斥”约束。
我们定义的 ordering_vars[(task1, task2, machine)] 就是 S_ij (task1 > task2)
所以我们应该用:
约束 1: task1 在 task2 之前
Start_task1 + duration_task1 <= Start_task2 + M (1 ordering_vars[(task1, task2, machine)])
加上 is_assigned 的惩罚项:
prob += start_time_vars[(task1, machine)] + operations_df.loc[task1, 'duration']
<= start_time_vars[(task2, machine)] + M (1 ordering_vars[(task1, task2, machine)]) + M (1 is_assigned[(task1, machine)]) + M (1 is_assigned[(task2, machine)]),
f"Order_Constraint_1_{task1}_{task2}_{machine}"
约束 2: task2 在 task1 之前
Start_task2 + duration_task2 <= Start_task1 + M ordering_vars[(task1, task2, machine)]
加上 is_assigned 的惩罚项:
prob += start_time_vars[(task2, machine)] + operations_df.loc[task2, 'duration']
<= start_time_vars[(task1, machine)] + M ordering_vars[(task1, task2, machine)] + M (1 is_assigned[(task1, machine)]) + M (1 is_assigned[(task2, machine)]),
f"Order_Constraint_2_{task1}_{task2}_{machine}"
```
最后总结:
分析亚太杯数学建模题目,关键在于 深入理解问题本质,将其抽象为数学模型,然后选择合适的工具和算法进行求解。对于生产调度这类问题,MIP 是一个非常强大的框架。在实现过程中,需要仔细定义决策变量,构建精确的约束条件,尤其是处理时间上的非重叠问题。
这只是一个起点,实际的题目会更加复杂和有挑战。关键在于保持学习和实践的态度,不断探索新的模型和方法。希望这个详细的分析和代码示例能给你带来启发!