遗传算法 与 作业车间调度问题(C++实现)

之前我们算法老师留的一个大作业,即作业车间调度问题,属于NP-hard问题,有很多种解法,这里给出遗传算法的解法。

问题描述

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

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

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

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

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

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

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

问题实例

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

  1. Jop1=[(0,3),(1,2),(2,2)]
  2. Jop2=[(0,2),(2,1),(1,4)]
  3. Jop3=[(1,4),(2,3)]

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

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

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

遗传算法

简介:

遗传算法(genetic algorithm (GA))是计算数学中用于解决最优化的搜索算法,是进化算法的一种。进化算法最初是借鉴了进化生物学中的一些现象而发展起来的,这些现象包括遗传、突变、自然选择以及杂交等。

遗传算法通常实现方式为一种计算机模拟。对于一个最优化问题,一定数量的候选解(称为个体)可抽象表示为染色体,使种群向更好的解进化。传统上,解用二进制表示(即0和1的串),但也可以用其他表示方法。进化从完全随机个体的种群开始,之后一代一代发生。在每一代中评价整个种群的适应度,从当前种群中随机地选择多个个体(基于它们的适应度),通过自然选择和突变产生新的生命种群,该种群在算法的下一次迭代中成为当前种群。

算法机理:

在遗传算法里,优化问题的解被称为个体,它表示为一个变量序列,叫做染色体或者基因串。染色体一般被表达为简单的字符串或数字符串,不过也有其他的依赖于特殊问题的表示方法适用,这一过程称为编码。首先,算法随机生成一定数量的个体,有时候操作者也可以干预这个随机产生过程,以提高初始种群的质量。在每一代中,都会评价每一个体,并通过计算适应度函数得到适应度数值。按照适应度排序种群个体,适应度高的在前面。这里的“高”是相对于初始的种群的低适应度而言。

下一步是产生下一代个体并组成种群。这个过程是通过选择和繁殖完成,其中繁殖包括交配(crossover,在算法研究领域中我们称之为交叉操作)和突变(mutation)。选择则是根据新个体的适应度进行,但同时不意味着完全以适应度高低为导向,因为单纯选择适应度高的个体将可能导致算法快速收敛到局部最优解而非全局最优解,我们称之为早熟。作为折中,遗传算法依据原则:适应度越高,被选择的机会越高,而适应度低的,被选择的机会就低。初始的数据可以通过这样的选择过程组成一个相对优化的群体。之后,被选择的个体进入交配过程。一般的遗传算法都有一个交配概率(又称为交叉概率),范围一般是0.6~1,这个交配概率反映两个被选中的个体进行交配的概率。例如,交配概率为0.8,则80%的“夫妻”会生育后代。每两个个体通过交配产生两个新个体,代替原来的“老”个体,而不交配的个体则保持不变。交配父母的染色体相互交换,从而产生两个新的染色体,第一个个体前半段是父亲的染色体,后半段是母亲的,第二个个体则正好相反。不过这里的半段并不是真正的一半,这个位置叫做交配点,也是随机产生的,可以是染色体的任意位置。再下一步是突变,通过突变产生新的“子”个体。一般遗传算法都有一个固定的突变常数(又称为变异概率),通常是0.1或者更小,这代表变异发生的概率。根据这个概率,新个体的染色体随机的突变,通常就是改变染色体的一个字节(0变到1,或者1变到0)。

经过这一系列的过程(选择、交配和突变),产生的新一代个体不同于初始的一代,并一代一代向增加整体适应度的方向发展,因为总是更常选择最好的个体产生下一代,而适应度低的个体逐渐被淘汰掉。这样的过程不断的重复:评价每个个体,计算适应度,两两交配,然后突变,产生第三代。周而复始,直到终止条件满足为止。一般终止条件有以下几种:

1.进化次数限制。

2.计算耗费的资源限制(例如计算时间、计算占用的内存等)。

3.一个个体已经满足最优值的条件,即最优值已经找到。

4.适应度已经达到饱和,继续进化不会产生适应度更好的个体。

5.人为干预。

6.以及以上两种或更多种的组合。

7.本算法中使用进化次数来终止算法,得到解。

遗传算法基本步骤:

1.选择初始生命种群(编码)

2.循环

3.评价种群中的个体适应度

4.以比例原则(分数高的挑中机率也较高)选择产生下一个种群(轮盘法(roulette wheel selection)、竞争法(tournament selection)及档次轮盘法(Rank Based Wheel Selection))。不仅仅挑分数最高的的原因是这么做可能收敛到局部的最佳点,而非整体的。本算法中使用竞争法来选择产生下一个种群。

5.改变该种群(交叉和变异)

6.直到停止循环的条件满足,即达到遗传次数。

算法流程图:

 遗传算法所需参数:

(1). 种群规模(P,population size):即种群中染色体个体的数目。其值根据问题规模设定。

(2). 染色体长度(cl, chromosome length):个体中染色体的长度。

(3). 交配概率(pc, probability of performing crossover):控制着交配算子的使用频率。交配操作可以加快收敛,使解达到最有希望的最佳解区域,因此一般取较大的交配概率,但交配概率太高也可能导致过早收敛,则称为早熟。

(4). 突变概率(pm, probability of mutation):控制着突变算子的使用频率。

(5). 中止条件(termination criteria):本算法中使用进化代数来终止算法,其中进化代数也由问题规模来决定。

遗传算法求解作业车间调度问题

阅读了以上关于遗传算法的内容,接下来使用遗传算法来求解作业车间调度问题

步骤如下

一. 编码(初始化种群)

根据种群数量population,生成population条染色体,染色体的编码长度与机器数量和工件数量相关,即m台机器,n个工件,每台机器的工序数为 ,则染色体长度为  ,即每个工件的工序数不定,有些工件的工序数可能小于m,所以得到染色体的长度应为  ,接下来对染色体进行编码,给定一个染色体序列

其中  代表的是第i个工件,其规则如下,对于一个工件的第几次出现当作是该工件的工序号,比如序列中第一次出现那么就是工件的第一道工序,第二次出现就是第二道工序,以此类推

二. 计算适应度

计算每条染色体的适应度,本算法中用加工完所用工件的所有工序的最晚时间表示,并且时间短的适应度高。其时间计算如下

       令 表示第i个工件第j个工序的最晚起始时间

       令 表示第i个工件第j个工序的终止时间

       令 表示第i个工件第j个工序所使用的机器编号

       令 表示加工完第i个工件第j个工序所用的时间

       令 表示第j台机器所加工的第i个工件的工序

       令 表示第i台机器的加工时间

       令 表示第i个工件的当前工序

 

从左到右遍历染色体序列 ,其中 表示第i个工件

的当前工序为 ,令其为 对应工序所使用的机器编号为  令其为M,对应的加工时间为 ,令其为T,则工件 的工序P的最晚开始时间为

 

而第M台机器的加工时间为

machineWorkTime_M= startTime_{j_i P}+T

       则工件j_i 的工序P的终止时间为

最后加工完所用工件的所有工序的最晚时间为

其值也为当前染色体的适应度

三. 选择个体

选择个体,个体的选择使用锦标赛法(Tournament Selection),其策略为从整个种群中抽取 y 个体,让他们进行竞争(锦标赛),抽取其中的最优的个体。参加锦标赛的个体为整个种群,并且将y设为3,即选择3个个体。y = 3的选择过程如下图所示

使用锦标赛法的优势为

  1. 时间复杂度更小,即O(n)
  2. 易并行化处理
  3. 不易陷入局部最优点
  4. 不需要对所有的适应度值进行排序处理过程

四. 交叉算子

算法中使用Order Crossover (OX)交叉算子,该算子的交叉过程如下

  1. 从选择的一对染色体选一个染色体当父本,然后从父本中随机产生一个起始位置start和终止位置end,并由父本中start到end的序列产生一个子代原型

  2. 将另一条染色体作为母本,并从母本中将其余编码加入Child Prototype中

  3. 上述过程产生一个Child,然后重复(1),(2)交换父本母本来产生第二个Child

五. 突变算子

算法中使用位置变异法,即从染色体中随机产生两个位置,交换这两个位置的值,如下图所示

六. 重复上述步骤,直到达到遗传代数,然后从种群中选择最优的一个个体当为整体的一个解

C++代码如下

#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <ctime>
#include <functional>
#include <vector>
#include <set>
#include <cstdlib>

using namespace std;
const ulong population_number = 20;
const ulong times = 5;
const ulong N = 200;

int machine;          //机器数量
int job;              //工件数量
int process;          //任务总数
int chromosome_size;  //染色体长度


const class Probability {        //概率
public:
    const double cross = 0.95;   //交叉概率
    const double mutation = 0.05;//变异概率
} probability;

class Matrix {              //矩阵
public:
    int Machine[N][N] = {}; //job process => machine
    int Time[N][N] = {};    //job process => time
    int Process[N][N] = {}; //job machine => process
    Matrix() {
        fill(Machine[0], Machine[0] + N * N, -1);
        fill(Process[0], Process[0] + N * N, -1);
    }
} matrix;

class Gene {     //基因
public:
    string chromosome = string(static_cast<ulong>(job * machine), '0'); //染色体
    int fitness = 0;                                                    //适应度

    explicit Gene(int fitness) { this->fitness = fitness; };

    Gene() = default;

    Gene(const Gene &other) {
        chromosome = other.chromosome;
        fitness = other.fitness;
    }

    Gene &operator=(const Gene &other) = default;

    bool operator==(const Gene &other) const {
        return chromosome == other.chromosome && fitness == other.fitness;
    }

    bool operator<(const Gene &other) const {
        return chromosome < other.chromosome;
    }
};

vector<Gene> populations;       //种群

class Store {
public:
    int machineWorkTime[N] = {}; //机器工作时间
    int processIds[N] = {};      //对应任务的工序
    int endTime[N][N] = {};      //job process => endtime
    int startTime[N][N] = {};    //job process => starttime
    Store() = default;

    Store(const Store &other) {
        copy(begin(other.machineWorkTime), end(other.machineWorkTime), begin(machineWorkTime));
        copy(begin(other.processIds), end(other.processIds), begin(processIds));
        copy(other.endTime[0], other.endTime[0] + N * N, endTime[0]);
        copy(other.startTime[0], other.startTime[0] + N * N, startTime[0]);
    }

    Store &operator=(const Store &other) {
        copy(begin(other.machineWorkTime), end(other.machineWorkTime), begin(machineWorkTime));
        copy(begin(other.processIds), end(other.processIds), begin(processIds));
        copy(other.endTime[0], other.endTime[0] + N * N, endTime[0]);
        copy(other.startTime[0], other.startTime[0] + N * N, startTime[0]);
        return *this;
    }
};

/**
 * 产生 start到end的随机整数
 * @param start
 * @param end
 * @return
 */
ulong randint(ulong start, ulong end) {
    return rand() % (end - start + 1) + start;
}

/**
 * 产生 0 到 end的随机整数
 * @param end
 * @return
 */
ulong randint(ulong end) {
    return rand() % (end + 1);
}

/**
 * char => int
 * @param ch
 * @return
 */
int Int(char ch) {
    return ch - '0';
}

/**
 *int => char
 * @param i
 * @return
 */
char Char(int i) {
    return static_cast<char>(i + '0');
}

/**
 * 将vector填充整数
 * @param v
 * @param length
 * @param except
 */
void generateVector(vector<int> &v, int length, int except = -1) {
    for (int i = 0; i < length; i++) {
        if (except != -1 && i == except) continue;
        v.push_back(i);
    }
}

/**
 * 计算适应度
 * @param gene
 * @param store
 */
void calculateFitness(Gene &gene, Store &store) {
    int fulfillTime = 0;
    for (char i : gene.chromosome) {
        int jobId = Int(i) - 1,
                processId = store.processIds[jobId],
                machineId = matrix.Machine[jobId][processId],
                time = matrix.Time[jobId][processId];
        store.processIds[jobId]++;
        store.startTime[jobId][processId] = processId == 0 ? store.machineWorkTime[machineId] : max(store.endTime[jobId][processId - 1],       store.machineWorkTime[machineId]);
        store.machineWorkTime[machineId] = store.startTime[jobId][processId] + time;
        store.endTime[jobId][processId] = store.machineWorkTime[machineId];
        if (store.machineWorkTime[machineId] > fulfillTime) {
            fulfillTime = store.machineWorkTime[machineId];
        }
    }
    gene.fitness = fulfillTime;
}

/**
 * 初始化种群
 * @param genes 
 * @param population 
 */
void initializePopulation(vector<Gene> &genes, int population) {
    set<Gene> gs;
    for (int i = 0; i < population; i++) {
        vector<int> index_list;
        generateVector(index_list, job * machine);
        Gene gene;
        for (int j = 1; j <= job; j++) {
            for (int k = 0; k < machine; k++) {
                ulong index = randint(index_list.size() - 1);
                int value = index_list[index];
                index_list.erase(index_list.begin() + index);
                if (matrix.Process[j - 1][k] != -1)
                    gene.chromosome[value] = Char(j);
            }
        }
        remove_if(gene.chromosome.begin(), gene.chromosome.end(), [](char v) -> bool { return v == '0'; });
        gene.chromosome.resize(static_cast<ulong>(chromosome_size));
        Store store;
        calculateFitness(gene, store);
        gs.insert(gene);
    }
    insert_iterator<vector<Gene>> insert_vector(genes, genes.begin());
    copy(gs.begin(), gs.end(), insert_vector);
}

/**
 * 基因突变
 * @param gene 
 * @param n 
 */
void geneticMutation(Gene &gene, int n = 2) {
    vector<int> index_list;
    generateVector(index_list, chromosome_size);
    for (int i = 0; i < n; i++) {
        ulong first = randint(0, index_list.size() - 1);
        index_list.erase(index_list.begin() + first);
        ulong second = randint(0, index_list.size() - 1);
        swap(gene.chromosome[first], gene.chromosome[second]);
    }
    Store store;
    calculateFitness(gene, store);
}

/**
 * 基因交叉
 * @param first 
 * @param second 
 * @return 
 */
pair<Gene, Gene> orderCrossover(Gene &first, Gene &second) {
    function<Gene(Gene &, Gene &)> generateChild = [](Gene &first, Gene &second) -> Gene {
        vector<int> index_list;
        generateVector(index_list, chromosome_size);

        ulong start = randint(0, index_list.size() - 1);
        index_list.erase(index_list.begin() + start);
        ulong end = randint(0, index_list.size() - 1);

        string middle = first.chromosome.substr(start, end - start),
                t = middle,
                k = second.chromosome;
        for (char &it : t) {
            for (int i = 0; i < k.size(); i++) {
                if (k[i] == it) {
                    k.erase(k.begin() + i);
                    break;
                }
            }
        }
        Gene child;
        child.chromosome = k.substr(0, start) + middle + k.substr(start, k.length() - start);
        Store store;
        calculateFitness(child, store);
        return child;
    };
    pair<Gene, Gene> child;
    child.first = generateChild(first, second);
    child.second = generateChild(second, first);
    return child;
}

/**
 * 选择个体
 * @param n 
 * @return 
 */
Gene selectIndividual(int n = 3) {
    vector<int> index_list;
    generateVector(index_list, population_number);
    vector<Gene> simple;
    for (int i = 0; i < n; i++) {
        ulong index = randint(index_list.size() - 1);
        index_list.erase(index_list.begin() + index);
        simple.push_back(populations[index]);
    }
    Gene best_gene(0xffffff);
    for (int i = 1; i < n; i++) {
        if (simple[i].fitness < best_gene.fitness) {
            best_gene = simple[i];
        }
    }
    return best_gene;
}

int main() {
    srand(static_cast<uint>(time(nullptr)));

    chromosome_size = 0;
    cout << "input job and machine:" << endl;
    cin >> job >> machine;
    for (int i = 0; i < job; i++) {
        int p;
        cout << "input job" << i << " process:" << endl;
        cin >> p;
        chromosome_size += p;
        if (process < p) process = p;
        cout << "input machine and time:" << endl;
        for (int j = 0; j < p; j++) {
            int m, t;
            cin >> m >> t;
            matrix.Machine[i][j] = m;
            matrix.Time[i][j] = t;
        }
    }

    for (int i = 0; i < job; i++) {
        for (int j = 0; j < process; j++) {
            if (matrix.Machine[i][j] != -1)
                matrix.Process[i][matrix.Machine[i][j]] = j;
        }
    }

    initializePopulation(populations, population_number);    //初始化种群
    ulong n = times;
    while (n-- > 0) {
        cout << "n = " << n << endl;
        double P = randint(0, 100) / 100.0;
        if (P < probability.mutation) {
            ulong index = randint(populations.size() - 1);
            geneticMutation(populations[index]);
        } else {
            ulong size = populations.size();
            ulong m = size / 4;
            set<Gene> children;
            while (m-- > 0) {
                Gene father = selectIndividual();
                Gene mother = selectIndividual();
                pair<Gene, Gene> child = orderCrossover(father, mother);
                children.insert(child.first);
                children.insert(child.second);
            }
            sort(populations.begin(), populations.end(),
                 [](const Gene &a, const Gene &b) -> bool { return a.fitness < b.fitness; });
            populations.resize(size - size / 4);
            for (const auto &it : children) {
                populations.push_back(it);
            }
            sort(populations.begin(), populations.end());
            auto iter = unique(populations.begin(), populations.end());
            populations.resize(static_cast<ulong>(distance(populations.begin(), iter)));
        }
    }

    Gene best_gene(0xffffff);
    for (const auto &it : populations) {
        cout << "chromosome = " << it.chromosome << " " << it.fitness << endl;
        if (best_gene.fitness > it.fitness) {
            best_gene = it;
        }
    }

    Store store;
    cout << "result = " << best_gene.chromosome << " time = " << best_gene.fitness << endl;
    calculateFitness(best_gene, store);
    for (int i = 0; i < machine; i++) {
        cout << "machine" << i << " work time " << store.machineWorkTime[i] << endl;
    }

    for (int j = 0; j < job; j++) {
        for (int k = 0; k < process; k++) {
            if (matrix.Machine[j][k] != -1)
                cout << "job" << j << " process" << k << " machine" << matrix.Machine[j][k] << " start time="
                     << store.startTime[j][k] << " end time=" << store.endTime[j][k] << endl;
        }
    }
    return 0;
}

输入测试用例

3 3
3
0 3
1 2
2 2
3
0 2
2 1
1 4
2
1 4
2 3

结果如下

上面的输入得到最优解11的调度有多种,这里我们可以任意选择一个即可,这里我挑选解序列为13223121, 调度时间为11,作其甘特图如下:

总结

遗传算法可以说是将生物学知识与现实生活相联系,利用生物学中物种的进化理论,并以随机化的方式来求解出作业车间调度问题的解,遗传算法的核心可以说就是对进化论里的“适者生存,不适者淘汰”的描述,为了并且使用突变的方式来跳出局部最优解的情况,在实现算法中只需要模拟生物进化。算法的基本操作也很简单,编码,求适应度,突变,选择,交叉,然而每一个操作都有多种实现方法,比如说编码,我使用的是常规的编码方式,但这种编码方式却有着效率低的缺点,学术界有一些比较好的论文讨论了使用其他的编码方式来提高效率,比如使用二次编码等,突变算法也有很多实现,比如基本位变异,均匀变异,边界变异,高斯近似变异等,选择也有很多的选择算法,例如轮盘赌,随机竞争,最佳保留等等,交叉算法有PMX,OX,PBX,OBX,CX等等,本算法中每个操作都挑选了一些比较好实现并且效率比较高的算子。

猜你喜欢

转载自blog.csdn.net/daydream13580130043/article/details/87916668