流水作业调度问题
问题描述
在流水线作业中,我们有N个作业(编号1到N),每个作业加工的顺序都是先在M1上加工,然后再在M2上加工。每个作业在M1上的加工时间是ai ,在M2上的加工时间是 bi 我们的目标是找到一个最优的作业顺序,以最小化从第一个作业开始到所有作业完成的总时间。
流水作业调度问题要求确定这N个作业的最优加工顺序,使得从第一个作业在机器M1上开始加工,到最后一个作业在机器M2上加工完成所需的时间最少。可以假定任何一个作业一旦开始加工,就不允许被中断,知道该作业被完成,即非优先调度,减少空闲时间。
采用归纳思路求解最少时间
初始情况
当只有一个作业 ( (a1, b1) ) 时,显然最少时间 ( T = a1 + b1 )。
两个作业的情况
考虑有两个作业 (a1, b1) 和(a2, b2)。
① 若(a1, b1) 在前执行而 (a2, b2) 在后执行:
(a) 作业2在M2上执行没有等待的情况
- 最少时间 ( T = a1 + b1 + a2 + b2 - b1 ) (( b1 < a2 ))
(b) 作业2在M2上执行有等待的情况
- 最少时间 ( T = a1 + b1 + a2 + b2 - a2 ) (( a2 < b1 ))
综合 (a)、(b)
- 我们可以得到最少时间:
[ T = a1 + b1 + a2 + b2 - min(a2, b1) ]
② 若 ( (a2, b2) ) 在前执行而 ( (a1, b1) ) 在后执行:
- 可以求出最少时间:
[ T = a2 + b2 + a1 + b1 - min(b2, a1) ]
综合 ①②
- 最少时间:
[ T = a1 + b1 + a2 + b2 - max(min(a2, b1), min(a1, b2)) ]
贪心性质
因此可以得到一个贪心的性质:
- 对于 ( (a1, b1) ) 和 ( (a2, b2) ) 中的元素
- 若 ( min(a1, b2)<=min(a2, b1) ), 则 ( (a1, b1) ) 放在 ( (a2, b2) ) 前面执行。
- 反之,若 ( min(a2, b1)<=min(a1, b2) ), 则 ( (a2, b2) ) 在 ( (a1, b1) ) 前面执行。
问题分析
想象一个餐厅的厨房:
- 作业卡:就像菜单上的菜品,每个菜品需要不同的烹饪时间和装盘时间。
- M1和M2:就像两个厨师,M1是第一个厨师,负责烹饪;M2是第二个厨师,负责最后的装盘。
贪心算法的步骤:
- 分组:
- 把菜单上的菜品分成两类:
- N1:那些烹饪时间不比准备时间长的菜品,就像是简单快速的菜品。
- N2:烹饪时间比准备时间长的菜品,就像是复杂耗时的菜品。
这些菜品的烹饪时间比装盘时间要长。我们想要尽量减少第二个厨师(M2)的空闲时间,因为如果第二个厨师等待第一个厨师(M1)完成烹饪,这会造成时间上的浪费。
- 把菜单上的菜品分成两类:
即:将所有作业分为两组:
- 第一组(N1):作业在M1上的加工时间小于或等于在M2上的加工时间。
- 第二组(N2):作业在M1上的加工时间大于在M2上的加工时间。
- 排序:
- N1组的菜品按照烹饪时间从短到长排序,就像是把简单快速的菜品先做。
- N2组的菜品按照准备时间从长到短排序,但实际上我们先按准备时间从短到长排序,然后再反向上菜,也就是说,我们最后做那些装盘时间最长的菜品。
即:
- 对第一组作业按M1的加工时间进行递增排序。
- 对第二组作业按M2的加工时间进行递减排序。
- 执行顺序:
- 先上N1组的菜品,就像是先给顾客上简单快速的菜品。
- 然后上N2组的菜品,就像是在简单菜品后面上复杂耗时的菜品。
贪心思想的形象解释:
- 贪心选择:每次选择菜品时,我们都选择当前最“贪心”的菜品,即在N1组中,我们先做烹饪时间短的菜品,因为这样可以让顾客更快地吃到菜品;在N2组中,我们最后做准备时间长的菜品,因为这样可以减少厨师M2的空闲时间。
贪心算法 步骤
示例:N=5
先用johnson贪心算法求解下面5个作业的流水调度问题,写出johnson贪心算法步骤,再给出具体的求解过程,最后用代码实现。
(1)把所有作业按照M1、M2的时间分为两组,a[i]<=b[i]对应第一组N1,a[i]>b[i]对应第0组N2
这张表格的意思是:
- 编号1的作业 a[1]<=b[1],3<6,所以分到N1组,组号为1,看M1的时间,time=3
- 编号2的作业 a[2]>b[2],8>2,所以分到N2组,组号为0,看M2的时间,time=2
- 编号3的作业 a[3]<=b[3],4<10,所以分到N1组,组号为1,看M1的时间,time=4
- 编号4的作业 a[4]>b[4],9>2,所以分到N2组,组号为0,看M2的时间,time=7
- 编号5的作业 a[5]>b[5],6>5,所以分到N2组,组号为0,看M2的时间,time=5
(2)将N1(组号为1,a[i]<=b[i])的作业按照a[i]递增排序,N2(组号为0,a[i]>b[i])的作业按照b[i]递增排序
将N1(组号为1,a[i]<=b[i])的作业按照a[i]递增排序:
- 编号1 和编号3 在N1组(组号为1,a[i]<=b[i]) 的作业按照a[i]递增排序
- 编号1的M1时间 a[1]=3
- 编号3的M1时间 a[3]=4
- 由于 ( a[1] < a[3] ),编号1排在编号3前面。
N2(组号为0,a[i]>b[i])的作业按照b[i]递增排序:
- 编号2和编号4,编号5 在N2组(组号为0,a[i]>b[i]) 的作业按照b[i]递增排序
- 编号2的M2时间 b[2]=2
- 编号4的M2时间 b[4]=7
- 编号5的M2时间 b[5]=5
- 由于 ( 2<5<7),所以按照编号2,编号5,编号4排列。
(3)按顺序先执行N1的作业,再反序执行N2的作业,得到的就是耗时最少的最优调度方案。
最优调度下耗费的总时间计算
初始条件
- ( T1 = 0 ) (M1上的累计执行时间)
- ( T2 = 0 ) (M2上的累计执行时间)
计算步骤
对于最优调度方案 best
,用 i
扫描 best
的元素,计算 ( T1 ) 和 ( T2 ) 如下:
[ T1 = T1 + a[best[i]] ]
[ T2 = max{T1, T2} + b[best[i]] ]
示例计算
作业1
- ( T1 = 0 + 3 = 3 )
- ( T2 = max{0, 3} + 6 = 9 )
//在这里T1=3,T2=0
作业3
- ( T1 = 3 + 4 = 7 )
- ( T2 = max{7, 9} + 10 = 19 )
作业4
- ( T1 = 7 + 9 = 16 )
- ( T2 = max{16, 19} + 7 = 26 )
作业5
- ( T1 = 16 + 6 = 22 )
- ( T2 = max{22, 26} + 5 = 31 )
作业2
- ( T1 = 22 + 8 = 30 )
- ( T2 = max{30, 31} + 2 = 33 )
最终结果
在最优调度下消耗的最少总时间为:
[ T = 33 ]
代码:
#include<bits\stdc++.h> // 包含C++标准库头文件
using namespace std; // 使用标准命名空间
int N=5; // 定义作业数量
int a[5]={3,8,4,9,6}; // 定义每个作业在M1机器上的执行时间
int b[5]={6,2,10,7,5}; // 定义每个作业在M2机器上的执行时间
int best[5]; // 存储最优调度方案的数组
// 定义作业节点结构体
struct NodeType{
int no; // 作业编号
bool group; // 作业分组,根据a[i]<=b[i]分组
int time; // 作业在M1或M2上的最小执行时间
// 重载小于运算符,用于排序
bool operator<(const NodeType &s)const{
return time<s.time;
}
};
// 定义求解函数
int solve(){
int i,j,k; // 循环变量
NodeType c[N]; // 存储作业节点的数组
for(i=0;i<N;i++){ // 遍历所有作业
c[i].no=i; // 赋值作业编号
c[i].group=(a[i]<=b[i]); // 根据条件判断作业分组
c[i].time=a[i]<=b[i]?a[i]:b[i]; // 赋值作业在M1或M2上的最小执行时间
}
sort(c,c+N); // 根据time对作业节点数组进行排序
j=0;k=N-1; // 初始化索引
for(i=0;i<N;i++){ // 遍历排序后的作业节点数组
if(c[i].group==1) // 根据分组条件将作业编号放入best数组
best[j++]=c[i].no;
else
best[k--]=c[i].no;
}
int T1=0; // 初始化M1机器的累计执行时间
int T2=0; // 初始化M2机器的累计执行时间
for(i=0;i<N;i++){ // 遍历最优调度方案
T1+=a[best[i]]; // 累加M1机器的执行时间
T2=max(T1,T2)+b[best[i]]; // 计算M2机器的执行时间
}
return T2; // 返回最优调度总时间
}
// 主函数
int main(){
printf("最优调度总时间为:%d\n",solve()); // 输出最优调度总时间
printf("方案:\n"); // 输出方案提示
for(int i=0;i<N;i++){ // 遍历最优调度方案数组
printf("%d ",best[i]+1); // 输出作业编号(+1是为了符合常规的从1开始编号)
}
printf("\n"); // 输出换行符
return 0; // 返回0,表示程序正常结束
}
代码运行截图: