1. 问题引入:当数学建模遇上芯片后端物理设计
去年带队参加华为杯数模竞赛,选的就是这道D题——PISA架构芯片的资源排布问题。说实话,刚拿到赛题时,我们团队几个搞算法和软件的同学都有点懵。题目背景是芯片设计,尤其是后端物理设计中的资源排布,这听起来更像是微电子专业同学的“主场”。但竞赛的魅力就在于此,它要求你快速跨界,将一个陌生的工程问题抽象成可计算的数学模型。我们花了整整两天时间,才把题目里那些“基本块”、“流水线”、“资源冲突”这些芯片设计术语,和我们熟悉的图论、整数规划、启发式算法对上号。这道题本质上是一个带复杂约束的二维装箱与调度混合问题,只不过“箱子”变成了芯片上的计算单元,“时间”变成了指令执行的时钟周期。
最终我们的论文拿到了不错的奖项,更重要的是,通过这次竞赛,我系统性地梳理了芯片后端物理设计中的一个核心环节——资源排布与调度优化的完整建模与求解思路。这不是一篇简单的解题报告,而是我想从一个算法工程师的视角,复盘我们如何将晦涩的芯片设计问题,拆解成一步步可执行、可优化的数学步骤。无论你是参加数模竞赛的学生,还是对芯片设计自动化(EDA)算法感兴趣的工程师,希望这篇结合了实战代码与深度思考的总结,能给你带来一些不一样的启发。
2. PISA架构与资源排布问题本质剖析
在深入解题之前,我们必须先理解PISA架构和题目到底在问什么。PISA(Precise, In-Order, Single-Issue Architecture)是一种精简的指令集架构模型,常用于计算机体系结构的教育和研究中。它假设指令按顺序执行、单发射(每个时钟周期最多完成一条指令),并且具有精确的异常处理机制。在这个模型下,芯片上的硬件资源被抽象为几种类型的功能单元,例如整数运算单元(ALU)、浮点运算单元(FPU)、加载存储单元(LSU)和分支预测单元(BPU)等。
竞赛题目中的“资源排布问题”,可以理解为两个层面的耦合优化:
第一层是空间上的布局(Placement):将许多个“基本块”(Basic Block,即一段顺序执行、只有一个入口和一个出口的代码块)映射到芯片二维网格的特定位置上。每个基本块在执行时需要占用特定类型和数量的硬件资源。芯片网格上的每个“节点”(或称为“站点”)只能放置一个基本块。这就好比在一个有限的棋盘上放置不同形状和功能的积木,积木不能重叠。
第二层是时间上的调度(Scheduling):由于指令流水线的存在,不同基本块在不同周期执行。一个基本块在执行时,不仅占用其所在位置节点的资源,还可能由于其功能单元的“辐射”范围,影响到相邻节点的资源可用性。同时,数据在基本块之间流动(模拟数据依赖),会产生通信延迟。调度决定了每个基本块在哪个时钟周期开始执行,目标是在满足所有资源约束和数据依赖的前提下,最小化整个程序的总执行时间(即流水线的吞吐周期)。
因此,这个问题的核心是一个“布局-调度”联合优化问题。布局影响资源竞争的局部密度和通信延迟,调度又反过来受限于布局决定的资源可用性和通信成本。两者相互制约,使得问题异常复杂,属于NP-Hard难题。我们的工作,就是为这个难题寻找一个高效的近似最优解。
3. 从问题描述到数学模型的关键抽象
面对一个工程问题,建立准确的数学模型是求解的第一步,也是最关键的一步。我们的建模过程经历了多次迭代。
3.1 决策变量定义
首先,我们定义核心决策变量:
- 布局变量 (x_{b, i, j}):二进制变量。如果基本块 (b) 被放置在第 (i) 行、第 (j) 列的网格节点上,则为1,否则为0。这里隐含约束:每个基本块必须且只能放置于一个节点,每个节点最多容纳一个基本块。
- 调度变量 (s_b):整数变量。表示基本块 (b) 开始执行的时钟周期(或称为“启动时间”)。
- 辅助变量——资源占用标识 (u_{t, i, j, c}):二进制变量。表示在时钟周期 (c),位于位置 ((i, j)) 的节点,其类型为 (t) 的资源是否被占用。这个变量由布局变量 (x) 和调度变量 (s) 共同决定。
3.2 约束条件形式化
接下来,我们将题目中文字描述的约束,转化为严格的数学不等式或等式。
1. 布局唯一性约束:[ \sum_{i, j} x_{b, i, j} = 1, \quad \forall b ] [ \sum_{b} x_{b, i, j} \leq 1, \quad \forall i, j ] 这两个约束确保了基本块与网格位置的一一对应关系。
2. 资源冲突约束(这是难点和重点):题目指出,一个功能单元被激活时,会占用其所在节点及相邻节点的同类资源。我们将其建模为“资源占用模版”。假设一个基本块 (b) 需要1个ALU类型资源,且该资源的影响范围是曼哈顿距离不超过 (R) 的区域(例如 (R=1) 表示上下左右相邻节点)。 那么,对于任意周期 (c),如果基本块 (b) 正在执行(即 (c \in [s_b, s_b + duration_b - 1]),其中 (duration_b) 是基本块执行时长),则对于所有满足曼哈顿距离 (dist((i,j), (i_b,j_b)) \leq R) 的位置 ((i, j)),其ALU资源在周期 (c) 都被标记为占用。 用数学语言描述,对于资源类型 (t): [ u_{t, i, j, c} \geq x_{b, i_b, j_b} \cdot I(c \in [s_b, s_b+duration_b-1]) \cdot I(b \text{ needs resource } t) \cdot I(dist((i,j), (i_b,j_b)) \leq R_t) ] 其中 (I(\cdot)) 是指示函数。此外,还需加上资源容量约束:在同一个周期 (c),同一个位置 ((i, j)),同一种资源类型 (t) 最多只能被一个基本块占用。这可以表示为: [ \sum_{b} [x_{b, i_b, j_b} \cdot I(...) \cdot I(...)] \leq 1, \quad \forall t, i, j, c ] 在实际建模中,我们需要将指示函数和距离约束线性化,引入大量辅助变量和约束,这是模型复杂度的主要来源。
3. 数据依赖约束:如果基本块 (b_1) 到 (b_2) 有数据依赖(即 (b_2) 需要 (b_1) 的计算结果),那么 (b_2) 的开始时间必须晚于 (b_1) 的完成时间加上通信延迟。 [ s_{b_2} \geq s_{b_1} + duration_{b_1} + delay(b_1, b_2) ] 其中 (delay(b_1, b_2)) 是与两个基本块布局位置相关的通信延迟,通常与曼哈顿距离成正比。这建立了布局与调度之间的耦合:布局位置影响延迟,延迟影响调度时间。
4. 流水线启动间隔约束:题目可能要求循环迭代间满足特定的启动间隔(II, Initiation Interval)。这意味着,同一个基本块在不同循环迭代中的实例,其启动时间需相差至少 II 个周期。这增加了调度问题的周期性约束。
3.3 目标函数
最直接的目标是最小化最后一个基本块完成的时间,即最小化最大完成时间(Makespan): [ \text{Minimize } \max_{b} (s_b + duration_b) ] 在满足所有约束的前提下,最小化此值。
注意:这个混合整数线性规划(MILP)模型虽然精确,但变量和约束数量会随着问题规模(基本块数、网格大小、时间周期范围)爆炸式增长,直接求解中等规模问题都可能不可行。因此,我们必须设计启发式或分解算法。
4. 分层优化:我们的求解策略与算法设计
直接求解完整的MILP模型不现实。我们采用了“分而治之”的分层优化策略,将布局和调度部分解耦,并迭代改进。
4.1 第一阶段:基于力导向的初始布局
目标:在不考虑调度和资源冲突细节的情况下,得到一个“好”的初始布局,重点优化通信成本。 我们借鉴了VLSI布局中的力导向算法。将每个基本块看作一个带电粒子,它们之间受到两种“力”:
- 吸引力:存在于有数据依赖的基本块之间。力的大小与依赖强度(如数据传输量)成正比,与距离成反比(或与距离成正比,目标是减小距离)。这促使有通信关系的基本块彼此靠近。
- 排斥力:存在于所有基本块之间。防止它们重叠,并促使它们均匀分布在芯片区域内。排斥力随距离减小而急剧增大。
算法迭代过程如下:
# 伪代码:力导向布局 def force_directed_placement(blocks, grid_size, max_iter=1000): # 随机初始化块的位置 positions = random_init_positions(blocks, grid_size) for iter in range(max_iter): total_force = np.zeros((len(blocks), 2)) # 计算吸引力(基于数据依赖图) for (b1, b2, weight) in data_dependency_edges: vec = positions[b2] - positions[b1] dist = max(np.linalg.norm(vec), 0.1) # 避免除零 attractive_force = weight * dist * (vec / dist) # 胡克定律模型 total_force[b1] += attractive_force total_force[b2] -= attractive_force # 计算排斥力(所有块对之间) for i in range(len(blocks)): for j in range(i+1, len(blocks)): vec = positions[j] - positions[i] dist = max(np.linalg.norm(vec), 0.1) repulsive_force = (k_repel / dist**2) * (vec / dist) # 库仑定律模型 total_force[i] -= repulsive_force total_force[j] += repulsive_force # 根据合力移动块,并投影到网格上 for i in range(len(blocks)): positions[i] += step_size * total_force[i] # 边界处理和网格对齐(取整到最近网格点) positions[i] = np.clip(positions[i], 0, grid_size-1) positions[i] = np.round(positions[i]).astype(int) # 早期停止条件:力很小或布局变化不大 if np.linalg.norm(total_force) < threshold: break return positions这个阶段得到的布局,通信成本较低,但完全没有考虑资源约束,很可能存在严重的资源热点。
4.2 第二阶段:基于列表调度的资源约束感知调度
在固定布局的基础上,我们进行调度。这里采用经典的列表调度(List Scheduling)算法,但加入了资源冲突的精细检查。
- 计算优先级:为每个基本块计算一个优先级,通常使用从该块到出口的最长路径长度(包括操作延迟和预估通信延迟),即高度(Height)。优先级高的块先调度。
- 维护就绪队列:所有前驱(数据依赖源)都已被调度的基本块进入就绪队列。
- 时钟周期推进:在每个时钟周期,检查就绪队列中的块,按优先级排序。
- 资源冲突检查:尝试将高优先级的块调度到当前周期启动。检查该块所需的所有资源类型,在其布局位置的影响范围内(曼哈顿距离≤R),在当前周期及后续执行周期内,是否均未被占用。这是一个精细的二维时空资源检查。
- 调度与资源占用:如果无冲突,则调度该块,并标记其占用的所有资源(所有类型,所有受影响位置,所有占用周期)为“已占用”。
- 循环:如果当前周期无法调度更多块(由于资源冲突),则时钟周期加1,重复步骤3-5,直到所有块被调度。
# 伪代码:资源约束列表调度 def resource_constrained_list_scheduling(blocks, positions, resource_grid): # 初始化:计算优先级,建立依赖图 ready_list = [b for b in blocks if b.predecessors_scheduled] schedule_result = {} resource_timeline = {} # 三维字典: resource_timeline[t][i][j][c] = 占用状态 current_cycle = 0 while unscheduled_blocks_exist: # 按优先级排序就绪列表 ready_list.sort(key=lambda b: -b.priority) scheduled_this_cycle = [] for block in ready_list: if can_schedule(block, current_cycle, positions, resource_timeline): # 调度该块 schedule_result[block] = current_cycle # 占用资源 occupy_resources(block, current_cycle, positions, resource_timeline) scheduled_this_cycle.append(block) # 从就绪列表中移除已调度的块 for b in scheduled_this_cycle: ready_list.remove(b) # 检查其后继是否就绪,并加入就绪列表 for succ in b.successors: if all(pred in schedule_result for pred in succ.predecessors): ready_list.append(succ) # 如果就绪列表非空但本轮一个都没调度(资源饱和),则时间前进 if not scheduled_this_cycle and ready_list: current_cycle += 1 elif scheduled_this_cycle: # 同一周期可以继续尝试调度剩余就绪块(某些资源可能释放) # 简单策略:直接进入下一周期以简化逻辑 current_cycle += 1 makespan = max([schedule_result[b] + b.duration for b in blocks]) return schedule_result, makespan4.3 第三阶段:布局与调度的迭代优化
第一阶段的布局只考虑了通信,第二阶段的调度基于固定布局。结果可能不理想。我们引入迭代优化循环:
- 根据调度结果,分析“关键路径”和“资源热点”。关键路径是限制整体执行时间最长的依赖链。资源热点是那些在多个周期内资源利用率接近100%的区域。
- 布局调整:针对关键路径上的基本块,尝试微调其位置(如与相邻块交换),以减少它们之间的通信延迟。针对资源热点区域的基本块,尝试将它们移动到资源相对宽松的区域,以缓解资源冲突,可能允许更紧凑的调度。
- 重新调度:布局微调后,重新运行列表调度算法。
- 接受准则:如果新的调度结果(总执行时间)优于之前,则接受此次布局调整;否则以一定概率接受(模拟退火思想),避免陷入局部最优。
- 循环:重复上述步骤,直到达到迭代次数上限或结果长时间无改善。
这个“布局微调 -> 重新调度 -> 评估”的循环,是我们算法获得高质量解的关键。它手动模拟了布局与调度之间的协同优化。
4.4 第四阶段:模拟退火框架下的全局优化
为了进一步提升解的质量并逃离局部最优,我们将第三阶段的迭代优化过程嵌入到一个模拟退火(Simulated Annealing)框架中。
- 状态:一个状态即一个完整的布局方案。
- 邻域操作:定义如何从一个布局状态产生微小扰动,生成邻居状态。我们主要采用三种操作:
- 交换(Swap):随机选择两个基本块,交换它们的位置。
- 移动(Move):随机选择一个基本块,将其移动到随机一个空闲网格节点上。
- 关键路径扰动:专门针对当前调度关键路径上的块进行移动或交换。
- 能量函数:状态的好坏用该布局下,通过列表调度得到的总执行时间(Makespan)来衡量。能量越低(执行时间越短)越好。
- 退火过程:从高温开始,迭代进行。每次迭代随机生成一个邻居状态,计算其能量。如果新能量更低,则接受新状态;如果更高,则以概率 (P = \exp(-\Delta E / T)) 接受,其中 (T) 是当前温度。然后缓慢降低温度 (T)。随着温度降低,系统越来越倾向于接受更优解,最终“淬火”到一个稳定解。
# 伪代码:模拟退火主循环 def simulated_annealing(initial_layout, initial_temp=1000, cooling_rate=0.95, max_iter=5000): current_layout = initial_layout current_schedule, current_makespan = list_scheduling(current_layout) best_layout = current_layout.copy() best_makespan = current_makespan T = initial_temp for iter in range(max_iter): # 生成邻居布局 new_layout = generate_neighbor(current_layout) # 通过Swap/Move操作 # 评估邻居布局 new_schedule, new_makespan = list_scheduling(new_layout) delta_e = new_makespan - current_makespan if delta_e < 0 or random.random() < math.exp(-delta_e / T): # 接受新布局 current_layout = new_layout current_makespan = new_makespan if current_makespan < best_makespan: best_layout = current_layout.copy() best_makespan = current_makespan # 降温 T *= cooling_rate if T < 1e-6: break return best_layout, best_makespan5. 代码实现中的工程细节与性能优化
将上述算法思想转化为可运行的代码,会遇到许多实际问题。这里分享几个关键的工程实现细节。
5.1 数据结构设计:高效管理资源时空占用
资源冲突检查是调度算法中最频繁、最耗时的操作。我们设计了一个高效的数据结构来管理三维(空间x, y + 时间t)资源占用状态。
- 核心思想:对于每种资源类型 (t),维护一个二维列表
resource_map_t,其元素是BitArray或Python的int位掩码。resource_map_t[i][j]是一个位掩码,其第 (c) 位表示在周期 (c),位置 ((i, j)) 的该类资源是否被占用。 - 占用操作:当调度一个在周期 (s) 开始、持续 (d) 周期、位于 ((i_b, j_b))、影响范围 (R) 的基本块时,需要对所有满足 (dist((i,j), (i_b,j_b)) \leq R) 的位置 ((i, j)),将
resource_map_t[i][j]的第 (s) 到第 (s+d-1) 位设置为1。这可以通过位运算(OR操作)快速完成。 - 冲突检查:检查一个基本块能否在周期 (s) 调度,只需检查其影响范围内所有位置对应的位掩码,在区间 ([s, s+d-1]) 内是否有任何一位已经被设置为1。这可以通过预计算的掩码进行按位与(AND)操作来实现,结果为0则表示无冲突。
class ResourceManager: def __init__(self, grid_rows, grid_cols, max_cycles, resource_types): self.grid_rows = grid_rows self.grid_cols = grid_cols self.max_cycles = max_cycles # 使用整数位掩码,假设max_cycles <= 64,否则需使用BitArray库 self.maps = {t: [[0 for _ in range(grid_cols)] for _ in range(grid_rows)] for t in resource_types} def can_allocate(self, res_type, center_row, center_col, start_cycle, duration, radius): """检查从start_cycle开始,持续duration周期,在中心(center_row, center_col)半径radius范围内,资源res_type是否可用""" mask = ((1 << duration) - 1) << start_cycle # 生成占用区间的位掩码 for dr in range(-radius, radius+1): for dc in range(-radius, radius+1): if abs(dr) + abs(dc) > radius: # 曼哈顿距离判断 continue r, c = center_row + dr, center_col + dc if 0 <= r < self.grid_rows and 0 <= c < self.grid_cols: if self.maps[res_type][r][c] & mask != 0: return False # 冲突 return True def allocate(self, res_type, center_row, center_col, start_cycle, duration, radius): """占用资源""" mask = ((1 << duration) - 1) << start_cycle for dr in range(-radius, radius+1): for dc in range(-radius, radius+1): if abs(dr) + abs(dc) > radius: continue r, c = center_row + dr, center_col + dc if 0 <= r < self.grid_rows and 0 <= c < self.grid_cols: self.maps[res_type][r][c] |= mask这种位运算的方法将三维检查降低为二维空间遍历和整数位操作,极大提升了性能。
5.2 调度算法的加速技巧
列表调度中,每一周期都需要遍历就绪列表并检查资源。我们可以引入以下优化:
- 优先级缓存与更新:基本块的优先级(高度)在调度开始前计算一次。除非依赖关系或布局发生重大变化,否则不需要重新计算。
- 就绪列表的增量维护:使用一个计数器记录每个基本块尚未被调度的前驱数量。当一个前驱被调度时,将其所有后继的计数器减1。当计数器减为0时,将该后继加入就绪列表。这比每次扫描所有块检查前驱要高效。
- 资源冲突的快速预筛:在精细的位掩码检查之前,可以先进行粗粒度检查。例如,维护每个资源类型在全局范围内的周期利用率直方图。如果某个周期某种资源全局利用率已经接近100%,那么在这个周期调度需要该资源的新块成功率就很低,可以优先考虑其他周期。
5.3 模拟退火参数调优
模拟退火的性能很大程度上取决于参数设置:
- 初始温度 (T_0):设置过高,前期会接受太多劣质解,收敛慢;设置过低,则过早失去跳出局部最优的能力。我们通过实验,让初始状态下接受劣质解的概率大约在0.5左右来反推 (T_0)。即计算初始布局随机扰动多次产生的能量差 (\Delta E),取平均值 (\bar{\Delta E}),令 (exp(-\bar{\Delta E} / T_0) = 0.5),解得 (T_0 = -\bar{\Delta E} / \ln(0.5))。
- 降温速率:我们采用几何降温 (T_{new} = \alpha \cdot T_{old})。(\alpha) 通常在0.9到0.99之间。我们选用0.95,在总迭代次数(如5000)内,温度可以下降到足够低。
- 马尔可夫链长度:每个温度下的迭代次数。我们采用固定次数,与问题规模相关,例如
100 * num_blocks。 - 终止条件:我们设定了双重条件:温度低于阈值(如1e-6),或连续若干次降温后最优解未更新。
实操心得:参数调优没有银弹。最好的方法是针对赛题提供的几个测试用例,用小规模的参数网格搜索,快速评估不同参数组合的性能,选择一个鲁棒性较好的组合。我们当时就写了一个简单的自动化脚本,遍历不同的 (T0, alpha, chain_length) 组合,记录最终 makespan 和运行时间。
6. 结果分析与可视化:如何呈现你的解决方案
对于数模竞赛,清晰的呈现和有力的分析同样重要。
6.1 关键结果输出
我们的程序最终输出包括:
- 最优布局图:用二维网格图展示每个基本块的位置,可以用不同颜色或形状区分基本块类型或所属循环。使用
matplotlib的imshow或scatter绘制。 - 调度甘特图:用甘特图展示每个基本块的开始时间、结束时间,以及它们占用的资源类型。这能直观显示流水线的填充情况和资源利用率。
- 资源利用率热力图:对于每种资源类型,生成一个随时间变化的二维空间热力图(可以做成动画或分周期静态图),清晰展示资源热点的形成与迁移。
- 性能指标:
- 总执行周期(Makespan):核心优化目标。
- 资源利用率:各种资源在时间和空间上的平均利用率。利用率越高,通常说明布局和调度越紧凑。
- 通信开销占比:所有数据依赖边的延迟总和占总执行时间的比例。用于评估布局对通信的优化效果。
- 算法收敛曲线:绘制模拟退火过程中能量(Makespan)随迭代次数的变化曲线,展示优化过程。
6.2 对比实验与灵敏度分析
为了体现算法有效性,我们设计了对比实验:
- 基准对比:与简单的随机布局+列表调度、贪心布局(如按依赖关系链排列)等方法进行对比,展示我们分层迭代+模拟退火策略的优越性。
- 参数灵敏度分析:分析关键参数(如资源影响半径R、通信延迟系数)对最终结果的影响。例如,绘制 Makespan 随 R 变化的曲线,讨论其权衡:R 增大,资源竞争加剧,可能拉长调度;R 减小,资源碎片化可能增加。
- 可扩展性测试:在更大的随机生成算例上测试算法运行时间和解的质量,分析其可扩展性。
6.3 可视化代码片段示例
import matplotlib.pyplot as plt import matplotlib.patches as mpatches def plot_layout(blocks, positions, grid_rows, grid_cols): fig, ax = plt.subplots(figsize=(10, 8)) # 绘制网格 for i in range(grid_rows+1): ax.axhline(i-0.5, color='gray', linestyle='-', linewidth=0.5) for j in range(grid_cols+1): ax.axvline(j-0.5, color='gray', linestyle='-', linewidth=0.5) # 绘制基本块 colors = plt.cm.tab20(np.linspace(0, 1, len(set(b.type for b in blocks)))) type_to_color = {t: colors[i] for i, t in enumerate(set(b.type for b in blocks))} for b in blocks: i, j = positions[b.id] rect = mpatches.Rectangle((j-0.4, i-0.4), 0.8, 0.8, linewidth=1, edgecolor='black', facecolor=type_to_color[b.type], alpha=0.7) ax.add_patch(rect) ax.text(j, i, f'B{b.id}', ha='center', va='center', fontsize=8) ax.set_xlim(-0.5, grid_cols-0.5) ax.set_ylim(-0.5, grid_rows-0.5) ax.set_aspect('equal') ax.invert_yaxis() # 矩阵坐标系,左上角为(0,0) ax.set_title('Final Block Placement') plt.show() def plot_gantt(schedule, durations): fig, ax = plt.subplots(figsize=(12, 6)) blocks = list(schedule.keys()) y_pos = range(len(blocks)) for i, b in enumerate(blocks): start = schedule[b] duration = durations[b] ax.barh(i, duration, left=start, height=0.6, edgecolor='black') ax.text(start + duration/2, i, f'B{b}', va='center', ha='center', color='white', fontsize=8) ax.set_yticks(y_pos) ax.set_yticklabels([f'B{b}' for b in blocks]) ax.set_xlabel('Clock Cycle') ax.set_title('Scheduling Gantt Chart') ax.grid(axis='x', linestyle='--', alpha=0.7) plt.tight_layout() plt.show()通过这些图表和数据分析,我们向评委展示了不仅是一个“答案”,更是一个完整、深入、可信的问题求解过程。
7. 参赛总结与对芯片EDA的思考
回顾整个解题过程,从最初的茫然到最终的豁然开朗,我们最大的收获不是学会了某个特定算法,而是掌握了一套处理复杂系统级优化问题的方法论:理解问题本质 -> 建立数学模型 -> 设计分层/分解策略 -> 实现核心算法 -> 工程优化提速 -> 实验验证分析。这套方法论在解决其他领域的调度、布局、路径规划等问题时同样适用。
这道赛题也让我对芯片设计,尤其是后端物理设计和高级综合(HLS)有了更感性的认识。现代芯片动辄数十亿晶体管,其布局布线问题比我们这个简化模型复杂千万倍,需要用到更强大的数学规划求解器(如GUROBI, CPLEX)、基于机器学习的预测模型,以及超大规模的并行计算。我们实现的模拟退火、力导向等方法,虽然是入门级技术,但却是理解更复杂算法的基础。
对于后来者,我的建议是:不要被问题的领域吓倒。数模竞赛考察的是将实际问题转化为数学模型并求解的能力。抓住“资源”、“约束”、“优化目标”这几个核心词,大胆假设,小心建模,充分利用熟悉的算法工具(图论、规划、启发式搜索),你就能找到通往答案的路径。最后,一定要重视编程实现和结果可视化,一个能跑出结果、生成漂亮图表的程序,比一篇只有公式的论文更有说服力。