两机器流水线调度问题(Johnson算法)
流水线调度问题(Flow Shop Scheduling Problem)是组合优化和算法设计中的一个经典问题,常见于制造业、计算机任务调度等场景。其核心目标是安排多个作业(jobs)在多台机器(machines)上的处理顺序,以最小化总完成时间(makespan)或其他目标函数。在算法竞赛中,这类问题通常需要结合贪心策略或动态规划来解决。
问题模型
• 输入:有 n
个作业和 m
台机器,每个作业必须依次经过所有机器(顺序固定,如机器1 → 机器2 → … → 机器m)。
• 处理时间:作业 i
在机器 j
上的处理时间为 p[i][j]
。
• 目标:找到作业的调度顺序,使得最后一个作业在所有机器上的完成时间最小(即最小化 makespan)。
经典贪心策略:Johnson算法(两机器流水线调度)
当 m=2
(两台机器)时,Johnson算法 可以找到最优解,其核心是通过贪心排序策略减少机器间的空闲等待时间。
算法步骤
- 分类作业:
• 将作业分为两类:
◦ A类:在机器1的处理时间 ≤ 机器2的处理时间(即p[i][1] ≤ p[i][2]
)。
◦ B类:在机器1的处理时间 > 机器2的处理时间(即p[i][1] > p[i][2]
)。 - 排序规则:
• A类作业按机器1的处理时间升序排序(先处理耗时短的)。
• B类作业按机器2的处理时间降序排序(先处理耗时长的)。 - 合并顺序:按
A类作业排序结果 + B类作业排序结果
的顺序调度所有作业。
正确性原理
• A类作业优先处理机器1耗时短的,让机器2尽快开始工作。
• B类作业优先处理机器2耗时长的,避免机器2长时间空闲。
示例
假设有4个作业,在两台机器上的处理时间如下:
作业 | 机器1时间 | 机器2时间 |
---|---|---|
1 | 3 | 6 |
2 | 5 | 2 |
3 | 1 | 4 |
4 | 6 | 7 |
• A类作业(p[i][1] ≤ p[i][2]
):作业1、3、4。
• 按机器1时间升序排序:作业3(1)→ 作业1(3)→ 作业4(6)。
• B类作业(p[i][1] > p[i][2]
):作业2。
• 按机器2时间降序排序:作业2(2)。
• 最终顺序:3 → 1 → 4 → 2。总完成时间为18。
多机器流水线调度(m > 2)
当机器数超过2时,问题变为NP-hard,无法在多项式时间内找到精确解。常用方法包括:
- 贪心启发式:
• NEH算法(Nawaz-Enscore-Ham):按总处理时间降序插入作业,每次选择最优插入位置。 - 动态规划:适用于小规模问题(如n ≤ 20)。
- 元启发式算法:遗传算法、模拟退火等(竞赛中较少用)。
算法竞赛中的常见变种
- 带准备时间的流水线调度:
• 机器切换作业时需要准备时间,需调整排序策略。 - 抢占式调度:
• 允许中断当前作业以处理更高优先级的任务(需用优先队列)。 - 并行机器:
• 每个阶段有多台并行机器(如LeetCode的“任务调度器”问题)。
典型例题
- 两机器流水线调度(Johnson算法直接应用):
• 题目描述:给定n个作业在两台机器的时间,求最小makespan。
• 代码实现:排序后模拟调度过程。 - HDOJ 1078(类似流水线调度):需结合贪心策略优化作业顺序。
- LeetCode 621(任务调度器):虽然不是严格流水线问题,但涉及类似的贪心策略(空闲时间填充)。
例题:蓝桥算法赛破译密码
代码:
c++:
#include <iostream>
#include <algorithm>
using namespace std;
pair<int, int> a[1005];
int main() {
int n;
cin>>n;
for (int i=1; i<=n; i++)
cin>>a[i].first;
for (int i=1; i<=n; i++)
cin>>a[i].second;
sort(a+1, a+1+n, [](pair<int, int> &p1, pair<int, int> &p2) {
if (p1.first <= p1.second && p2.first > p2.second)
return true;
if (p2.first <= p2.second && p1.first > p1.second)
return false;
if (p1.first <= p1.second)
return p1.first < p2.first;
else
return p1.second > p2.second;
});
int ans = 0, t = 0;
for (int i=1; i<=n; i++)
t += a[i].first, ans = max(ans, t) + a[i].second;
cout<<ans<<'\n';
return 0;
}
java:
import java.util.*;
// 自定义包含两个整数的类
class PII {
public int first;
public int second;
public PII(int first, int second) {
this.first = first;
this.second = second;
}
}
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt();
PII[] jobs = new PII[n];
// 读取机器1的处理时间
for (int i = 0; i < n; i++) {
jobs[i] = new PII(scanner.nextInt(), 0);
}
// 读取机器2的处理时间并填充对象
for (int i = 0; i < n; i++) {
jobs[i].second = scanner.nextInt();
}
// Johnson算法排序
Arrays.sort(jobs, new Comparator<PII>() {
@Override
public int compare(PII p1, PII p2) {
boolean p1IsA = (p1.first <= p1.second);
boolean p2IsA = (p2.first <= p2.second);
// A类作业排在前,B类在后
if (p1IsA && !p2IsA) return -1;
if (!p1IsA && p2IsA) return 1;
// 同类作业的排序规则
if (p1IsA) {
return Integer.compare(p1.first, p2.first); // A类升序
} else {
return Integer.compare(p2.second, p1.second); // B类降序
}
}
});
// 计算总完成时间
int timeMachine1 = 0;
int makespan = 0;
for (PII job : jobs) {
timeMachine1 += job.first;
makespan = Math.max(makespan, timeMachine1) + job.second;
}
System.out.println(makespan);
}
}