给定n个作业的集合 J=( J 1 J_1 J1, J 2 J_2 J2,……, J n J_n Jn)。每个作业 J i J_i Ji都有两项任务分别在两台机器上完成。每个作业必须先由机器1处理,再由机器2处理。作业 J i J_i Ji需要机器j的处理时间为 t i j t_{ij} tij(i=1,2,……,n;j=1,2)。对于一个确定的作业调度,设 F j i F_{ji} Fji是作业i在机器j上完成处理的时间,则所有作业在机器2上完成处理的时间和f = ∑ i = 1 n F 2 i \sum_{i=1}^{n}{F_{2i}} ∑i=1nF2i称为该作业调度的完成时间和。可以证明,当作业以相同次序在机器1和机器2上完成处理时,可以得到一种最佳调度,即使该作业调度的完成时间和最小。(证明略)
- 对于给定的n个作业,指定最佳作业调度方案,使其完成时间和最小,因此是求一个最优值。
- 调度必须遵循:
① 一个作业必须先由机器1处理,再由机器2处理,顺序不可颠倒;
② 机器1处理n个作业的顺序必须和机器2处理n个作业的顺序相同(因为只有这样才能使作业调度的完成时间和最小)。 - 由于是一种作业调度顺序的方案,因此该问题的解空间树是排列树。
如下图,给出了3个作业分别需要机器1和机器2的处理时间,试给出一种调度方案,使该作业调度的完成时间和最小。
解:3个作业可能的调度顺序有6种:1→2→3、1→3→2、2→1→3、2→3→1、3→1→2、3→2→1。经过计算,它们的完成时间和对应为:19、18、20、21、19、19,因此最佳调度方案为1→3→2,其完成时间和为18。
解析:现在就来分析一下完成时间和怎样计算(以1→3→2调度为例)
假设是从零点开始的:
- 作业1先在机器1上处理,处理完后紧接着在机器2上处理。对应红线部分,这时作业1在机器2上完成处理的时间为2, F 21 F{21} F21=3;
- 作业2 需要在机器1上完成的任务可以紧接着作业1在机器1上处理,但需要在机器2上完成的任务就要看作业2在机器1上完成处理的时间晚,还是作业1在机器2上完成处理的时间晚。如图,显然作业2 需要在机器1上完成的任务完成的时间较晚,因此作业2需要在机器1上完成后才能在机器2上继续完成。作业2的处理对应蓝线部分,这时作业2在机器2上完成处理的时间为2, F 22 F{22} F22=7;
- 同作业2的处理情况,作业3 需要在机器1上完成的任务可以紧接着作业2在机器1上处理,但 需要在机器2上完成的任务就要看作业3在机器1上完成处理的时间晚,还是作业2在机器2上完成处理的时间晚。如图,显然作业3 需要在机器1上完成的任务与需要在机器2上完成的任务的完成时间相同,因此作业3在机器1上完成后可以紧接着在机器2上处理。作业3的处理对应绿线部分,这时作业3在机器2上完成处理的时间为2, F 23 F{23} F23=8;
综上,作业在机器2上完成处理的时间和为:f = ∑ i = 1 3 F 2 i \sum_{i=1}^{3}{F_{2i}} ∑i=13F2i = F 21 F{21} F21+ F 22 F{22} F22+ F 23 F{23} F23 = 18。
注意:这里要强调的是应该如何判断某个作业需要在机器2上完成的任务何时开始,这个节点是上一个作业在机器2上完成处理的时间和本作业在机器1上完成处理的时间的较大者,具体原因已经在解析中详解,这里不再做赘述。
#include <iostream>
using namespace std;
int n; //作业数
int M[100][100]; //M[i][j]表示第i个作业在机器j上需要处理的时间
int x[100]; //x[i]表示第i个处理的作业为x[i]
int bestx[100]; //x[i]的最优值
int f1; //作业在机器1上完成处理的时间
int f2[100]; //f2[i]表示第i个作业在机器2上完成处理的时间
int f; //用于记录前i个作业在机器2上完成处理的时间和
int bestf; //f的最优值
void Swap(int &a,int &b) //交换函数
{
int temp;
temp=a;
a=b;
b=temp;
}
void Backtrack(int i)
{
if(i>n) //每到达一个叶子结点,一定是产生了一个最优解,因此要更新之前最优解的值
{
if(f<bestf) //更新最优解
{
for(int j=1;j<=n;j++)
bestx[j]=x[j];
bestf=f;
}
}
else
{
for(int j=i;j<=n;j++) //控制展开i-1层结点的各个分支。例如当i=1时,表示在整棵排列树的根结点处,刚要开始探索结点,这时可以展开的分支有1、2、3……
{
f1+=M[x[j]][1]; //计算第i层(个)作业在机器1上的完成处理的时间
if(f2[i-1]>f1) //如果第(i-1)个作业在机器2上的完成处理的时间大于第i个作业在机器1上的完成处理的时间,那么第i个作业想进入机器2,就要等第(i-1)个作业在机器2上完成后再说
f2[i]=f2[i-1]+M[x[j]][2];
else //否则,第i个作业可以在机器1上完成处理后直接进入机器2。
f2[i]=f1+M[x[j]][2];
f+=f2[i]; //计算完第i个作业在机器2上的完成处理的时间,就可以计算出前i个作业在机器2上完成处理的时间和了
if(f<bestf) //截止到这,已经得到一个前i个作业在机器2上完成处理的时间和f,如果f比之前记录的前i个作业在机器2上的完成处理的时间和的最优值bestf都要小,就可以生成第i层结点的孩子结点,继续探索下一层
{
Swap(x[i],x[j]); //把处于同一层的并且使f更小的那个结点拿过来,放到正在探索的这个结点处(这里结合排列数的图可以更好地理解)
Backtrack(i+1); //继续探索以第i层结点为根结点的子树
Swap(x[i],x[j]); //探索完要回溯时,只要做探索前的反“动作”就可以了
}
f-=f2[i]; //探索完要回溯时,只要做探索前的反“动作”就可以了
f1-=M[x[j]][1]; //探索完要回溯时,只要做探索前的反“动作”就可以了
}
}
}
void inPut() //输入函数
{
cout<<"请输入作业的个数n:"<<endl;
cin>>n;
cout<<"请分别输入n个作业在机器1和机器2上各自需要处理的时间(分两行):"<<endl;
for(int i=1;i<=2;i++)
{
for(int j=1;j<=n;j++)
cin>>M[j][i];
}
}
void initialize() //初始化函数
{
for(int i=1;i<=n;i++)
x[i]=i; //初始化当前作业调度的一种排列顺序
bestf=10000; //此问题是得到最佳作业调度方案以便使其完成处理时间和达到最小,所以当前最优值bestf应该初始化赋值为较大的一个值
}
void outPut() //输出函数
{
cout<<"这"<<n<<"个作业的最佳调度顺序为:"<<endl;
for(int i=1;i<=n;i++)
cout<<bestx[i]<<" ";
cout<<endl;
cout<<"该作业调度的完成时间和为:"<<endl;
cout<<bestf<<endl;
}
int main()
{
inPut();
initialize();
Backtrack(1);
outPut();
return 0;
}