题目描述
“加工顺序问题”又被称为“批处理作业调度问题”。
设有n个工件需要在机器M1和M2上加工,每个工件的加工顺序都是先在M1上加工,然后在M2上加工。t1j
,t2j
分别表示工件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<iostream>
#include<algorithm>
#include<math.h>
#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");
}
附:手动计算的部分调度结果
空:T({},x) = x 集合中无元素时 等待时间就是加工时间
作业编号 M1M2
1 2 5
2 4 2
3 3 3
4 6 1
5 1 7
T({1},0) = 2+T({},5) = 2+5 = 7
T({2},0) = 4+T({},2) = 4+2 = 6
T({3},0) = 3+T({},3) = 3+3 = 6
T({4},0) = 6+T({},1) = 6+1 = 7
T({5},0) = 1+T({},7) = 1+7 = 8
T({1},0) = 2+T({},5+(0-2) = 2+5 = 7
T({1},1) = 2+T({},5+(1-2) = 2+5 = 7
T({1},2) = 2+T({},5+(2-2) = 2+5 = 7
T({1},3) = 2+T({},5+(3-2) = 2+6 = 8
T({1},4) = 2+T({},5+(4-2) = 2+7 = 9
T({1},5) = 2+T({},5+(5-2) = 2+8 = 10
T({2},0) = 4+T({},2+(0-4) = 4+2 = 6
T({2},1) = 4+T({},2+(1-4) = 4+2 = 6
T({2},2) = 4+T({},2+(2-4) = 4+2 = 6
T({2},3) = 4+T({},2+(3-4) = 4+2 = 6
T({2},4) = 4+T({},2+(4-4) = 4+2 = 6
T({2},5) = 4+T({},2+(5-4) = 4+3 = 7
T({2},6) = 4+T({},2+(6-4) = 4+4 = 8
T({3},0) = 3+T({},3+(0-3) = 3+3 = 6
T({3},1) = 3+T({},3+(1-3) = 3+3 = 6
T({3},2) = 3+T({},3+(2-3) = 3+3 = 6
T({3},3) = 3+T({},3+(3-3) = 3+3 = 6
T({3},4) = 3+T({},3+(4-3) = 3+4 = 7
T({3},5) = 3+T({},3+(5-3) = 3+5 = 8
T({3},6) = 3+T({},3+(6-3) = 3+6 = 9
T({3},7) = 3+T({},3+(7-3) = 3+7 = 10
T({3},8) = 3+T({},3+(8-3) = 3+8 = 11
T({4},0) = 6+T({},1+(0-6) = 6+1 = 7
T({4},1) = 6+T({},1+(1-6) = 6+1 = 7
T({4},2) = 6+T({},1+(2-6) = 6+1 = 7
T({4},3) = 6+T({},1+(3-6) = 6+1 = 7
T({4},4) = 6+T({},1+(4-6) = 6+1 = 7
T({4},5) = 6+T({},1+(5-6) = 6+1 = 7
T({4},6) = 6+T({},1+(6-6) = 6+1 = 7
T({4},7) = 6+T({},1+(7-6) = 6+2 = 8
T({4},8) = 6+T({},1+(8-6) = 6+3 = 9
T({5},0) = 1+T({},7+(0-1) = 1+7 = 8
T({5},1) = 1+T({},7+(1-1) = 1+7 = 8
T({5},2) = 1+T({},7+(2-1) = 1+8 = 9
T({5},3) = 1+T({},7+(3-1) = 1+9 = 10
T({5},4) = 1+T({},7+(4-1) = 1+10 = 11
T({5},5) = 1+T({},7+(5-1) = 1+11 = 12
T({5},6) = 1+T({},7+(6-1) = 1+12 = 13
T({1,2},0) = min i={1}: 2+T({2},5) = 2+7 = 9
i={2}: 4+T({1},2) = 4+7 = 11
= 9
T({1,2},3) = min i={1}: 2+T({2},5+(3-2)) = 2+T({2},6) = 2+8 = 10
i={2}: 4+T({1},2+(3-4)) = 4+T({1},2) = 4+7 = 11
= 10
T({2,3},0) = min i={2}: 4+T({3},2) = 4+6 = 10
i={3}: 3+T({2},3) = 3+6 = 9
= 9
T({3,4},0) = min i={3}: 3+T({4},3) = 3+7 = 10
i={4}: 6+T({3},1) = 6+6 = 12
= 10
T({4,5},0) = min i={4}: 6+T({5},1) = 6+8 = 14
i={5}: 1+T({4},7) = 1+8 = 9
= 10
T({2,3},5) = min i={2}: 4+T({3},2+(5-4)) = 4+6 = 10
i={3}: 3+T({2},3+(5-3)) = 3+7 = 10
= 10
T({1,3},2) = min i={1}: 2+T({3},5+(5-2)) = 2+T({3},8) = 2+11=12
i={3}: 3+T({1},3+(5-3)) = 3+T({1},5) = 3+10=13
= 12
T({1,2,3},0) = min i={1}: 2+T({2,3},5) = 2+10 = 12
i={2}: 4+T({1,3},2) = 4+12 = 16
i={3}: 3+T({1,2},3) = 3+11 = 14
= 12