问题:

n个作业 N={1,2,…,n}要在2台机器M1和M2组成的流水线上完成加工。每个作业须先在M1上加工,然后在M2上加工。M1和M2加工作业 i 所需的时间分别为 ai 和bi,每台机器同一时间最多只能执行一个作业。

流水作业调度问题要求确定这n个作业的最优加工顺序,使得所有作业在两台机器上都加工完成所需最少时间。最优调度应该是:

  1. 使M1上的加工是无间断的。即M1上的加工时间是所有ai之和,但M2上不一定是bi之和。
  2. 使作业在两台机器上的加工次序是完全相同的。
  3. 算法:流水作业调度_流水作业调度

  4. 则得结论:仅需考虑在两台机上加工次序完全相同的调度。
    最优子解结构
  5. 算法:流水作业调度_i++_02

  6. 机器M1开始加工S中作业时,机器M2还在加工其他作业,要等时间 t 后才可利用,则:

1. 则完成S中作业所需的最短时间记为T(S,t)

2. 完成所有作业所需的最短时间为T(N,0)

3. T(N,0)=min{ai + T(N-{i}, bi)}, i∈N。

ai:选一个作业i先加工,在M1的加工时间。

T(N-{i},bi}:剩下的作业要等bi时间后才能在M2上加工。注意这里函数的定义,因为一开始工作i是随机取的,M1加工完了ai之后,要开始加工bi了,这里M1是空闲的可以开始加工剩下的N-i个作业了,但此时M2开始加工bi,所以要等bi时间之后才能重新利用,对应到上面函数T(s,t)的定义的话,这里就应该表示成T(N-{i},bi), 所以最优解可表示为T(N,0)=min{ai + T(N-{i}, bi)}, i∈N,即我们要枚举所有的工作i,使这个式子取到最小值。这里顺便吐槽一句:算法中会利用很多数学知识,一定要搞清楚函数的意义以及每个数学符号所代表的含义,这样不至于各种懵比。继续分析T(S,t)可得:

T(S,t)={ai + T(S-{i}, bi+max{t-ai,0})}, i∈S

其中:T(S-{i}, bi+max{t-ai,0}):剩下的作业等bi+max{t-ai,0}才能在M2加工,至于这里是怎么推导出来的呢?见下面推导:

  

算法:流水作业调度_最优解_03


   最优子结构的证明(问题最优解包括子问题最优解):

最优子结构:设π是N的一个最优调度,其加工顺序为π1,…, πn,其所需的加工时间为 aπ1+T’(即第一个作业π1在M1上加工的时间和其它的加工时间)。记S=N-{π1},则T’=T(S, bπ1)。

证明:由T的定义知T(S, bπ1)是对S最优的,故T’>=T(S, bπ1)。若T’>T(S, bπ1),设π’是作业集S在机器M2的等待时间为bπ1情况下的一个最优调度。则π1,π’2, …,π’n是N的一个调度,且该调度所需的时间为aπ1+T’ > aπ1+T(S, bπ1)。这与π是N的最优调度矛盾。故T’<=T(S, bπ1), 从而T’=T(S, bπ1)。最优子结构的性质得证。

分析:

这段证明我开始有点云里雾里的,简单来说就是要证明问题的最优解包含子问题的最优解就行了,那么这里的证明思路是先假设一个最优调度,对于他的子调度T’,因为T(S,t)被定义为是完成S中作业所需的最短时间记为T(S,t),所以有T’>=T(S, bπ1),那么如果这个子调度这里不是最优解的话即T’>T(S, bπ1),会得出aπ1+T’ > aπ1+T(S, bπ1)即原来假设的最优调度不符和最优调度的标准,矛盾,从而推出 T’是一定等于T(S, bπ1),即这个子调度也是最优调度。

问题是:虽然满足最优子结构性质,也在一定程度满足子问题重叠性质。N的每个非空子集都计算一次,共2n-1次,指数级的。

算法:流水作业调度_流水作业调度_04


为了解决这个问题引入Johnson不等式:

算法:流水作业调度_流水作业调度_05


算法:流水作业调度_最优解_06


算法:流水作业调度_最优解_07

结论:先把所有作业的ai和bi放在一起,
从这之中选个最小的,
如果是bi的话这个作业i就放最后
,如果是ai的话这个作业就放最前,
把这个已经安排好的作业从作业集中删除。
重复上述步骤即可。

源代码:

#include<bits/stdc++.h>
using namespace std;
const int N = 5;

class Jobtype
{
public:
int key,index;//key为机器所用时间,index为作业序号
bool job;//job为ture表示M1,false表示M2
inline bool operator <(const Jobtype &a) const //重载 <=
{
return(key<a.key);
}
};

int FlowShop(int n,int a[],int b[],int c[]);

int main()
{
int a[] = {2,4,3,6,1};
int b[] = {5,2,3,1,7};
int c[N];

int minTime = FlowShop(N,a,b,c);

cout<<"作业在机器1上的运行时间为:"<<endl;
for(int i=0; i<N; i++)
{
cout<<a[i]<<" ";
}
cout<<endl;
cout<<"作业在机器2上的运行时间为:"<<endl;
for(int i=0; i<N; i++)
{
cout<<b[i]<<" ";
}
cout<<endl;

cout<<"完成作业的最短时间为:"<<minTime<<endl;
cout<<"编号从0开始,作业调度的顺序为:"<<endl;
for(int i=0; i<N; i++)
{
cout<<c[i]<<" ";
}
cout<<endl;
return 0;
}

int FlowShop(int n,int a[],int b[],int c[])
{
Jobtype *d = new Jobtype[n];
for(int i=0; i<n; i++)
{
d[i].key = a[i]>b[i]?b[i]:a[i];//按Johnson法则分别取对应的b[i]或a[i]值作为关键字
//找作业在两台机器上处理时间小的那个作业
d[i].job = a[i]<=b[i];//给符合条件a[i]<b[i]的放入到N1子集标记为true
d[i].index = i;
}

sort(d,d+n);//对数组d按关键字升序进行排序 快排

int j = 0,k = n-1;

for(int i=0; i<n; i++)
{
if(d[i].job)//N1集合,ai<=bi
{
c[j++] = d[i].index;//将排过序的数组d,取其中作业序号属于N1的从前面进入
}
else//N2集合,ai>bi
{
c[k--] = d[i].index;//属于N2的从后面进入,从而实现N1的非减序排序,N2的非增序排序
}
}

j = a[c[0]];//第一个作业在M1上的处理时间
k = j+b[c[0]];//第一个作业处理完所需时间
for(int i=1; i<n; i++)
{
j += a[c[i]];//M1在执行c[i]作业的同时,M2在执行c[i-1]号作业,最短执行时间取决于M1与M2谁后执行完
k = j<k?k+b[c[i]]:j+b[c[i]];//计算最优加工时间
}

delete d;
return k;
}

运行效果:

算法:流水作业调度_最优解_08