#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<Job>& jobs) {
    vector<long long> 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[
在具有并行开销的资源调度场景中，盲目追求高并行度（激进策略）是不可取的。通过感知系统负载来动态调整资源分配粒度的 *自适应策略*，是更为优越的解决方案。
]
