【算法设计与分析】流水作业调度问题 动态规划算法

题目描述

“加工顺序问题”又被称为“批处理作业调度问题”。

设有n个工件需要在机器M1和M2上加工,每个工件的加工顺序都是先在M1上加工,然后在M2上加工。t1jt2j分别表示工件j在M1,M2上所需的加工时间(j=1,2,···,n)

问:应如何在两机器上安排生产,使得第一个工件从在M1上加工开始到最后一个工件在M2上加工完所需的总加工时间最短?关于此(类)问题的回溯法求解被作为经典案例在很多教材或参考文献中出现,现要求设计求解此问题的动态规划算法

请用数学语言对“加工顺序问题”加以抽象,在此基础上给出动态规划求解该问题的递归公式。要求对所给公式中的符号意义加以详细说明,并简述算法求解步骤。用一种你熟悉的程序设计语言加以实现。

动态规划思路

流水作业调度问题要求确定这n个作业的最优加工顺序,使得从第一个作业在机器M1上开始加工,到最后一个作业在机器M2上加工完成所需的时间最少。

直观上,一个最优调度应使机器M1没有空闲时间,且机器M2的空闲时间最少。在一般情况下,机器M2上会有机器空闲和作业积压两种情况。

设全部作业的集合为N={1,2,...,n}S⊆N,S是N的一个子集。在一般情况下,机器M1开始加工S中作业时,机器M2还在加工其他作业,要等时间t后才可利用。这种情况下,完成S中作业所需的最短时间记为T(S,t),流水作业调度问题的最优值为T(N,0)
在这里插入图片描述
证明最优子结构性质

因为公式手写比较方便,所以暂时使用手写,待转电子版。
在这里插入图片描述

递归计算最优值

由流水作业最优子结构的性质可知,全部作业集合N的最优调度时间T(N,0)计算方式如下:

  • 注意:作业i是从全部作业集合中任意抽取的,作业i被作为第一个加工的作业。


推广到一般情形下,便有递归函数:
在这里插入图片描述
max{t-ai,0} 这一项是由于:在作业i完成M1上的加工,该转到M2上加工的时候,M2的状态可能是空闲/占用

  • 如果作业i在M1上加工完毕后,M2是空闲的,则不需要等待,M2直接加工作业i即可。作业i占用M2的时间为bi+0
  • 如果作业i在M1上加工完毕后,M2仍然被之前的作业占用,则作业i在M2上需要等待的时间为t-ai。作业i占用M2的时间为bi+(t-ai)

上述递归函数的更详细的解释如下图:
在这里插入图片描述
为了便于理解,手动走一下递归↓(更完整的手动递归过程,见页面最下附录)
在这里插入图片描述
然后就是代码了。关于如何存储求解的小问题,避免重复运算,下面是思考过程。

脑洞大开过程↓

一开始,我想模仿矩阵连乘问题求解,用二维数组表示被加工的工件,横坐标表示工件的起点,纵坐标表示工件的终点。但是发现本问题中的工件不连续(5个工件就有32种不同的组合),无法用二维数组表示。

既然工件组合不连续,我又考虑模仿0-1背包问题的解法求解。0-1背包是建立一个二维数组,横坐标是背包的容量,纵坐标是可以装入的物品编号。然后从装入物品编号为0开始,将将背包容量从0开始,一点一点扩大,计算能装入的最大的物品价值。当可装入的物品编号为总物品个数的情况下,当背包容量达到实际容量时,能装入物品的最大价值就是整个问题的最优解。
但是流水作业调度问题和背包问题差别仍然很大。
1、交换0-1背包问题的放入顺序,最优解仍然是最优解,而交换流水作业的加工顺序,最优解可能就不是最优了。
2、0-1背包息壤得到装入的最大价值;流水作业希望得有最少的加工时间。
于是放弃模仿0-1背包问题。

考虑后发现,流水线作业调度问题具有两个特点:
1、子问题的工件不连续,可跳跃
2、一般情况下,交换两个工件的加工顺序后,总加工时间会改变。

又受到某五子棋人机对战算法的启发(此处省略五子棋算法),具体是将每种赢的状态用0和1表示出来(整个棋盘二维数组上只有5个连续的1,其余全是0,可以表示其中一种赢的状态)。

类比可得,本流水线调度问题也可以用0和1表示所有可能的工件集合(每一个工件被加工或不被加工的状态)。

比如说有5个工件,每一个工件都可能被加工或不被加工,因此共有2^5=32种状态(组合方式)

又考虑到在每种状态下,如果想要求最小加工时间,还要知道该状态下机器M2上的等待时间。于是建立一个类JiHe,实例化为JiHe[32],里面设置minTime[]属性,存储在当前组合方式下,等待时间为数组下标时的最短加工时间。

这样,就成功的存储了子问题的最优解。求解顺序就是:从加工零个工件开始填写minTime[],一直到加工完所有工件。

代码思路+详细运行过程

首先,我们假设有5个工件待加工,分别为:J0,J1,J2,J3,J4,J5
它们在两个机器上加工时间如下:
在这里插入图片描述
1、定义一个类GongJian,记录每个工件两个加工步骤分别需要的时间(程序运行时用户输入)。
在这里插入图片描述
输入时间后
在这里插入图片描述
2、定义一个类JiHe,表示一种状态(组合方式),表示有哪些工件被加工。在这里插入图片描述
用类似于二进制加一的方式,填写所有可能的工件组合。比如,5个工件有2^5=32种组合方式。
在这里插入图片描述
之后遍历32种工件组合JiHe[32],把每种组合下,被加工的总工件个数保存在jihe[i].num中,此操作便于后续根据总工件个数(0,1,2,3,4,5),按顺序进行自底向上计算。

3、自底向上,计算每个状态的最短时间

  • 先计算0个工件被加工时,等待时间为0,1,2,3,…,100时的最短加工时间
  • 再计算1个工件被加工时,等待时间为0,1,2,3,…,100时的最短加工时间
  • 最后计算5个工件被加工时,等待时间为0,1,2,3,…,100时的最短加工时间

5个工件被加工时,等待时间为0时的最短加工时间即为整个流水作业调度问题的解。
在这里插入图片描述
以下为逐步调试过程:

初始状态下,数组值都为0↓
在这里插入图片描述
0个工件被加工时,等待时间为0,1,2,3,…,100时的最短加工时间↓
在这里插入图片描述
1个工件被加工时,等待时间为0,1,2,3,…,100时的最短加工时间↓
在这里插入图片描述
2个工件被加工时,等待时间为0,1,2,3,…,100时的最短加工时间↓
在这里插入图片描述
3个工件被加工时,等待时间为0,1,2,3,…,100时的最短加工时间↓
在这里插入图片描述
4个工件被加工时,等待时间为0,1,2,3,…,100时的最短加工时间↓
在这里插入图片描述
5个工件被加工时,等待时间为0,1,2,3,…,100时的最短加工时间(至此,全部计算完成)↓
在这里插入图片描述
此时,jihe[31].minTime[0]存放的就是加工完全部工件所需的最短时间,也就是在加工5个工件,且M2等待时间为0时,加工所需要的最短时间(最优解)。

最后将此时间输出即可。

测试用例

注意:使用时,要在代码的宏定义中修改工件总数NUM,手动计算并填写POWNUM = 2^NUM
如下图所示,工件总数为6,2^6=64,因此宏定义NUM=6 POWNUM=64
在这里插入图片描述
输入1

2 5 7 3 6 2 4 7 6 9 8 2

输出1

最短时间:35

输入2

2 5 4 2 3 3 6 1 1 7

输出2

最短时间:19

运行效果

在这里插入图片描述

代码

工件总数根据宏定义可变,需要在运行前,填写宏定义的NUM POWNUM
各个工件在机器上的加工时间运行时由用户录入

#include
#include
#include
#define NUM 5		//工件总数
#define POWNUM 32	//2^NUM 状态总数

using namespace std;

//2 5 7 3 6 2 4 7 6 9 8 2
//答案:35

//2 5 4 2 3 3 6 1 1 7
//答案:19

class JiHe
{
public:
	int a[NUM];			//1:被加工 0:不被加工
	int num;			//当前状态下 被加工的工件个数
	int minTime[100];	//数组下标是等待时间t
};

class GongJian
{
public:
	int t1;				//该工件在 M1 上加工需要的时间
	int t2;				//该工件在 M2 上加工需要的时间
};

GongJian gongjian[NUM];
JiHe jihe[POWNUM];

//寻找最小时间的递归函数
int findMinTime(JiHe jihe, int t)
{
	if (jihe.num == 0)			//集合中无元素时 等待时间就是加工时间
	{
		return t;
	}

	int i;
	int curMinTime;
	int mintime = 1000;			//初始化巨大值
	JiHe withoutI = jihe;

	for (i = 0; i < NUM; i++)	//i放在第一个加工 循环找到最小值情况下的i 但是i没有被记录
	{
		withoutI = jihe;
		if (jihe.a[i] == 1)
		{
			withoutI.a[i] = 0;
			withoutI.num = jihe.num - 1;
			curMinTime = gongjian[i].t1 + findMinTime(withoutI, gongjian[i].t2 + max(t - gongjian[i].t1, 0));
			if (curMinTime < mintime)
			{
				mintime = curMinTime;
			}
		}
	}
	return mintime;
}


int main()
{
	//每个工件的时间状况
	cout << "请输入工件在机器M1 M2上的加工时间:\n";
	for (int i = 0; i < NUM; i++)
	{
		cout << "工件序号" << i << "的加工时间\n";
		cout << "t1 = ";
		cin >> gongjian[i].t1;
		cout << "t2 = ";
		cin >> gongjian[i].t2;
	}



	//填写每个工件在与不在的状态 32个
	int i;
	int x[NUM];
	for (int i = 0; i < NUM; i++)//用于二进制计数
	{
		x[i] = 1;
	}
	int num;
	int t, p;
	for (i = 0; i < POWNUM; i++)
	{
		//二进制计数 罗列所有工件组合
		for (t = NUM - 1, p = 0; t >= 0; t--, p++)
		{
			if ((i % (int)pow(2, t)) == 0)
			{
				x[p] = -x[p];
			}
		}
		/*if (i % 32 == 0)x[0] = -x[0];
		if (i % 16 == 0)x[1] = -x[1];
		if (i % 8 == 0)x[2] = -x[2];
		if (i % 4 == 0)x[3] = -x[3];
		if (i % 2 == 0)x[4] = -x[4];
		if (i % 1 == 0)x[5] = -x[5];*/

		for (t = 0; t < NUM; t++)//把-1改成0
		{
			jihe[i].a[t] = (x[t] > 0 ? x[t] : 0);
		}
		/*jihe[i].a[0] = (x[0] > 0 ? x[0] : 0);
		jihe[i].a[1] = (x[1] > 0 ? x[1] : 0);
		jihe[i].a[2] = (x[2] > 0 ? x[2] : 0);
		jihe[i].a[3] = (x[3] > 0 ? x[3] : 0);
		jihe[i].a[4] = (x[4] > 0 ? x[4] : 0);
		jihe[i].a[5] = (x[5] > 0 ? x[5] : 0);*/

		//填写当前状态下有几个工件被加工 num
		num = 0;
		for (int j = 0; j < NUM; j++)
		{
			if (jihe[i].a[j] == 1)num++;
		}
		jihe[i].num = num;
	}

	//自底向上 计算每个状态的最短时间
	int k;
	int workNum;
	for (workNum = 0; workNum <= NUM; workNum++)
	{
		// 0,1,2,...,workNum 个工件被加工
		for (i = 0; i < POWNUM; i++)
		{
			if (jihe[i].num == workNum)
			{
				//等待时间k
				for (k = 0; k < 100; k++)
				{
					jihe[i].minTime[k] = findMinTime(jihe[i], k);
				}
			}
		}
	}

	cout << "最短时间:" << jihe[POWNUM - 1].minTime[0] << endl;
	system("pause");
}

原文链接

你可能感兴趣的:(算法,动态规划,数据结构)