【华为OD机考真题】流水线调度 · 最短完工时间 (Java/Go)
一、题目
题目描述:
一个工厂有 m 条流水线,来并行完成 n 个独立的作业,该工厂设置了一个调度系统,在安排作业时,总是优先执行处理时间最短的作业。
现给定流水线个数 m,需要完成的作业数 n,每个作业的处理时间分别为 t1, t2 ... tn。请你编程计算处理完所有作业的耗时为多少?
当 n > m 时,首先处理时间短的 m 个作业进入流水线,其他的等待,当某个作业完成时,依次从剩余作业中取处理时间最短的进入处理。
输入描述:
第一行为 2 个整数(采用空格分隔),分别表示流水线个数 m 和作业数 n
第二行输入 n 个整数(采用空格分隔),表示每个作业的处理时长 t1, t2 ... tn。
0 < m, n < 100
0 < t1, t2 ... tn < 100
注:保证输入都是合法的。
输出描述:
输出处理完所有作业的总时长示例
输入:
3 5
8 4 3 2 10输出:
13说明:
先安排时间为 2、3、4 的 3 个作业。
第一条流水线先完成作业(时间 2),然后调度剩余时间最短的作业 8。
第二条流水线完成作业(时间 3),然后调度剩余时间最短的作业 10。
总工时就是第二条流水线完成作业的时间 13(3 + 10)。
二、解题思路:贪心 + 最小堆模拟
1. 核心逻辑
这道题是一个典型的贪心算法结合优先队列的问题。
- 贪心策略:为了让总时间最短,必须让忙碌的流水线尽早空闲下来去接新的活。因此,总是把新任务交给当前预计完成时间最早的那条流水线。同时,任务本身也要按从小到大的顺序处理,确保短任务优先被消化。
- 数据结构:我们需要一个数据结构来维护 mm 条流水线的当前完成时间,并能快速取出最小值。这正是最小堆(Min-Heap)的用武之地。
2. 算法步骤
- 排序:将 n 个作业时长数组
tasks从小到大排序。 - 初始化堆:
- 如果 n≤m :直接返回最大的那个作业时长(因为可以并行同时做)。
- 如果 n>m :将前 m 个作业的时长放入最小堆中,代表 m 条流水线目前的完工时间。
- 模拟调度:
- 遍历剩余的 n−m 个作业。
- 每次从堆顶弹出最小值
min_time(代表最早空闲的流水线时刻)。 - 将该时刻加上当前作业时长
task,得到新的完工时间new_time = min_time + task。 - 将
new_time重新推入堆中。
- 获取结果:
- 当所有作业都分配完毕后,堆中存储的是每条流水线的最终完工时间。
- 答案即为堆中的最大值。
- 技巧:在 Go/Java 中,堆弹空后的最后一个元素,或者遍历堆数组找最大值即可。其实更简单的逻辑是:最后一次弹出的
min_time加上对应的task并不一定是最大值,必须遍历堆中所有元素取最大值,或者在循环结束后,不断弹出堆直到只剩一个最大值(不推荐,破坏结构),最直接的方法是遍历堆底层数组找最大。
3. 复杂度分析
- 时间复杂度:
- 排序: O(NlogN) 。
- 堆操作:共进行 N−m 次入堆和出堆,每次 O(logM) 。
- 总复杂度: O(NlogN+NlogM) 。鉴于 N,M<100 ,效率极高。
- 空间复杂度: O(M) 用于存储堆。
三、代码实现💻
1. Java 实现
Java 拥有强大的标准库 java.util.PriorityQueue,默认就是最小堆,实现非常简洁。
import java.util.Arrays;
import java.util.PriorityQueue;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 读取 m 和 n
if (!scanner.hasNextInt()) return;
int m = scanner.nextInt();
int n = scanner.nextInt();
int[] tasks = new int[n];
for (int i = 0; i < n; i++) {
tasks[i] = scanner.nextInt();
}
// 1. 贪心:先对作业时长排序
Arrays.sort(tasks);
// 特殊情况:如果作业数少于等于流水线数,耗时就是最长的那个作业
if (n <= m) {
System.out.println(tasks[n - 1]);
return;
}
// 2. 初始化最小堆,存储每条流水线的“当前完成时间”
// Java PriorityQueue 默认是最小堆
PriorityQueue<Integer> pq = new PriorityQueue<>();
// 先将前 m 个作业分配给 m 条流水线
for (int i = 0; i < m; i++) {
pq.offer(tasks[i]);
}
// 3. 模拟调度过程
for (int i = m; i < n; i++) {
// 取出最早完成的流水线时间
int earliestFinishTime = pq.poll();
// 将当前最短作业分配给它,计算新的完成时间
int newFinishTime = earliestFinishTime + tasks[i];
// 放回堆中
pq.offer(newFinishTime);
}
// 4. 结果是所有流水线完成时间的最大值
// 此时堆中可能有多个元素,需要遍历找最大值
int maxTime = 0;
for (int time : pq) {
if (time > maxTime) {
maxTime = time;
}
}
System.out.println(maxTime);
scanner.close();
}
}
2. Go 实现
Go 的标准库 container/heap 需要用户实现 heap.Interface 接口。虽然稍微繁琐一点,但能让我们更深入理解堆的底层运作,且性能极佳。
package main
import (
"container/heap"
"fmt"
"sort"
)
// IntHeap 定义一个最小堆类型
type IntHeap []int
// 实现 heap.Interface 接口的五个方法
func (h IntHeap) Len() int { return len(h) }
func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] } // 最小堆:小的在上面
func (h IntHeap) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
func (h *IntHeap) Push(x interface{}) {
*h = append(*h, x.(int))
}
func (h *IntHeap) Pop() interface{} {
old := *h
n := len(old)
x := old[n-1]
*h = old[0 : n-1]
return x
}
func solve() {
var m, n int
if _, err := fmt.Scan(&m, &n); err != nil {
return
}
tasks := make([]int, n)
for i := 0; i < n; i++ {
fmt.Scan(&tasks[i])
}
// 1. 贪心:排序
sort.Ints(tasks)
// 特殊情况
if n <= m {
fmt.Println(tasks[n-1])
return
}
// 2. 初始化堆
h := &IntHeap{}
heap.Init(h)
// 放入前 m 个作业
for i := 0; i < m; i++ {
heap.Push(h, tasks[i])
}
// 3. 模拟调度
for i := m; i < n; i++ {
// 弹出最小值(最早完成的流水线时间)
earliest := heap.Pop(h).(int)
// 加上新作业时间
newTime := earliest + tasks[i]
// 推回堆
heap.Push(h, newTime)
}
// 4. 找最大值
maxTime := 0
for _, t := range *h {
if t > maxTime {
maxTime = t
}
}
fmt.Println(maxTime)
}
func main() {
solve()
}
四、关键点深度解析🔍
1. 为什么必须先排序作业?
题目明确要求“总是优先执行处理时间最短的作业”。
- 如果不排序,直接按输入顺序分配,可能会导致长作业阻塞了流水线,而短作业在后面等待,违背了题目的调度规则。
- 排序保证了我们每次取出的
tasks[i]都是当前剩余任务中最小的,符合贪心策略。
2. 为什么用最小堆维护流水线时间?
- 我们的目标是找到“谁最先闲着”。
- 数组遍历找最小值是 O(M) ,而堆顶取值是 O(1) ,调整堆是 O(logM) 。
- 在 N 较大时,堆的优势非常明显。本题虽然 N,M<100 数据量小,但在机考中养成使用合适数据结构的习惯至关重要。
3. 结果为何是堆中的最大值?
- 堆中存储的是每条流水线最终的任务完成时刻。
- 整个工厂完工,取决于最慢的那条流水线。
- 注意:堆顶是最小值(最早完成的),所以不能直接
Pop出来当结果,必须遍历堆内所有元素取Max。
4. 边界情况处理
- n≤m :作业比流水线少或刚好够分。此时所有作业同时开始,总耗时就是单个作业中最长的那个(排序后的最后一个)。代码中做了特判,避免逻辑复杂化。
- 输入合法性:题目保证合法,实际工程中可增加对 m=0 等的防御性编程。
五、总结与建议🚀
这道题是“多机调度问题”的一个简化变种(带有特定贪心规则)。
- 思维转换:将物理世界的“流水线空闲”抽象为数字世界中的“最小值弹出”。
- 语言特性:
- Java:利用
PriorityQueue可以快速写出生产级代码,适合追求开发效率的场景。 - Go:通过实现
heap.Interface,展示了 Go 接口组合的灵活性,适合对性能和底层控制有要求的场景。
- Java:利用
给考生的建议:
- 熟记模板:Java 的
PriorityQueue用法和 Go 的heap接口实现模板要烂熟于心。 - 审题细节:注意题目中的“优先执行短作业”是指任务队列的排序,而“空闲即插”是指资源的分配策略,两者结合才是正解。
- 结果提取:千万不要以为堆顶就是答案,对于“完工时间”类问题,答案往往是集合中的最大值。
掌握这种“排序 + 优先队列模拟”的模式,你可以轻松解决类似的资源分配、任务调度、甚至哈夫曼编码构造等问题!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)