贪心算法-流水作业调度问题

流水作业调度问题

问题描述

在流水线作业中,我们有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是第二个厨师,负责最后的装盘。

贪心算法的步骤:

  1. 分组
    • 把菜单上的菜品分成两类:
      • N1:那些烹饪时间不比准备时间长的菜品,就像是简单快速的菜品。
      • N2:烹饪时间比准备时间长的菜品,就像是复杂耗时的菜品。
        这些菜品的烹饪时间比装盘时间要长。我们想要尽量减少第二个厨师(M2)的空闲时间,因为如果第二个厨师等待第一个厨师(M1)完成烹饪,这会造成时间上的浪费。

即:将所有作业分为两组:

  • 第一组(N1):作业在M1上的加工时间小于或等于在M2上的加工时间。
  • 第二组(N2):作业在M1上的加工时间大于在M2上的加工时间。
  1. 排序
    • N1组的菜品按照烹饪时间从短到长排序,就像是把简单快速的菜品先做。
    • N2组的菜品按照准备时间从长到短排序,但实际上我们先按准备时间从短到长排序,然后再反向上菜,也就是说,我们最后做那些装盘时间最长的菜品。

即:

  • 对第一组作业按M1的加工时间进行递增排序。
  • 对第二组作业按M2的加工时间进行递减排序。
  1. 执行顺序
    • 先上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. 编号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]递增排序:

  1. 编号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,表示程序正常结束
}

代码运行截图:
在这里插入图片描述

  • 24
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值