#import "labtemplate.typ": * #show: nudtlabpaper.with( author1: "程景愉", id1: "202302723005", advisor: " 胡罡", jobtitle: "教授", lab: "306-707", date: "2025.12.18", header_str: "贪心算法分析实验报告", minimal_cover: true, ) #set page(header: [ #set par(spacing: 6pt) #align(center)[#text(size: 11pt)[《算法设计与分析》实验报告]] #v(-0.3em) #line(length: 100%, stroke: (thickness: 1pt)) ],) #show heading: it => box(width: 100%)[ #v(0.50em) #set text(font: hei) #it.body ] #outline(title: "目录",depth: 3, indent: 1em) // #pagebreak() #outline( title: [图目录], target: figure.where(kind: image), ) #show heading: it => box(width: 100%)[ #v(0.50em) #set text(font: hei) #counter(heading).display() #it.body ] #set enum(indent: 0.5em,body-indent: 0.5em,) #pagebreak() = 实验介绍 #para[ 贪心算法(Greedy Algorithm)是指在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,算法得到的是在某种意义上的局部最优解。多机调度问题是经典的 NP-Hard 问题,本实验旨在通过实现和对比不同的贪心策略(List Scheduling 和 LPT),深入理解贪心算法的近似比性质,并探讨其在实际场景(如 GPU 集群调度)中的应用。 ] = 实验内容 #para[ 本实验主要围绕多机调度问题的贪心算法展开,并扩展至在线 GPU 集群调度模拟。具体内容包括: ] + 实现两种贪心策略:任意顺序列表调度 (List Scheduling, LS) 和 最长处理时间优先 (Longest Processing Time, LPT)。 + 实现基于分支限界 (Branch and Bound) 的最优解求解算法,作为性能评估的基准。 + 构造特定的“最坏情况”输入,验证贪心算法的理论近似比下界。 + 通过大量随机测试样本,统计不同算法的近似比分布及运行时间,分析 $m$ (机器数) 和 $n$ (作业数) 对性能的影响。 + (附加)模拟 GPU 集群在线调度场景,设计并对比不同的调度策略在不同负载下的表现。 = 实验要求 #para[ 针对多机调度问题,实验具体要求如下: ] + 针对多机调度问题,实现 LS 和 LPT 两种贪心算法。 + 实现遍历的最优解求解算法(分支限界法)。 + 构造最坏情况输入,结合理论证明进行讨论。 + 固定 $m, n$,随机产生大量样本,计算贪心解与最优解的比值(近似比),并分析其概率分布。 + 改变 $m, n$,对比分析结果。 + 附加:模拟 GPU 集群调度,考虑利用率 $eta$ 和用户延迟 $delta$,设计多种策略并分析。 = 实验步骤 == 算法设计 === 算法一:列表调度 (List Scheduling, LS) #para[ LS 算法是最朴素的贪心策略。它按照作业输入的任意顺序,依次将作业分配给当前负载最小的机器。 该算法是一种在线算法,其时间复杂度为 $O(n log m)$ (使用优先队列维护机器负载) 或 $O(n m)$ (线性扫描)。 理论上,LS 算法的近似比为 $2 - 1/m$。 ] ```cpp // 核心代码片段 long long greedy_ls(int m, const vector& jobs) { vector machines(m, 0); for (const auto& job : jobs) { int min_idx = 0; // Find machine with min load for (int i = 1; i < m; ++i) { if (machines[i] < machines[min_idx]) min_idx = i; } machines[min_idx] += job.duration; } return *max_element(machines.begin(), machines.end()); } ``` === 算法二:最长处理时间优先 (LPT) #para[ LPT 算法在 LS 的基础上增加了预处理步骤:将所有作业按处理时间递减排序,然后依次分配给负载最小的机器。 排序操作使得较大的作业优先被处理,从而避免了最后剩下一个大作业导致机器负载极不均衡的情况。 该算法的时间复杂度主要由排序决定,为 $O(n log n)$。 理论上,LPT 算法的近似比为 $4/3 - 1/(3m)$。 ] === 算法三:最优解 (Branch and Bound) #para[ 为了评估贪心算法的性能,我们需要求得问题的最优解。由于多机调度是 NP-Complete 问题,我们采用深度优先搜索配合分支限界 (Branch and Bound) 来求解。 剪枝策略包括: ] 1. 当前最大负载已经超过已知最优解,停止搜索。 2. 理论下界剪枝:如果 `max(当前最大负载, (剩余作业总长 + 当前总负载)/m)` 超过已知最优解,停止搜索。 3. 对称性剪枝:若多台机器当前负载相同,则分配给它们是等价的,只尝试第一台。 == 最坏情况构造与分析 === LS 算法最坏情况 #para[ *构造方法:* 对于 $m$ 台机器,输入 $m(m-1)$ 个时长为 1 的小作业,紧接着 1 个时长为 $m$ 的大作业。 ] #para[ *分析:* LS 算法会将前 $m(m-1)$ 个小作业均匀分配给 $m$ 台机器,每台机器负载为 $m-1$。最后的大作业将被分配给任意一台机器,使其最终负载变为 $(m-1) + m = 2m-1$。 而最优解是将所有小作业均匀分配给 $m-1$ 台机器(每台负载 $m$),将大作业单独分配给剩下一台机器(负载 $m$),此时 MakeSpan 为 $m$。 近似比为 $(2m-1)/m = 2 - 1/m$。 本实验通过代码验证了 $m=3, 4, 5$ 时的该情况,结果与理论完全一致。 ] === LPT 算法最坏情况 #para[ *构造方法:* 经典的 LPT 最坏情况较为复杂,例如 $m=2$ 时,作业集为 $\{3, 3, 2, 2, 2\}$。 ] #para[ *分析:* 排序后为 $3, 3, 2, 2, 2$。 LPT 分配:M1: $3, 2, 2$ (总 7), M2: $3, 2$ (总 5)。MakeSpan = 7。 最优解:M1: $3, 3$ (总 6), M2: $2, 2, 2$ (总 6)。MakeSpan = 6。 近似比 $7/6 approx 1.167$。理论界 $4/3 - 1/6 = 7/6$。实验验证吻合。 ] == 实验数据与可视化 #para[ 我们对 $m in {3, 5, 8}$ 和 $n in {10, dots, 100}$ 进行了大量随机测试。 ] #figure( image("results/ratio_boxplot.png", width: 80%), caption: [LS 与 LPT 算法近似比分布对比], ) #figure( image("results/ratio_vs_n.png", width: 80%), caption: [近似比随作业数量 n 的变化趋势], ) #figure( image("results/time_comparison.png", width: 80%), caption: [算法平均运行时间对比], ) = 实验结果分析 #para[ 1.*近似比性能:* 从箱线图可以看出,LPT 算法的近似比极其接近 1(通常在 1.0 - 1.05 之间),性能极其优越且稳定。相比之下,LS 算法的近似比分布较宽,平均在 1.1 - 1.3 之间,且随着 $m$ 的增加,最差情况(近似比上界)有升高的趋势,符合 $2 - 1/m$ 的理论预测。 ] #para[ 2.*规模的影响:* 随着作业数 $n$ 的增加,LS 的近似比往往会下降并趋于稳定。这是因为大量随机作业往往能“填平”机器间的负载差异。LPT 则始终保持高效。 ] #para[ 3.*运行时间:* 贪心算法(LS, LPT)的运行时间极短(微秒级),且随 $n$ 线性或近线性增长。最优解算法(B&B)随 $n$ 指数级增长,当 $n > 20$ 时已难以在短时间内求解,验证了 NP-Hard 问题的计算复杂性。 ] = 实验总结 #para[ 本实验深入分析了多机调度问题的贪心求解策略。实验结果表明,虽然 LS 算法实现简单,但在最坏情况下性能较差。简单的排序预处理(LPT 策略)能带来巨大的性能提升,使其在绝大多数随机及构造测试中都能获得极接近最优解的结果。这启示我们在设计贪心算法时,合理的贪心顺序(如优先处理“困难”或“大”的任务)至关重要。 ] #pagebreak() = 附加:GPU 集群在线调度模拟 == 场景描述 #para[ 模拟一个拥有 $m=64$ 块 GPU 的集群任务调度。任务到达服从泊松分布,单机执行时间服从均匀分布。任务支持并行 ($k$ 块 GPU),但存在并行效率损耗:效率因子 $E_k = sigma^(log_2 k)$,其中 $sigma in [0.75, 0.95]$。系统目标是平衡 *集群利用率 ($eta$)* 和 *用户平均延迟 ($delta$)*。 ] == 调度策略设计 #para[ 我们设计了三种策略进行对比: ] + *策略 A:保守策略 (Conservative)* 总是为每个任务分配 $k=1$ 块 GPU。其思路是最大化计算资源的“有效性”,避免并行损耗。 + *策略 B:激进策略 (Aggressive)* 总是尽可能分配最大的并行度(如 $k=32$ 或 $64$)。其思路是最小化单任务执行时间,但忽略了巨大的资源浪费。 + *策略 C:自适应策略 (Adaptive)* 根据当前等待队列的长度动态调整 $k$。若队列为空,使用高并行度加速;若队列拥堵,降低并行度以提高吞吐量。 == 模拟结果 #figure( image("results/gpu_sim_plots.png", width: 90%), caption: [不同负载下三种策略的利用率与延迟对比], ) #para[ 实验在轻负载 ($lambda=0.5$)、中负载 ($lambda=0.9$) 和重负载 ($lambda=1.1$) 下进行了模拟。结果显示: ] + *保守策略 (Conservative)*:在所有负载下都能保持较低的延迟。 + *激进策略 (Aggressive)*:表现极差。由于并行效率损失,导致系统迅速过载,用户延迟呈爆炸式增长。 + *自适应策略 (Adaptive)*:表现最为均衡。在轻负载时加速任务,在重负载时保证系统稳定性。 == 结论 #para[ 在具有并行开销的资源调度场景中,盲目追求高并行度(激进策略)是不可取的。通过感知系统负载来动态调整资源分配粒度的 *自适应策略*,是更为优越的解决方案。 ]