批处理作业调度【回溯算法】

  • 问题描述

      给定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上完成处理时,可以得到一种最佳调度,即使该作业调度的完成时间和最小。(证明略)

  • 问题解读

  1. 对于给定的n个作业,指定最佳作业调度方案,使其完成时间和最小,因此是求一个最优值
  2. 调度必须遵循:
    ① 一个作业必须先由机器1处理,再由机器2处理,顺序不可颠倒;
    ② 机器1处理n个作业的顺序必须和机器2处理n个作业的顺序相同(因为只有这样才能使作业调度的完成时间和最小)。
  3. 由于是一种作业调度顺序的方案,因此该问题的解空间树是排列树
  • 实例分析

如下图,给出了3个作业分别需要机器1和机器2的处理时间,试给出一种调度方案,使该作业调度的完成时间和最小。
批处理作业调度【回溯算法】_第1张图片
解: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调度为例)
批处理作业调度【回溯算法】_第2张图片
假设是从零点开始的:

  1. 作业1先在机器1上处理,处理完后紧接着在机器2上处理。对应红线部分,这时作业1在机器2上完成处理的时间为2, F 21 F{21} F21=3;
  2. 作业2 需要在机器1上完成的任务可以紧接着作业1在机器1上处理,但需要在机器2上完成的任务就要看作业2在机器1上完成处理的时间晚,还是作业1在机器2上完成处理的时间晚。如图,显然作业2 需要在机器1上完成的任务完成的时间较晚,因此作业2需要在机器1上完成后才能在机器2上继续完成。作业2的处理对应蓝线部分,这时作业2在机器2上完成处理的时间为2, F 22 F{22} F22=7;
  3. 同作业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 
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;
}
  • 运行结果

批处理作业调度【回溯算法】_第3张图片

你可能感兴趣的:(Hi!,Dasha,算法,dfs,c++)