题目描述

我们现在要利用m台机器加工n个工件,每个工件都有m道工序,每道工序都在不同的指定的机器上完成。每个工件的每道工序都有指定的加工时间。
每个工件的每个工序称为一个操作,我们用记号j-k表示一个操作,其中j为1到n中的某个数字,为工件号;k为1到m中的某个数字,为工序号,例如2-4表示第2个工件第4道工序的这个操作。在本题中,我们还给定对于各操作的一个安排顺序。
例如,当n=3,m=2时,“1-1,1-2,2-1,3-1,3-2,2-2”就是一个给定的安排顺序,即先安排第1个工件的第1个工序,再安排第1个工件的第2个工序,然后再安排第2个工件的第1个工序,等等。
一方面,每个操作的安排都要满足以下的两个约束条件。
(1) 对同一个工件,每道工序必须在它前面的工序完成后才能开始;
(2) 同一时刻每一台机器至多只能加工一个工件。
另一方面,在安排后面的操作时,不能改动前面已安排的操作的工作状态。
由于同一工件都是按工序的顺序安排的,因此,只按原顺序给出工件号,仍可得到同样的安排顺序,于是,在输入数据中,我们将这个安排顺序简写为“1 1 2 3 3 2”。
还要注意,“安排顺序”只要求按照给定的顺序安排每个操作。不一定是各机器上的实际操作顺序。在具体实施时,有可能排在后面的某个操作比前面的某个操作先完成。
例如,取n=3,m=2,已知数据如下:
则对于安排顺序“1 1 2 3 3 2”,下图中的两个实施方案都是正确的。但所需要的总时间分别是10与12。

当一个操作插入到某台机器的某个空档时(机器上最后的尚未安排操作的部分也可以看作一个空档),可以靠前插入,也可以靠后或居中插入。为了使问题简单一些,我们约定:在保证约束条件(1)(2)的条件下,尽量靠前插入。并且,我们还约定,如果有多个空档可以插入,就在保证约束条件(1)(2)的条件下,插入到最前面的一个空档。于是,在这些约定下,上例中的方案一是正确的,而方案二是不正确的。
显然,在这些约定下,对于给定的安排顺序,符合该安排顺序的实施方案是唯一的,请你计算出该方案完成全部任务所需的总时间。

输入描述:

第1行为两个正整数,用一个空格隔开:m n(其中m表示机器数,n表示工件数)
第2行:m*n个用空格隔开的数,为给定的安排顺序。
接下来的2n行,每行都是用空格隔开的m个正整数,每个数不超过20。
其中前n行依次表示每个工件的每个工序所使用的机器号,第1个数为第1个工序的机器号,第2个数为第2个工序机器号,等等。
后n行依次表示每个工件的每个工序的加工时间。

输出描述:

输出一个正整数,为最少的加工时间。

示例1

输入
2 3
1 1 2 3 3 2
1 2 
1 2 
2 1
3 2 
2 5 
2 4

输出
10

备注

m < 20,n < 20

解答

这是目前为止我所做过的难度最大的模拟题。一般来说,模拟题的题解文字说明都比较少,因为代码具体什么意思大家一般都能看明白,但这道题不太一样,所以我打算写的稍微多一些。

这类长模拟代码看的时间久了对一些变量可能有记忆混淆的事情发生,所以我在主要过程基本没使用简单的单字母或者双字母命名变量,那样会严重丧失可读性。

我们来分析一下题意。

题目已经给出了安排好的工序,而每个工序需要在几号机上完成以及每个工序的时间也给了出来,我们要做的就是合理安排机器的工作,让总的加工时间最短。

按照题意的约定,最短方案有且只有一种,而且不必判断输入的合法性。

我们可以把机器想成若干个「时间线」,在这条时间线上去安排工作。

那么明显的,每个时间段对应的机器就只有俩状态:

1.我在干活

2.我闲着呢

而每一个工件也有自己的加工要求,对于每个工件的工序,总应该先完成小号工序再完成大号工序,也就是必须顺着编号来。

每台机器只能在某时刻进行一种工作,并且后面的安排不能把前面的安排改动掉。

模拟的思想便是从左到右无限扫描整个时间线,然后去尝试插空。

这里有三个辅助数组,如果难理解它们的作用将对我的代码有理解困难。第一个是cnt_now_work_step,它表示当前取到工件的工序数。根据之前输入的workline,每个数都代表一个安排的工序,这个数组就是用来方便后面处理工序的,尽管它名字比较长。第二个是lasttime,它代表某个工件出现的最晚的时间(点),它可以用来方便我们扫描时间线,因为每一个工件必须要完全完成上一道工序后才能接着继续下一道工序。第三个是二维bool数组timeline,它代表某一台机器在某一个时间(点)上是不是正在干活。

有了这三个辅助数组,我们就可以开始按照模拟的思路写代码了。

我们取当前工件nowitem[i],让cnt_now_work_step[nowitem]++,即代表这个工件的工序+1,用nownumber记录当前工件在当前工序时位于哪一台机器,costtime表示做完这道工序应该花费的时间,lasttime[nowitem]+1便是我们扫描时间线的开端,注意lasttime记录的是时间点。这个for没有终止条件,因为时间轴可能会无穷远。

接下来是关键,判断从这个时间点到干完这道工序,机器有没有空,如果机器表示“我闲着呢”,那么就把这道工序安排给机器的这个时间段,更新timeline和lasttime(lasttime[nowitem] = time + costtime - 1,干完活之后这个工件出现的最晚的时间点应该是这道新工序做完的那一刻),然后更新操作立即break掉,继续扫描时间线。如果机器表示“这个时间段我在干其他活”,那这个任务就不能放在这一段,时间线继续扫描。

这个判断要如何写?我用了一个函数,它传入起始时间点和终止时间点和工件编号,然后去判断它的timeline就好。

循环完所有的工件,整个的时间轴也就确定了。

最后去寻找ans,ans应该等于值最大的那个lasttime(即这个工件最后才做完)。

输出ans即可。

参考代码:

#include <iostream>
  #define maxn 50
  using namespace std;
  int n,m;
  int ans = 0;
 int worklist[maxn * maxn];
  int worknumber[maxn][maxn];
  int worktime[maxn][maxn];
  int cnt_now_work_step[maxn];
  int lasttime[maxn];
  bool timeline[maxn * maxn][maxn * maxn];
   
  bool check_in_line(int begin_time_point,int end_time_length,int workid){
     for (int time = begin_time_point; time <= end_time_length;time++)
         if (timeline[workid][time])
              return false;
      return true;    
  }
 
  int main(){
      cin >> m >> n;
      for (int i=1;i<=n*m;i++)
          cin >> worklist[i];
  
     for (int i=1;i<=n;i++)
         for (int j=1;j<=m;j++)
              cin >> worknumber[i][j];
 
    for (int i=1;i<=n;i++)
          for (int j=1;j<=m;j++)
             cin >> worktime[i][j];
  
     for (int i=1;i<=n*m;i++){
          int nowitem = worklist[i];
          cnt_now_work_step[nowitem]++;//工序数
         int nownumber = worknumber[nowitem][cnt_now_work_step[nowitem]];
         int costtime = worktime[nowitem][cnt_now_work_step[nowitem]];
          
         for (int time = lasttime[nowitem]+1;;time++)//扫描时间轴
              if (check_in_line(time,time+costtime-1,nownumber)){
                  for (int marktime = time;marktime <= time+costtime-1;marktime++)
                     timeline[nownumber][marktime] = true;
                 lasttime[nowitem] = time + costtime - 1;
                  break;
             }
     }
 
     for (int i=1;i<=n;i++)
         ans = max(ans,lasttime[i]);
 
     cout << ans << endl;
 
    return 0;
 }


来源:退役的Shawn Zhou