logo头像
Snippet 博客主题

超详细遗传算法解析附带TSP代码

00 目录

  • 遗传算法定义
  • 生物学术语
  • 问题导入
  • 大体实现
  • 具体细节
  • 代码实现

01 什么是遗传算法?

1.1 遗传算法的科学定义

遗传算法(Genetic Algorithm, GA)是模拟达尔文生物进化论的自然选择和遗传学机理的生物进化过程的计算模型,是一种通过模拟自然进化过程搜索最优解的方法。

其主要特点是直接对结构对象进行操作,不存在求导和函数连续性的限定;具有内在的隐并行性和更好的全局寻优能力;采用概率化的寻优方法,不需要确定的规则就能自动获取和指导优化的搜索空间,自适应地调整搜索方向。

遗传算法以一种群体中的所有个体为对象,并利用随机化技术指导对一个被编码的参数空间进行高效搜索。其中,选择、交叉和变异构成了遗传算法的遗传操作;参数编码、初始群体的设定、适应度函数的设计、遗传操作设计、控制参数设定五个要素组成了遗传算法的核心内容。

1.2 遗传算法的执行过程(参照百度百科)

遗传算法是从代表问题可能潜在的解集的一个种群(population)开始的,而一个种群则由经过基因(gene)编码的一定数目的个体(individual)组成。每个个体实际上是染色体(chromosome)带有特征的实体。

染色体作为遗传物质的主要载体,即多个基因的集合,其内部表现(即基因型)是某种基因组合,它决定了个体的形状的外部表现,如黑头发的特征是由染色体中控制这一特征的某种基因组合决定的。因此,在一开始需要实现从表现型到基因型的映射即编码工作。由于仿照基因编码的工作很复杂,我们往往进行简化,如二进制编码。

初代种群产生之后,按照适者生存和优胜劣汰的原理,逐代(generation)演化产生出越来越好的近似解,在每一代,根据问题域中个体的适应度(fitness)大小选择(selection)个体,并借助于自然遗传学的遗传算子(genetic operators)进行组合交叉(crossover)和变异(mutation),产生出代表新的解集的种群。

这个过程将导致种群像自然进化一样的后生代种群比前代更加适应于环境,末代种群中的最优个体经过解码(decoding),可以作为问题近似最优解。

1.3 遗传算法过程图解

02 相关生物学术语

为了大家更好了解遗传算法,在此之前先简单介绍一下相关生物学术语,大家了解一下即可。

  • 基因型(genotype):性状染色体的内部表现;

  • 表现型(phenotype):染色体决定的性状的外部表现,或者说,根据基因型形成的个体的外部表现;

  • 进化(evolution):种群逐渐适应生存环境,品质不断得到改良。生物的进化是以种群的形式进行的。

  • 适应度(fitness):度量某个物种对于生存环境的适应程度。

  • 选择(selection):以一定的概率从种群中选择若干个个体。一般,选择过程是一种基于适应度的优胜劣汰的过程。

  • 复制(reproduction):细胞分裂时,遗传物质DNA通过复制而转移到新产生的细胞中,新细胞就继承了旧细胞的基因。

  • 交叉(crossover):两个染色体的某一相同位置处DNA被切断,前后两串分别交叉组合形成两个新的染色体。也称基因重组或杂交;

  • 变异(mutation):复制时可能(很小的概率)产生某些复制差错,变异产生新的染色体,表现出新的性状。

  • 编码(coding):DNA中遗传信息在一个长链上按一定的模式排列。遗传编码可看作从表现型到基因型的映射。

  • 解码(decoding):基因型到表现型的映射。

  • 个体(individual):指染色体带有特征的实体;

  • 种群(population):个体的集合,该集合内个体数称为种群

03 问题引出与解决

3.1 一元函数最大值问题

如下的函数图像:

现在我们要在既定的区间内找出函数的最大值。

学过高中数学的孩纸都知道,上面的函数存在着很多的极大值和极小值。而最大值则是指定区间的极大值中的最大的那一个。从图像上具体表现为,极大值像是一座座山峰,极小值则是像一座座山谷。因此,我们也可以把遗传算法的过程看作是一个在多元函数里面求最优解的过程。

这些山峰对应着局部最优解,其中有一个山峰是海拔最高的,这个山峰则对应的是全局最优解。那么,遗传算法要做的就是尽量爬到最高峰,而不是困在较低的小山峰上。(如果问题求解是最小值,那么要做的就是尽量走到最低谷,道理是一样的)。

3.2 “袋鼠蹦跳”

既然我们把函数曲线理解成一个一个山峰和山谷组成的山脉。那么我们可以设想所得到的每一个解就是一只袋鼠,我们希望它们不断的向着更高处跳去,直到跳到最高的山峰。所以求最大值的过程就转化成一个“袋鼠跳”的过程。

下面介绍介绍“袋鼠跳”的几种方式。

  • 爬山算法:一只袋鼠朝着比现在高的地方跳去。它找到了不远处的最高的山峰。但是这座山不一定是最高峰。这就是爬山算法,它不能保证局部最优值就是全局最优值。

  • 模拟退火:袋鼠喝醉了。它随机地跳了很长时间。这期间,它可能走向高处,也可能踏入平地。但是,它渐渐清醒了并朝最高峰跳去。这就是模拟退火算法。

  • 遗传算法:有很多袋鼠,它们降落到喜玛拉雅山脉的任意地方。这些袋鼠并不知道它们的任务是寻找珠穆朗玛峰。但每过几年,就在一些海拔高度较低的地方射杀一些袋鼠。于是,不断有袋鼠死于海拔较低的地方,而越是在海拔高的袋鼠越是能活得更久,也越有机会生儿育女。就这样经过许多年,这些袋鼠们竟然都不自觉地聚拢到了一个个的山峰上,可是在所有的袋鼠中,只有聚拢到珠穆朗玛峰的袋鼠被带回了美丽的澳洲。

04 大体实现过程

遗传算法中每一条染色体,对应着遗传算法的一个解决方案,一般我们用适应性函数(fitness function)来衡量这个解决方案的优劣。所以从一个基因组到其解的适应度形成一个映射。遗传算法的实现过程实际上就像自然界的进化过程那样。

下面我们用袋鼠跳中的步骤一一对应解释,以方便大家理解:

1) 首先寻找一种对问题潜在解进行“数字化”编码的方案。(建立表现型和基因型的映射关系)

2) 随机初始化一个种群(那么第一批袋鼠就被随意地分散在山脉上),种群里面的个体就是这些数字化的编码。

3) 接下来,通过适当的解码过程之后(得到袋鼠的位置坐标)。

4) 用适应性函数对每一个基因个体作一次适应度评估(袋鼠爬得越高当然就越好,所以适应度相应越高)。

5) 用选择函数按照某种规定择优选择(每隔一段时间,射杀一些所在海拔较低的袋鼠,以保证袋鼠总体数目持平。)。

6) 让个体基因变异(让袋鼠随机地跳一跳)。

7) 然后产生子代(希望存活下来的袋鼠是多产的,并在那里生儿育女)。

遗传算法并不保证你能获得问题的最优解,但是使用遗传算法的最大优点在于你不必去了解和操心如何去“找”最优解。(你不必去指导袋鼠向那边跳,跳多远。)而只要简单的“否定”一些表现不好的个体就行了。(把那些总是爱走下坡路的袋鼠射杀,这就是遗传算法的精粹!)

由此我们可以得出遗传算法的一般步骤:

1) 随机产生种群。
2) 根据策略判断个体的适应度,是否符合优化准则,若符合,输出最佳个体及其最优解,结束。否则,进行下一步。
3) 依据适应度选择父母,适应度高的个体被选中的概率高,适应度低的个体被淘汰。
4) 用父母的染色体按照一定的方法进行交叉,生成子代。
5) 对子代染色体进行变异。

由交叉和变异产生新一代种群,返回步骤2,直到最优解产生。

具体图解可以回到1.3查看。

05 开始我们的进化(具体实现细节)

5.1 先从编码说起

编码是应用遗传算法时要解决的首要问题,也是设计遗传算法时的一个关键步骤。编码方法影响到交叉算子、变异算子等遗传算子的运算方法,大很大程度上决定了遗传进化的效率。

迄今为止人们已经提出了许多种不同的编码方法。总的来说,这些编码方法可以分为三大类:二进制编码法、浮点编码法、符号编码法。下面分别进行介绍:

5.1.1 二进制编码法

就像人类的基因有AGCT 4种碱基序列一样。不过在这里我们只用了0和1两种碱基,然后将他们串成一条链形成染色体。一个位能表示出2种状态的信息量,因此足够长的二进制染色体便能表示所有的特征。这便是二进制编码。如下:
1110001010111

它由二进制符号0和1所组成的二值符号集。它有以下一些优点:

1) 编码、解码操作简单易行
2) 交叉、变异等遗传操作便于实现
3) 合最小字符集编码原则
4) 利用模式定理对算法进行理论分析。

二进制编码的缺点是:对于一些连续函数的优化问题,由于其随机性使得其局部搜索能力较差,如对于一些高精度的问题(如上题),当解迫近于最优解后,由于其变异后表现型变化很大,不连续,所以会远离最优解,达不到稳定。

5.1.2 浮点编码法

二进制编码虽然简单直观,但明显地。但是存在着连续函数离散化时的映射误差。个体长度较短时,可能达不到精度要求,而个体编码长度较长时,虽然能提高精度,但增加了解码的难度,使遗传算法的搜索空间急剧扩大。

所谓浮点法,是指个体的每个基因值用某一范围内的一个浮点数来表示。在浮点数编码方法中,必须保证基因值在给定的区间限制范围内,遗传算法中所使用的交叉、变异等遗传算子也必须保证其运算结果所产生的新个体的基因值也在这个区间限制范围内。如下所示:

1.2-3.2-5.3-7.2-1.4-9.7

浮点数编码方法有下面几个优点:

1) 适用于在遗传算法中表示范围较大的数
2) 适用于精度要求较高的遗传算法
3) 便于较大空间的遗传搜索
4) 改善了遗传算法的计算复杂性,提高了运算交率
5) 便于遗传算法与经典优化方法的混合使用
6) 便于设计针对问题的专门知识的知识型遗传算子
7) 便于处理复杂的决策变量约束条件

5.1.3 符号编码法

符号编码法是指个体染色体编码串中的基因值取自一个无数值含义、而只有代码含义的符号集如{A,B,C…}。
符号编码的主要优点是:

1) 符合有意义积术块编码原则
2) 便于在遗传算法中利用所求解问题的专门知识
3) 便于遗传算法与相关近似算法之间的混合使用。

5.2 为我们的袋鼠染色体编码

在上面介绍了一系列编码方式以后,那么,如何利用上面的编码来为我们的袋鼠染色体编码呢?首先我们要明确一点:编码无非就是建立从基因型到表现型的映射关系。这里的表现型可以理解为个体特征(比如身高、体重、毛色等等)。那么,在此问题下,我们关心的个体特征就是:袋鼠的位置坐标(因为我们要把海拔低的袋鼠给杀掉)。无论袋鼠长什么样,爱吃什么。我们关心的始终是袋鼠在哪里,并且只要知道了袋鼠的位置坐标(位置坐标就是相应的染色体编码,可以通过解码得出),我们就可以:

1) 在喜马拉雅山脉的地图上找到相应的位置坐标,算出海拔高度。(相当于通过自变量求得适应函数的值)然后判读该不该射杀该袋鼠。
2) 可以知道染色体交叉和变异后袋鼠新的位置坐标。

回到3.1中提的求一元函数最大值的问题。在上面我们把极大值比喻为山峰,那么,袋鼠的位置坐标可以比喻为区间[-1, 2]的某一个x坐标(有了x坐标,再通过函数表达式可以算出函数值 <==> 得到了袋鼠染色体编码,解码得到位置坐标,在喜马拉雅山脉地图查询位置坐标算出海拔高度)。这个x坐标是一个实数,现在,说白了就是怎么对这个x坐标进行编码。下面我们以二进制编码为例讲解,不过这种情况下以二进制编码比较复杂就是了。(如果以浮点数编码,其实就很简洁了,就一浮点数而已。)

我们说过,一定长度的二进制编码序列,只能表示一定精度的浮点数。在这里假如我们要求解精确到六位小数,由于区间长度为2 - (-1) = 3 ,为了保证精度要求,至少把区间[-1,2]分为3 × 10^6等份。又因为

2^21 = 2097152 < 3*10^6 < 2^22 = 4194304

所以编码的二进制串至少需要22位。

把一个二进制串(b0,b1,….bn)转化为区间里面对应的实数值可以通过下面两个步骤:

1) 将一个二进制串代表的二进制数转化为10进制数:

2) 对应区间内的实数:

例如一个二进制串(1000101110110101000111)2通过上面换算以后,表示实数值0.637197。

好了,上面的编码方式只是举个例子让大家更好理解而已,编码的方式千奇百怪,层出不穷,每个问题可能采用的编码方式都不一样。在这一点上大家要注意。

5.3 评价个体的适应度–适应度函数(fitness function)

前面说了,适应度函数主要是通过个体特征从而判断个体的适应度。在本例的袋鼠跳中,我们只关心袋鼠的海拔高度,以此来判断是否该射杀该袋鼠。这样一来,该函数就非常简单了。只要输入袋鼠的位置坐标,在通过相应查找运算,返回袋鼠当前位置的海拔高度就行。

适应度函数也称评价函数,是根据目标函数确定的用于区分群体中个体好坏的标准。适应度函数总是非负的,而目标函数可能有正有负,故需要在目标函数与适应度函数之间进行变换。

评价个体适应度的一般过程为:

  1. 对个体编码串进行解码处理后,可得到个体的表现型。

  2. 由个体的表现型可计算出对应个体的目标函数值。

  3. 根据最优化问题的类型,由目标函数值按一定的转换规则求出个体的适应度。

5.4 射杀一些袋鼠–选择函数(selection)

遗传算法中的选择操作就是用来确定如何从父代群体中按某种方法选取那些个体,以便遗传到下一代群体。选择操作用来确定重组或交叉个体,以及被选个体将产生多少个子代个体。前面说了,我们希望海拔高的袋鼠存活下来,并尽可能繁衍更多的后代。但我们都知道,在自然界中,适应度高的袋鼠越能繁衍后代,但这也是从概率上说的而已。毕竟有些适应度低的袋鼠也可能逃过我们的眼睛。

那么,怎么建立这种概率关系呢?

下面介绍几种常用的选择算子:

  1. 轮盘赌选择(Roulette Wheel Selection):是一种回放式随机采样方法。每个个体进入下一代的概率等于它的适应度值与整个种群中个体适应度值和的比例。选择误差较大。

  2. 随机竞争选择(Stochastic Tournament):每次按轮盘赌选择一对个体,然后让这两个个体进行竞争,适应度高的被选中,如此反复,直到选满为止。

  3. 最佳保留选择:首先按轮盘赌选择方法执行遗传算法的选择操作,然后将当前群体中适应度最高的个体结构完整地复制到下一代群体中。

  4. 无回放随机选择(也叫期望值选择Excepted Value Selection):根据每个个体在下一代群体中的生存期望来进行随机选择运算。方法如下:

    (1) 计算群体中每个个体在下一代群体中的生存期望数目N。

    (2) 若某一个体被选中参与交叉运算,则它在下一代中的生存期望数目减去0.5,若某一个体未 被选中参与交叉运算,则它在下一代中的生存期望数目减去1.0。

    (3) 随着选择过程的进行,若某一个体的生存期望数目小于0时,则该个体就不再有机会被选中。

  5. 确定式选择:按照一种确定的方式来进行选择操作。具体操作过程如下:

    (1) 计算群体中各个个体在下一代群体中的期望生存数目N。

    (2) 用N的整数部分确定各个对应个体在下一代群体中的生存数目。

    (3) 用N的小数部分对个体进行降序排列,顺序取前M个个体加入到下一代群体中。至此可完全确定出下一代群体中M个个体。

  6. 无回放余数随机选择:可确保适应度比平均适应度大的一些个体能够被遗传到下一代群体中,因而选择误差比较小。

  7. 均匀排序:对群体中的所有个体按期适应度大小进行排序,基于这个排序来分配各个个体被选中的概率。

  8. 最佳保存策略:当前群体中适应度最高的个体不参与交叉运算和变异运算,而是用它来代替掉本代群体中经过交叉、变异等操作后所产生的适应度最低的个体。

  9. 随机联赛选择:每次选取几个个体中适应度最高的一个个体遗传到下一代群体中。

  10. 排挤选择:新生成的子代将代替或排挤相似的旧父代个体,提高群体的多样性。

下面以轮盘赌选择为例给大家讲解一下:

假如有5条染色体,他们的适应度分别为5、8、3、7、2。

那么总的适应度为:F = 5 + 8 + 3 + 7 + 2 = 25。

那么各个个体的被选中的概率为:

α1 = ( 5 / 25 ) * 100% = 20%

α2 = ( 8 / 25 ) * 100% = 32%

α3 = ( 3 / 25 ) * 100% = 12%

α4 = ( 7 / 25 ) * 100% = 28%

α5 = ( 2 / 25 ) * 100% = 8%

所以转盘如下:

当指针在这个转盘上转动,停止下来时指向的个体就是天选之人啦。可以看出,适应性越高的个体被选中的概率就越大。

5.5 遗传–染色体交叉(crossover)

遗传算法的交叉操作,是指对两个相互配对的染色体按某种方式相互交换其部分基因,从而形成两个新的个体。

适用于二进制编码个体或浮点数编码个体的交叉算子:

  1. 单点交叉(One-point Crossover):指在个体编码串中只随机设置一个交叉点,然后再该点相互交换两个配对个体的部分染色体。

  2. 两点交叉与多点交叉:

    (1) 两点交叉(Two-point Crossover):在个体编码串中随机设置了两个交叉点,然后再进行部分基因交换。

    (2) 多点交叉(Multi-point Crossover)

  3. 均匀交叉(也称一致交叉,Uniform Crossover):两个配对个体的每个基因座上的基因都以相同的交叉概率进行交换,从而形成两个新个体。

  4. 算术交叉(Arithmetic Crossover):由两个个体的线性组合而产生出两个新的个体。该操作对象一般是由浮点数编码表示的个体。

咳咳,根据国际惯例。还是抓一个最简单的二进制单点交叉为例来给大家讲解讲解。

二进制编码的染色体交叉过程非常类似高中生物中所讲的同源染色体的联会过程――随机把其中几个位于同一位置的编码进行交换,产生新的个体。

对应的二进制交叉:

5.6 变异–基因突变(Mutation)

遗传算法中的变异运算,是指将个体染色体编码串中的某些基因座上的基因值用该基因座上的其它等位基因来替换,从而形成新的个体。

例如下面这串二进制编码:

101101001011001

经过基因突变后,可能变成以下这串新的编码:

001101011011001

以下变异算子适用于二进制编码和浮点数编码的个体:

  1. 基本位变异(Simple Mutation):对个体编码串中以变异概率、随机指定的某一位或某几位仅因座上的值做变异运算。

  2. 均匀变异(Uniform Mutation):分别用符合某一范围内均匀分布的随机数,以某一较小的概率来替换个体编码串中各个基因座上的原有基因值。(特别适用于在算法的初级运行阶段)

  3. 边界变异(Boundary Mutation):随机的取基因座上的两个对应边界基因值之一去替代原有基因值。特别适用于最优点位于或接近于可行解的边界时的一类问题。

  4. 非均匀变异:对原有的基因值做一随机扰动,以扰动后的结果作为变异后的新基因值。对每个基因座都以相同的概率进行变异运算之后,相当于整个解向量在解空间中作了一次轻微的变动。

  5. 高斯近似变异:进行变异操作时用符号均值为P的平均值,方差为P**2的正态分布的一个随机数来替换原有的基因值。

06 代码实现环节

好了,上面我们介绍了一大截具体原理。现在就是把各个具体的零部件组装起来,动手写我们的代码了。

代码说明

遗传算法解决TSP旅行商问题

算法分为4个类:

  • GeneticAlgorithm

  • SpeciesIndividual

  • SpeciesPopulation

  • TSPData

数据规模: 10 cities, 20 cities and 31 cities.

类说明:

  • GeneticAlgorithm: 遗传算法的主体部分,包括选择、交叉、变异

  • SpeciesIndividual: 物种个体类

  • SpeciesPopulation: 物种种群类

  • TSPData: TSP数据类

  • MainRun: 主函数运行类

运行平台:

eclipse + windows10

详细代码

MainRun.java

主函数运行类,也就是程序入口。在这里创建算法类,创建种群,并开始运行我们的算法。得出结果以后,打印出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package GeneticTSP;

/**
* 主函数运行类
*/

public class MainRun {

public static void main(String[] args) {
// TODO Auto-generated method stub

//创建遗传算法驱动对象
GeneticAlgorithm GA=new GeneticAlgorithm();

//创建初始种群
SpeciesPopulation speciesPopulation = new SpeciesPopulation();

//开始遗传算法(选择算子、交叉算子、变异算子)
SpeciesIndividual bestRate=GA.run(speciesPopulation);

//打印路径与最短距离
bestRate.printRate();

}

}

TSPData.java

测试数据类,在这里做城市坐标数据等的输入。我直接写死在程序里了,有需要的朋友直接改动一下做一个IO就差不多了。disMap城市距离矩阵,记录各个城市间的距离。比如disMap[i][j]就是城市i和城市j之间的距离。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package GeneticTSP;

/**
* TSP数据类
* 包含:
* disMap 各个城市间距离矩阵
*/

public class TSPData {

static int CITY_NUM; //城市数
static final int SPECIES_NUM=200; //种群数
static final int DEVELOP_NUM=1000; //进化代数
static final float pcl=0.6f,pch=0.95f;//交叉概率
static final float pm=0.4f;//变异概率
static final float[][] disMap; //地图数据
static
{
// int[][] cityPosition={
// {0,0},{12,32},{5,25},{8,45},{33,17},
// {25,7},{15,15},{15,25},{25,15},{41,12}};//10个城市(最优解:147)
// int[][] cityPosition={
// {60,200},{180,200},{80,180},{140,180},
// {20,160},{100,160},{200,160},{140,140},
// {40,120},{100,120},{180,100},{60,80},
// {120,80},{180,60},{20,40},{100,40},
// {200,40},{20,20},{60,20},{160,20}};//20个城市(最优解:870)
//
//城市坐标集合
int[][] cityPosition={
{1304, 2312},{3639, 1315},
{4177, 2244},{3712, 1399},
{3488, 1535},{3326, 1556},
{3238, 1229},{4196, 1004},
{4312, 790},{4386, 570},
{3007, 1970},{2562, 1756},
{2788, 1491},{2381, 1676},
{1332, 695},{3715, 1678},
{3918, 2179},{4061, 2370},
{3780, 2212},{3676, 2578},
{4029, 2838},{4263, 2931},
{3429, 1908},{3507, 2367},
{3394, 2643},{3439, 3201},
{2935, 3240},{3140, 3550},
{2545, 2357},{2778, 2826},
{2370, 2975}};//31个城市(最优解:14700)

//路径集合
CITY_NUM=cityPosition.length;
disMap=new float[CITY_NUM][CITY_NUM];
for(int i=0;i<CITY_NUM;i++)
{
for(int j=i;j<CITY_NUM;j++)
{
float dis=(float)Math.sqrt(Math.pow((cityPosition[i][0] - cityPosition[j][0]),2) + Math.pow((cityPosition[i][1] - cityPosition[j][1]),2));

disMap[i][j]=dis;
disMap[j][i]=disMap[i][j];
}
}
}

}

SpeciesIndividual.java

物种个体类,每一个个体的染色体对应着一个解决方案。下面做几点说明:

基因:这里要解决的是TSP问题,因此我们直接采用城市序列作为基因的编码。染色体由随机排列的基因组成。

物种适应度:我们说了,物种适应度是评判物种个体的好坏的一个标准。那么,对于TSP问题,解决方案的总距离越小当然就越好了。因此我们直接用了总距离的倒数作为物种适应度。那么,总距离越小,物种适应度相应就越大了。

最后再加一个打印解决方案的方法,就是把城市排列输出来。至于贪婪法生成基因,大家了解一下,这里不做介绍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
package GeneticTSP;

import java.util.Random;

/**
* 个体类
* 包含:
* 1.createByRandomGenes 初始物种基因(随机) 基因直接用城市序列编码
* 2.calFitness 计算物种适应度
* 3.printRate 打印路径
*/

public class SpeciesIndividual {

String[] genes;//基因序列
float distance;//路程
float fitness;//适应度
SpeciesIndividual next;
float rate;

SpeciesIndividual()
{
//初始化
this.genes=new String[TSPData.CITY_NUM];
this.fitness=0.0f;
this.distance=0.0f;
this.next=null;
rate=0.0f;
}

//初始物种基因(随机)
void createByRandomGenes()
{
//初始化基因为1-CITY_NUM序列
for(int i = 0;i < genes.length;i++)
{
genes[i]=Integer.toString(i+1);
}

//获取随机种子
Random rand=new Random();

for(int j=0;j<genes.length;j++)
{
int num= j + rand.nextInt(genes.length-j);

//交换
String tmp;
tmp=genes[num];
genes[num]=genes[j];
genes[j]=tmp;
}
}

//初始物种基因(贪婪)
void createByGreedyGenes()
{
Random rand=new Random();
int i= rand.nextInt(TSPData.CITY_NUM); //随机产生一个城市作为起点
genes[0]=Integer.toString(i+1);
int j;//终点
int cityNum=0;
do
{
cityNum++;

//选出单源最短城市
float minDis=Integer.MAX_VALUE;
int minCity=0;
for(j=0;j<TSPData.CITY_NUM;j++)
{
if(j != i)
{
//判是否和已有重复
boolean repeat=false;
for(int n=0;n<cityNum;n++)
{
if(Integer.parseInt(genes[n]) == j+1)
{
repeat=true;//重了
break;
}
}
if(repeat == false)//没重
{
//判长度
if(TSPData.disMap[i][j] < minDis)
{
minDis=TSPData.disMap[i][j];
minCity=j;
}
}
}
}

//加入到染色体
genes[cityNum]=Integer.toString(minCity+1);
i=minCity;
}while(cityNum < TSPData.CITY_NUM-1);
}

//计算物种适应度
void calFitness()
{
float totalDis=0.0f;
for(int i = 0;i < TSPData.CITY_NUM;i++)
{
int curCity=Integer.parseInt(this.genes[i])-1;
int nextCity=Integer.parseInt(this.genes[(i+1) % TSPData.CITY_NUM])-1;

totalDis += TSPData.disMap[curCity][nextCity];
}

this.distance=totalDis;
this.fitness=1.0f/totalDis;
}

//深拷贝
public SpeciesIndividual clone()
{
SpeciesIndividual species=new SpeciesIndividual();

//复制值
for(int i=0;i<this.genes.length;i++)
species.genes[i]=this.genes[i];
species.distance=this.distance;
species.fitness=this.fitness;

return species;
}

//打印路径
void printRate()
{
System.out.print("最短路线:");
for(int i=0;i<genes.length;i++)
System.out.print(genes[i]+"->");
System.out.print(genes[0]+"\n");
System.out.print("最短长度:" + distance);
}

}

SpeciesPopulation.java

种群类,总群由物种组成。该类功能主要是把物种聚集起来形成总群的。我们姑且就当做一个物种只有一个个体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package GeneticTSP;

/**
* 种群类
* 包含:
* 1.add 添加物种
* 2.traverse 遍历
*/

public class SpeciesPopulation {

SpeciesIndividual head;//头结点
int speciesNum;//物种数量

SpeciesPopulation()
{
head=new SpeciesIndividual();
speciesNum=TSPData.SPECIES_NUM;
}

//添加物种
void add(SpeciesIndividual species)
{
SpeciesIndividual point=head;//游标
while(point.next != null)//寻找表尾结点
point=point.next;
point.next=species;
}

//遍历
void traverse()
{
SpeciesIndividual point=head.next;//游标
while(point != null)//寻找表尾结点
{
for(int i=0;i<TSPData.CITY_NUM;i++)
System.out.print(point.genes[i]+" ");
System.out.println(point.distance);
point=point.next;
}
System.out.println("_______________________");
}

}

GeneticAlgorithm.java

重头戏来了。下面重点介绍GA算法类中的几个方法:

  • createBeginningSpecies

    创建种群,100%随机创建或者40%贪婪创建。40%贪婪创建就是40的物种采用贪婪算法生成基因。物种数由TSPData类中的物种数指定。

  • calRate

    计算每一物种被选中的概率。物种个体中的rate变量记录了该概率。

  • select

    轮盘制选择物种进行染色体交叉。这里的策略讲讲,我们的目标是选出优秀个体染色体交叉生成新的种群。然后再提一句,该方法只是选择个体而已,还没进行交叉操作。

    1) 我们先找出最大适应度的个体。然后复制该个体到新种群去,复制数量占原来种群的1/4。

    2) 然后新种群的3/4我们采用轮盘制选择,选择概率随机产生,毕竟大自然选择也是看天意的……然后每次选择那些,选中的概率(前面算出来了)大于或等于随机概率的个体,塞进新种群。注意边界处理。

  • crossover

    交叉操作,以一定的概率区间进行。详细说明一下步骤:

    1) 先随机找出两个个体(个体point和个体point.next)。

    2) 在一定的概率区间。对个体point和个体point.next进行如下操作:

循环 i to city_num(i随机产生)

找出point.genes中与point.next.genes[i]相等的位置fir

找出point.next.genes中与point.genes[i]相等的位置sec

然后执行如下交换操作:

直到结束循环

  • mutate

    变异操作。每一种物种都有变异的可能,我们以一定概率进行。在这个TSP问题中,我们采用的变异操作其实跟迭代搜索那个two opt有点类似。在基因序列中,随机产生i~j的区间,然后将区间反转,形成新的染色体。很easy吧……

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
package GeneticTSP;

import java.util.Random;

/**
* 遗传算法类
* 包含:
* 1.run 开始跑算法
* 2.createBeginningSpecies 创建种群
* 3.calRate 计算每一种物种被选中的概率
* 4.select 轮盘策略 选择适应度高的物种
* 5.crossover 染色体交叉
* 6.mutate 染色体变异
* 7.getBest 获得适应度最大的物种
*/

public class GeneticAlgorithm {

//开始遗传
SpeciesIndividual run(SpeciesPopulation list)
{
//创建初始种群
createBeginningSpecies(list);

for(int i=1;i<=TSPData.DEVELOP_NUM;i++)
{
//选择
select(list);

//交叉
crossover(list);

//变异
mutate(list);
}

return getBest(list);
}

//创建初始种群
void createBeginningSpecies(SpeciesPopulation list)
{
//100%随机
int randomNum=(int)(TSPData.SPECIES_NUM);
for(int i=1;i<=randomNum;i++)
{
SpeciesIndividual species=new SpeciesIndividual();//创建结点
species.createByRandomGenes();//初始种群基因

list.add(species);//添加物种
}

// //40%贪婪
// int greedyNum=TSPData.SPECIES_NUM-randomNum;
// for(int i=1;i<=greedyNum;i++)
// {
// SpeciesIndividual species=new SpeciesIndividual();//创建结点
// species.createByGreedyGenes();//初始种群基因
//
// this.add(species);//添加物种
// }
}

//计算每一物种被选中的概率
void calRate(SpeciesPopulation list)
{
//计算总适应度
float totalFitness=0.0f;
list.speciesNum=0;
SpeciesIndividual point=list.head.next;//游标
while(point != null)//寻找表尾结点
{
point.calFitness();//计算适应度

totalFitness += point.fitness;
list.speciesNum++;

point=point.next;
}

//计算选中概率
point=list.head.next;//游标
while(point != null)//寻找表尾结点
{
point.rate=point.fitness/totalFitness;
point=point.next;
}
}

//选择优秀物种(轮盘赌)
void select(SpeciesPopulation list)
{
//计算适应度
calRate(list);

//找出最大适应度物种
float talentDis=Float.MAX_VALUE;
SpeciesIndividual talentSpecies=null;
SpeciesIndividual point=list.head.next;//游标

while(point!=null)
{
if(talentDis > point.distance)
{
talentDis=point.distance;
talentSpecies=point;
}
point=point.next;
}

//将最大适应度物种复制talentNum个
SpeciesPopulation newSpeciesPopulation=new SpeciesPopulation();
int talentNum=(int)(list.speciesNum/4);
for(int i=1;i<=talentNum;i++)
{
//复制物种至新表
SpeciesIndividual newSpecies=talentSpecies.clone();
newSpeciesPopulation.add(newSpecies);
}

//轮盘赌list.speciesNum-talentNum次
int roundNum=list.speciesNum-talentNum;
for(int i=1;i<=roundNum;i++)
{
//产生0-1的概率
float rate=(float)Math.random();

SpeciesIndividual oldPoint=list.head.next;//游标
while(oldPoint != null && oldPoint != talentSpecies)//寻找表尾结点
{
if(rate <= oldPoint.rate)
{
SpeciesIndividual newSpecies=oldPoint.clone();
newSpeciesPopulation.add(newSpecies);

break;
}
else
{
rate=rate-oldPoint.rate;
}
oldPoint=oldPoint.next;
}
if(oldPoint == null || oldPoint == talentSpecies)
{
//复制最后一个
point=list.head;//游标
while(point.next != null)//寻找表尾结点
point=point.next;
SpeciesIndividual newSpecies=point.clone();
newSpeciesPopulation.add(newSpecies);
}

}
list.head=newSpeciesPopulation.head;
}

//交叉操作
void crossover(SpeciesPopulation list)
{
//以概率pcl~pch进行
float rate=(float)Math.random();
if(rate > TSPData.pcl && rate < TSPData.pch)
{
SpeciesIndividual point=list.head.next;//游标
Random rand=new Random();
int find=rand.nextInt(list.speciesNum);
while(point != null && find != 0)//寻找表尾结点
{
point=point.next;
find--;
}

if(point.next != null)
{
int begin=rand.nextInt(TSPData.CITY_NUM);

//取point和point.next进行交叉,形成新的两个染色体
for(int i=begin;i<TSPData.CITY_NUM;i++)
{
//找出point.genes中与point.next.genes[i]相等的位置fir
//找出point.next.genes中与point.genes[i]相等的位置sec
int fir,sec;
for(fir=0;!point.genes[fir].equals(point.next.genes[i]);fir++);
for(sec=0;!point.next.genes[sec].equals(point.genes[i]);sec++);
//两个基因互换
String tmp;
tmp=point.genes[i];
point.genes[i]=point.next.genes[i];
point.next.genes[i]=tmp;

//消去互换后重复的那个基因
point.genes[fir]=point.next.genes[i];
point.next.genes[sec]=point.genes[i];

}
}
}
}

//变异操作
void mutate(SpeciesPopulation list)
{
//每一物种均有变异的机会,以概率pm进行
SpeciesIndividual point=list.head.next;
while(point != null)
{
float rate=(float)Math.random();
if(rate < TSPData.pm)
{
//寻找逆转左右端点
Random rand=new Random();
int left=rand.nextInt(TSPData.CITY_NUM);
int right=rand.nextInt(TSPData.CITY_NUM);
if(left > right)
{
int tmp;
tmp=left;
left=right;
right=tmp;
}

//逆转left-right下标元素
while(left < right)
{
String tmp;
tmp=point.genes[left];
point.genes[left]=point.genes[right];
point.genes[right]=tmp;

left++;
right--;
}
}
point=point.next;
}
}

//获得适应度最大的物种
SpeciesIndividual getBest(SpeciesPopulation list)
{
float distance=Float.MAX_VALUE;
SpeciesIndividual bestSpecies=null;
SpeciesIndividual point=list.head.next;//游标
while(point != null)//寻找表尾结点
{
if(distance > point.distance)
{
bestSpecies=point;
distance=point.distance;
}

point=point.next;
}

return bestSpecies;
}

}

以上就是遗传算法的java代码。

运行结果:

10个城市(最优解:147)

20个城市(最优解:870)

31个城市(最优解:14700)

最后在多说一句,这代码跑不出最优解也正常。启发式算法求近似解还是得靠人品的胸弟。

上一篇