C++模拟蚁群算法解决作业车间调度问题(Job-Shop Scheduling Problem,JSP)

算法 同时被 2 个专栏收录
32 篇文章 1 订阅
1 篇文章 0 订阅

一、作业车间调度问题描述

作业车间调度问题(Job Shop Scheduling, JSP)是最经典的几个NP-hard问题之一。其应用领域极其广泛,涉及航母调度,机场飞机调度,港口码头货船调度,汽车加工流水线等。

JSP问题描述:一个加工系统有M台机器,要求加工N个作业,其中,作业i包含工序数为Li。令,则L为任务集的总工序数。其中,各工序的加工时间已确定,并且每个作业必须按照工序的先后顺序加工。调度的任务是安排所有作业的加工调度排序,约束条件被满足的同时,使性能指标得到优化

作业车间调度需要考虑如下约束:

Cons1:每道工序在指定的机器上加工,且必须在其前一道工序加工完成后才能开始加工;

Cons2:某一时刻1台机器只能加工1个作业;

Cons3:每个作业只能在1台机器上加工1次;

Cons4:各作业的工序顺序和加工时间已知,不随加工排序的改变而改变。

二、作业车间调度问题的数学模型

在本课程的综合设计与实现环节中,我们将作业车间调度问题的优化目标设为最大完工时间最小:令(i,j)表示作业i的第j个工序。SijTij分别表示(i,j)的加工起始时刻和加工时间。Zijk表示(i,j)是否在第k台机器上加工:如果(i,j)在第k台机器上加工,Zijk=1;否则,Zijk=0。Ck为第k台机器的完工时间,则问题的数学模型如下:

(1)

(2)

(3)

(4)

公式(1)为目标函数,使最迟完工的机器尽早完成,即加工时间最短;公式(2)表示1个作业只能在加工完成前一道工序后才可以加工后一道工序;公式(3)表示1个作业的第1道工序的起始加工时刻大于或等于0;公式(4)表示在1台机床上不会同时加工1个以上的作业。

三、问题实例

下面给出作业车间调度问题的一个实例,其中每个工序上标注有一对数值(m,p),其中,m表示当前工序必须在第m台机器上进行加工,p表示第m台机器加工当前工序所需要的加工时间。(注:机器和作业的编号从0开始)

  1. jop0=[(0,3),(1,2),(2,2)]
  2. jop1=[(0,2),(2,1),(1,4)]
  3. jop2=[(1,4),(2,3)]

在这个例子中,作业jop0有3道工序:它的第1道工序上标注有(0,3),其表示第1道工序必须在第0台机器上进行加工,且需要3个单位的加工时间;它的第2道工序上标注有(1,2),其表示第2道工序必须在第1台机器上进行加工,且需要2个单位的加工时间;余下的同理。总的来说,这个实例中共有8道工序。

该问题的一个可行解是L=8道工序开始时间的一个排列,且满足问题的约束。下图给出了一个可行解(注:该解不是最优解)的示例:

在上图中,我们能看到每个作业的工序按照问题给定的顺序进行了加工,且相互之间没有时间区间重叠。这个可行解的结果是12,即三个作业均被加工完成的时间。

四、算法设计思想

对于本次课题,我选用蚁群算法来解决车间调度问题。

1.关于蚁群算法:

蚁群系统(Ant Colony System)是由意大利学者Dorigo、Maniezzo等人于20世纪90年代首先提出来的。他们在研究蚂蚁觅食的过程中,发现单个蚂蚁的行为比较简单,但是蚁群整体却可以体现一些智能的行为。例如蚁群可以在不同的环境下,寻找最短到达食物源的路径。这是因为蚁群内的蚂蚁可以通过某种信息机制实现信息的传递。后又经进一步研究发现,蚂蚁会在其经过的路径上释放一种可以称之为“信息素”的物质,蚁群内的蚂蚁对“信息素”具有感知能力,它们会沿着“信息素”浓度较高路径行走,而每只路过的蚂蚁都会在路上留下“信息素”,这就形成一种类似正反馈的机制,这样经过一段时间后,整个蚁群就会沿着最短路径到达食物源了。

蚂蚁找到最短路径要归功于信息素和环境,假设有两条路可从蚁窝通向食物,开始时两条路上的蚂蚁数量差不多:当蚂蚁到达终点之后会立即返回,距离短的路上的蚂蚁往返一次时间短,重复频率快,在单位时间里往返蚂蚁的数目就多,留下的信息素也多,会吸引更多蚂蚁过来,会留下更多信息素。

在车间调度问题中,蚂蚁的行走是在一个由活动及其可选模式所构成的图上的,该情况下的项目调度问题的解是由活动的序列及其模式所出现的顺序所构成的一个路径图,其目标是希望所有活动都完成所耗费的时间最短,而蚂蚁的信息素是释放在活动及其对应选择的模式上的,因此在蚁群算法的设计中,最短项目工期所构成的路径中,随着信息素的更新,使得其各个路径点的信息素含量会积累得最高,蚂蚁选择走的概率最大。

具体来说,各个蚂蚁在没有事先告知食物在什么地方的前提下开始寻找食物。当一只找到食物以后,它会向环境释放一种挥发性分泌物pheromone (称为信息素,该物质随着时间的推移会逐渐挥发消失,信息素浓度的大小表征路径的远近)信息素能够让其他蚂蚁感知从而起到一个引导的作用。通常多个路径上均有信息素时,蚂蚁会优先选择信息素浓度高的路径,从而使浓度高的路径信息素浓度更高,形成一个正反馈。有些蚂蚁并没有像其它蚂蚁一样总重复同样的路,他们会另辟蹊径,如果另开辟的道路比原来的其他道路更短,那么,渐渐地,更多的蚂蚁被吸引到这条较短的路上来。最后,经过一段时间运行,可能会出现一条最短的路径被大多数蚂蚁重复着。最终,信息素浓度最高的路径即是最终被蚂蚁选中的最优路径。

2.车间调度问题模型的建立:

Job-Shop调度问题因其复杂度高、随机性强、多约束、多目标的特点,要把蚁群算法运用在其中,一个关键问题是如何将Job-Shop调度问题转化成适合蚁群算法的一个自然表达模型。

我们可以把工件的各道工序看成是TSP问题中的各个城市,利用蚁群遍历各工件的工序所得的路径与一个可行的调度形成一一对应的关系,并把各工件待加工工序的加工时间看成距离,把总的完工时间看成是蚂蚁走过的总距离。同时Job-Shop调度的结果必须满足前面所提到的四个约束条件。

Job-Shop调度问题的析取图模型位G(V,A,E),其中V是节点集合,包括所有工件的所有操作和两个特殊节点,起始点,终止节点,分别代表调度的开始和结束。A是合取弧,表示同一工件的加工顺序方向。E是析取弧,所连接的两个操作要在同一台机器上进行加工。如图是一个大概的模型:

3.图的遍历:

初始情况下,将蚂蚁统一放置到超级源点,并且将整个图的所有路径上的信息素设为一个固定值。之后,所有蚂蚁一起向终点行动,蚂蚁会受到信息素的影响,蚂蚁选择路径的方式是蚂蚁当前可走路径的信息素加和,作为取随机数m的最大值,通过随机数m和可选信息素,计算出蚂蚁选择的路径。当所有蚂蚁都走到终点的时候,将全图的信息素按照信息素消散参数进行消散,并计算出这些蚂蚁的路径。这个路径一种工件加工的顺序,按照这个顺序计算出工件加工的时间T。通过这个时间,计算出这个蚂蚁新增的信息素W=Q/T(Q是一个常数)。这个蚂蚁之前经历过的路径上的信息素都加上W。当所有的蚂蚁的信息素全部新增到图当中的时候。将所有蚂蚁再放回到起点,重复之前的步骤,随着重复次数的增多,蚂蚁会越来越向着最优解靠近。

4.流程图:

5.最优解的寻找:

在计算蚂蚁行走的路径过程中,会时刻记录着当前出现过的,工件加工时间最短的情况。在程序结束之后,把最短的加工时间输出出来。

五、伪代码

(1)机器加工时间计算函数:

定义几个用来记录目前机器加工总时间的临时变量sum。然后是真正机器的总加工总时间的数组MachineWorkTime[maxn]。接下来是加工持续时间,目前加工步数,记录加工次序的变量的定义。在加工总工数的限定下,记录现在加工工件的工序、取加工顺序最晚加工完成的的时间作为下一个工件开始的时间、还有现在加工工件的件号、加工结束时间、持续时间、还有这是第几个加工机器。最后算出机器加工的时间。再接着往下计算下一工件在不同机器的加工时间。最后选出最大机器加工结束时间,以作为下一机器,下一工件的开始时间。继续下一个循环。

int time_caculation(int 加工需要的步数,bool 假) //计算需要加工的时间的函数
{
    int 总加工时间
    int 机器加工时间
    int J/加工持续时间
    int 加工进行到的步数
    recording 记录加工次序结构体数组
    //清空以上数组
    for(int k=0; k<工序总数; k++)
    {
        //记录现在加工工件的工序
        //取加工顺序里面最晚加工完成的作为起始点
        rec[k].job=i;//现在加工工件的件号
        rec[k].endd=rec[k].start +job[i][JobStep[i]].len;//加工结束时间
        JobLast[i]=rec[k].endd; //持续时间
        rec[k].machine=job[i][JobStep[i]].machine; //第几个加工机器
        MachineWorkTime[job[i][JobStep[i]].machine]=rec[k].endd;
        JobStep[i]++;
    }
    for(int i=0; i<m; i++)
    {
        max 取最大加工时间(作为下一个工件的开始时间)
    }
  //甘特图绘制
    return sum; //返回总加工时间
}

(2)甘特图绘画部分:

在time_calculation(int Job[],bool draw),设定了一个draw变量,如果draw=false 仅输出sum,即机器的最大加工时间。如果draw=true即执行上面的代码段,在加工工序数量和机器总最大加工时间的限定下,输出甘特图。

    if(条件为真) 
    {
        int 甘特图数组
        memset 清空数组
        for(int i=0; i<工序总数; i++)
        {
            for(int j=开始点; j<结束时间; j++)
            {
                把工件序号依次输出作为甘特图
            }
        }
        循环输出甘特图
    }

(3)正反馈机制过程:

除了蚂蚁走过时会形成信息素,接下来是定义一个reward变量模拟反馈机制。如果加工时间越短,说明距离短,说明蚂蚁往返一次时间短,重复频率高,单位时间内往返蚂蚁的数量就更多,留下的信息素就更多,会吸引的蚂蚁就更多,然后会留下更多的信息素。形成一个正反馈作用,记录这些反馈作用增加信息素的位置,再将这个正反馈形成的信息素加到信息素数组Pheromone[][][][]中。

            for(int i=0; i<蚂蚁数量; i++)
            {
                int 临时记录加工时间变量=time_caculation(ant[i].Path,false);
                if(ans <最优加工时间 && 总工序数所需要时间 <=临时最优加工时间)
                {
                    最优加工时间=临时记录加工时间
                    bestAnt=ant[i];
                }
                int reward =2000/ans; //回馈机制中的回馈,数字越小,回馈越大。
                for(int j=0; j<ant[i].Pathlen-1; j++)
                {
	  记录信息素增加的位置。
	  并且将增加的信息素加上原来的信息素:
                    Pheromone[a][b][c][d]+=reward;
                }
            }

(4)蚂蚁寻优过程:

首先定义蚂蚁数量,定义一个蚂蚁结构体,还有记录最佳次序的蚂蚁,还有记录最优加工时间的变量。设置随机数种子,循环进行蚂蚁寻道的过程。我们设定一个类似网络流里的超级源点,让蚂蚁随机在一个超级源点开始寻道。当蚂蚁行走时候(也就是蚂蚁选择了一个工件进行加工),如果选择的这个跟现在机器进行加工的工件以及次序是一样的,就进行信息素的累加。接下来是在加工数量有限的工序下,记录信息素的变化情况。

        int 蚂蚁数量
        Ant 蚂蚁结构体数组
        Ant 最优的蚂蚁
        把最优加工时间设定为无限大
        for(int i=0; i<10; i++)//进行10次迭代
        {
            //初始化随机数发生器,并且设置启动种子。生成随机数
            for(int j=0; j<antnum; j++) //第j只蚂蚁的旅程。
            {
                设定一个作为图的超级源点
                蚂蚁进行到的起始步数=SUPER_START;
                for(int k=0; k<Total_Step; k++)
                {
                    int allpre=0;//累计信息素
                    for(int l=0; l<m; l++)
                    {
                        if(蚂蚁现在加工的工件步数= 实际上机器加工的工件步数)
                        {
                            continue;
                        }
                        计算现在累计的信息素
                    }
                    int 一个随机选择数字
                    while(随机选择数字>=0)
                    {
                        if(现在蚂蚁加工的工件已经加工过了)
                        {
                            选择下一个工件,继续循环
                        }
                        将蚂蚁遍历过的路径“剪掉”,选择另一条路径进行加工
	      并且接着选择下一个工件
                    }
                }

 

六、程序代码及运行结果

源代码:

#include <bits/stdc++.h>
#define maxn 60 //最大工作量
#define Initialize_Pre 3000 //道路初始信息素量
#define DIS 0.5 //信息素消散速率
#define SUPER_START 48 //超级源点
using namespace std;

int Total_Step; //总步数
int Step[maxn]; //记录步数
int Pheromone[maxn][maxn][maxn][maxn]; //信息素,费洛蒙
int n ; //工件的数量
int m; //机器的数量
const int INF=9999999;

struct Pair
{
    int i;
    int j;
    void get(int a,int b)
    {
        i=a;
        j=b;
    }
} Jobnum[maxn];

struct Job
{
    int machine; //机器数量
    int len; //加工需要的时间段
} job[maxn][maxn];

struct Ant
{
    int JobStep[maxn]; //任务已经加工的步数
    int Path[maxn]; //记录最优加工次序的数组
    int Pathlen; //长度
    int get_Full_Path()
    {
        int sum=0;
        for(int i=0; i<Pathlen; i++)
        {
            sum+=Path[i];
        }
        return sum;
    }
    Pair paths[maxn];
};

void Initialize() //初始化函数
{
    memset(Pheromone,0,sizeof(Pheromone));
    for(int i=0; i<Total_Step; i++)
    {
        for(int j=0; j<Total_Step; j++)
        {
            for(int k=0; k<Total_Step; k++)
            {
                for(int l=0; l<Total_Step; l++)
                {
                    Pheromone[i][j][k][l]=Initialize_Pre; //初始化信息素道路的信息素
                }
            }
        }
    }
    for(int i=0; i<Total_Step; i++)
    {
        for(int j=0; j<Total_Step; j++)
        {
            Pheromone[SUPER_START][SUPER_START][i][j]=Initialize_Pre;
        }
    }
    return;
}

void Dissipation() //信息素消散函数
{
    for(int i=0; i<Total_Step; i++)
    {
        for(int j=0; j<Total_Step; j++)
        {
            for(int k=0; k<Total_Step; k++)
            {
                for(int l=0; l<Total_Step; l++)
                {
                    Pheromone[i][j][k][l] *=DIS; //乘以消散率,随着时间消散
                }
            }
        }
    }
    for(int i=0; i<Total_Step; i++)
    {
        for(int j=0; j<Total_Step; j++)
        {
            Pheromone[SUPER_START][SUPER_START][i][j] *=DIS;
        }
    }
    return;
}

struct recording
{
    int start; //开始的时间
    int endd; //结束的时间
    int job; //工件
    int machine; //机器
};

int time_caculation(int Job[],bool draw) //计算需要加工的时间的函数
{
    int sum=0; //总加工时间
    int MachineWorkTime[maxn]; //机器加工时间
    int JobLast[maxn]; //加工持续时间
    int JobStep[maxn]; //加工进行到的步数
    recording rec[maxn]; //记录加工次序
    //清空数组
    memset(MachineWorkTime,0,sizeof(MachineWorkTime));
    memset(JobLast,0,sizeof(JobLast));
    memset(JobStep,0,sizeof(JobStep));
    memset(rec,0,sizeof(rec));
    for(int k=0; k<Total_Step; k++)
    {
        int i=Job[k];//记录现在加工工件的工序
        rec[k].start=max(JobLast[i],MachineWorkTime[job[i][JobStep[i]].machine]);
        //取加工顺序里面最晚加工完成的作为起始点
        rec[k].job=i;//现在加工工件的件号
        rec[k].endd=rec[k].start +job[i][JobStep[i]].len;//加工结束时间
        JobLast[i]=rec[k].endd; //持续时间
        rec[k].machine=job[i][JobStep[i]].machine; //第几个加工机器
        MachineWorkTime[job[i][JobStep[i]].machine]=rec[k].endd;
        JobStep[i]++;
    }
    for(int i=0; i<m; i++)
    {
        sum=max(sum,MachineWorkTime[i]);
    }
    if(draw==true) //甘特图绘制
    {
        int gantt[maxn][maxn];//甘特图数组
        memset(gantt,0,sizeof(gantt));//清空数组
        for(int i=0; i<Total_Step; i++)
        {
            for(int j=rec[i].start; j<rec[i].endd; j++)
            {
                gantt[rec[i].machine][j]=rec[i].job+1;
            }
        }
        for(int i=0; i<m; i++)
        {
            for(int j=0; j<sum; j++)
            {
                printf("%d%c",gantt[i][j],j==sum-1?'\n':' '); //输出甘特图
            }
        }
    }
    return sum; //返回总加工时间
}

int main()
{
    cout << "请输入工件的数量以及机器的数量:"<< endl;
    while(cin >> n >> m)
    {
        Total_Step=0;
        for(int i=0; i<n; i++)
        {
            cout << "请输入加工所需要的步骤数量:"<< endl;
            cin >> Step[i];
            Total_Step+=Step[i];
            cout << "请输入需要在其加工的机器号码以及所需要加工的时间:"<< endl;
            for(int j=0; j<Step[i]; j++)
            {
                cin >> job[i][j].machine >> job[i][j].len;
            }
        }
        Initialize(); //初始化信息素
        //这部分是蚂蚁行走部分
        int antnum=Total_Step*2;//蚂蚁数量
        Ant ant[antnum+5];
        Ant bestAnt;
        int Best_Process_Time=INF;
        for(int i=0; i<10; i++)
        {
            srand(time(0)); //初始化随机数发生器,并且设置启动种子。生成随机数
            memset(ant,0,sizeof(ant));
            for(int j=0; j<antnum; j++) //第j只蚂蚁的旅程。
            {
                int nowJob=SUPER_START;//作为图的超级源点
                ant[i].JobStep[nowJob]=SUPER_START;
                for(int k=0; k<Total_Step; k++)
                {
                    int allpre=0;
                    for(int l=0; l<m; l++)
                    {
                        if(ant [i].JobStep[l]==Step[l])
                        {
                            continue;
                        }
                        allpre+=Pheromone[nowJob][ant[i].JobStep[nowJob]][l][ant[i].JobStep[l]];
                    }
                    int randSelectNum=rand()*rand()%allpre;
                    int select=0;
                    while(randSelectNum>=0)
                    {
                        if(ant[i].JobStep[select]==Step[select])
                        {
                            select++;
                            continue;
                        }
                        randSelectNum -= Pheromone[nowJob][ ant[i].JobStep[nowJob] ][select][ ant[i].JobStep[select] ];
                        select++;
                    }
                    select--;
                    ant[i].Path[ant[i].Pathlen]=select;
                    ant[i].paths[ant[i].Pathlen++].get(select,ant[i].JobStep[select]);
                    //ant[i].JobStep[select]++;
                    nowJob=select;
                }
            }
            Dissipation();//每次蚂蚁行走完之后,信息素都会消散
            //这部分是反馈部分
            for(int i=0; i<antnum; i++)
            {
                int ans=time_caculation(ant[i].Path,false);
                if(ans <Best_Process_Time && Total_Step <=ans)
                {
                    Best_Process_Time =ans;
                    bestAnt=ant[i];
                }
                int reward =2000/ans; //回馈机制中的回馈,数字越小,回馈越大。
                for(int j=0; j<ant[i].Pathlen-1; j++)
                {
                    int a=ant[i].paths[j].i;
                    int b=ant[i].paths[j].j;
                    int c=ant[i].paths[j+1].i;
                    int d=ant[i].paths[j+1].j;
                    Pheromone[a][b][c][d]+=reward;
                }
            }
            cout << "最佳的加工时间是:"<< Best_Process_Time << endl;
            cout << "甘特图是:"<< endl;
            time_caculation(bestAnt.Path,true);
            cout << "加工顺序是:"<< endl;
            for(int i=0; i<Total_Step; i++)
            {
                printf("%d%s",bestAnt.Path[i]+1,i==Total_Step-1?"\n":"->");
            }
        }
        return 0;
    }
}

1.进行一次迭代的结果图:

2.进行10次迭代的结果:

七、总结

经过本项目,初次接触NP完全问题,较好地理解车间调度问题的解决方案以及对蚁群算法更好的认识。

蚁群算法有以下特点,总结如下:

1.从算法的性质而言,蚁群算法是在寻找一个比较好的局部最优解,而不是强调全局最优解

2.开始时算法收敛速度较快,在随后寻优过程中,迭代一定次数后,容易出现停滞现象

3.蚁群算法对JSP、TSP及相似问题具有良好的适应性,无论车间规模大还是小,都能进行有效地求解,而且求解速度相对较快

4.蚁群算法解得稳定性较差,及时参数不变,每次执行程序都有可能得到不同界,为此需要多执行几次,已寻找最佳解。

5.蚁群算法中有多个需要设定的参数,而且这些参数对程序又都有一定的影响,所以选择合适的参数组合在算法设计过程中也非常重要

八、参考文献

[1]https://blog.csdn.net/qq_41249282/article/details/84318943

[2]https://blog.csdn.net/weixin_33690367/article/details/87979062

[3]https://blog.csdn.net/weixin_42715356/article/details/83590449

[4]https://blog.csdn.net/qq_41249282/article/details/84797347

[5]吴 统 威 . 遗传蚁群算法在Job_Shop调度的应用研究

[6]ZHANG Xiaoling, HE Caixiang, CHEN Jianhua . Ant Colony Algorithm for Solving Job-Shop Scheduling Problem(College of Mathematics and Computer Science,Dali University, Dali,Yunnan 671003,China)

[7]曹 岩,雷 蕾,房亚东.蚁群算法在离散型车间派工中的应用(西安工业大学 机电工程学院,西安 710032)

  • 5
    点赞
  • 7
    评论
  • 54
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 1024 设计师:白松林 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值