遗传算法求解 TSP 旅行商问题机及其实现


一、TSP 概述

1. TSP

旅行商问题即 TSP(Traveling Salesman Problem),又称为货郎担问题,是数学领域中著名问题之一。假设有一个旅行商人要拜访n个城市,他必须选择所要走的路径,路径的限制是每个城市只能拜访一次,而且最后要回到原来出发的城市。路径的选择目标是要求得的路径路程为所有路径之中的最小值。
TSP问题是一个组合优化问题,该问题可以被证明具有NPC计算复杂性。从图论的角度来看,该问题实质是给定一个带权完全无向图(顶点表示城市,边表示道路,权重是道路的成本或距离),找一个权值最小的 Hamilton 回路。

2. 数学模型

记 G = (V, E) 为赋权图,V = {1, 2 , ……,n} 为顶点集,E 为边集,各顶点间距离为 Cij,已知(Cij > 0,Cij = +∞,i,j ∈ V),并设
x i j = { 1 , 边 ( i , j ) 在 最 优 路 线 上 0 , 其 他 x_{ij}= \begin{cases} 1,边(i, j)在最优路线上 \\ 0,其他 \end{cases} xij={1(i,j)线0
则旅行商问题的数学模型可写成如下的线性规划形式:
p = ∑ i ≠ j c i j x i j p = \sum_{i ≠ j} c_{ij}x_{ij} p=i=jcijxij
s . t . = { ∑ j ≠ i x i j = 1 , i ∈ V ∑ i ≠ j x i j = 1 , j ∈ V ∑ i , j ∈ K x i j ≤ ∣ K ∣ , K ⊂ V x i j ∈ { 0 , 1 } i , j ∈ V s.t.=\begin{cases} \sum_{j ≠ i} x_{ij} = 1 ,& i∈V \\ \sum_{i ≠ j} x_{ij} = 1 ,& j∈V \\ \sum_{i,j∈K} x_{ij} ≤|K| ,& K ⊂V \\ x_{ij}∈\{0, 1\} & i,j∈V\\ \end{cases} s.t.=j=ixij=1,i=jxij=1,i,jKxijK,xij{0,1}iVjVKVi,jV
K 为 V 的所有非空子集,|K| 为集合 K 中所含图 G 的顶点个数。前两个约束意味着对每个顶点而言,出度和入度都为1,后一约束则保证没有任何子回路解的产生,于是满足上述约束的解构成了一条 Hamilton 回路。

3. TSP分类

旅行商问题按把不同的分类方法可以分为不同的种类,这里只介绍距离矩阵划分: 当 cij = cji,(i, j ∈ V)时,问题被称为对称型旅行商问题,反之称为非对称型旅行商问题,非对称旅行商问题可以化为对称型旅行商问题,用对称型的方法求解。当对所有的 i, j, k ∈[1, n],有不等式 cij + cjk ≥ cik,问题满足三角形不等式的,也称三角型旅行商问题。
旅行售货商问题(TSP)是组合优化领域的经典问题之一,而其中考虑多个旅行商的多旅行商问题(MTSP)是经典的旅行商问题的扩展,多种扩展形式1如下:

  1. 最小哈密顿链的问题:起点和终点不同;
  2. 非对称旅行商问题(asymmetric TSP):距离矩阵非对称的旅行商问题;
  3. 多人旅行商问题(muti-person TSP):由多人完成旅行的旅行商问题;
  4. 多目标旅行商问题(multi-objective TSP);
  5. 依次排序问题(Sequence ordering problem ,SOP):这类问题是非对称旅行商问题,在给定一系列顶点和距离矩阵下,寻找最短从顶点 1 到顶点 n 哈密顿链,同时满足限制:某些顶点要在一些顶点之前被连接。
  6. 载货量受限制的车辆路径问题(Capacitated vehicle routing problem,CVRP):给定 n-1 个顶点和一个仓库,一直顶点和顶点、仓库和顶点的距离,卡车载货量受限制,卡车每次在部分顶点和仓库之间往返,寻求一条经过所有顶点的最短路线。

二、遗传算法

遗传算法(Genetic Algorithm)是模拟达尔文生物进化论的自然选择和遗传学机理的生物进化过程的计算模型,是一种通过模拟自然进化过程搜索最优解的方法。在求解较为复杂的组合优化问题时,相对一些常规的优化算法,通常能够较快地获得较好的优化结果。主要特点如下:

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

遗传算法以一种群体中的所有个体为对象,并利用随机化技术指导对一个被编码的参数空间进行高效搜索。选择交叉变异构成了遗传算法的遗传操作。

1. 算法思路

遗传算法的主要思路2:从代表问题可能潜在的解集的一个种群(population)开始,而一个种群则由经过基因编码的一定数目的个体(individual)组成。每个个体实际上是染色体(chromosome)带有特征的实体。
初代种群产生之后,按照适者生存和优胜劣汰的原理,迭代演化产生出越来越好的近似解,在每一代,根据问题域中个体的适应度(fitness)大小选择(selection)个体,并借助于自然遗传学的遗传算子进行组合交叉(crossover)和变异(mutation),产生出代表新的解集的种群。迭代过程将导致种群像自然进化一样的后生代种群比前代更加适应于环境,末代种群中的最优个体经过解码(decoding),可以作为问题近似最优解。

2. 算法流程

遗传算法是以适应度为依据的逐代搜索过程,主要有编码机制控制参数适应度函数遗传算子组成。基本遗传算法 GA 求解 TSP 问题过程如下3

  1. 编码
    确定编码机制,生成初始种群。解决 TSP 问题通常采用城市序号对路径进行编码,按照访问城市的顺序排列组成编码。
  2. 初始化种群
    初始化对象:种群规模 POP_NUM、城市数量 N、运行代数 GENERATE_COUNT、交叉概率 PC、变异概率 PM
    初始化数据:读入数据源,将坐标转换为距离矩阵;
    初始化种群:随机生成 POP_NUM 个路径序列;
  3. 计算种群适应度
    计算总距离的倒数作为每个个体的适应度,适应度越高,被保留概率越大。
  4. 计算累计概率
    计算初始化种群中各个个体的累积概率
  5. 迭代
    选择算子:通常采用精英个体保存策略和轮盘赌算法选择算子;
    交叉运算:按交叉概率选择父代第 k 个个体和 k+1 个个体有一定的概率交叉变换;
    变异运算:为保持种群个体 的多样性,防止陷入局部最优,按照变异概率随机变异个体;
  6. 迭代终止条件,若满足则停止迭代;否则转至 3,计算新一代的种群适应度以及个体累积概率,并更新最优解;
    遗传算法流程图

3. 关键算法

3.1 种群初始化

基本 GA 算法采用随机数产生初始种群,考虑到该种算法收敛结果较慢,且不容易得到最优解。因此结合贪婪算法对初始种群个体进行优化,利用贪婪算法局部寻优的优势产生新个体(具体见博客 贪心算法求解 TSP 旅行商问题及其实现)。贪婪算法生成的初始种群不失随机性,同时整体质量有所提高,有助于寻优速度。

3.2 选择

选择采用轮盘赌算法(比例选择算法),基于思想是各个个体被选中的概率与其适应度函数值大小成正比,在遗传算法中用于对个体进行优胜略汰操作。适应度高的个体被遗传到下一代群体中的概率大;适应度低的个体,被遗传到下一代群体中的概率小。
轮盘赌
步骤如下:

  1. 计算群体中所有个体的适应度值 popFit[i]
    p o p F i t [ i ] = 1 p o p D i s t a n c e [ i ] popFit[i] = \frac{1}{popDistance[i]} popFit[i]=popDistance[i]1
  2. 计算每个个体遗传到下一代的概率p[i]
    p [ i ] = p o p F i t [ i ] ∑ j = 1 N p o p F i t [ j ] p[i] = \frac{popFit[i]}{\sum\limits_{j=1}^{N}popFit[j]} p[i]=j=1NpopFit[j]popFit[i]
  3. 计算每个个体的累计概率;
    q i = ∑ j = 1 i p ( x j ) q_i = \sum\limits_{j = 1}^{i}p(x_j) qi=j=1ip(xj)
  4. 采用模拟赌盘操作(即生成0到1之间的随机数与每个个体遗传到下一代群体的概率进行匹配)来确定各个个体是否遗传到下一代群体中。
    具体实现:在 [0, 1] 中产生一个均匀分布的随机数 temp,若 temp ≤ q1,则染色体被选中;若 qk-1< r ≤ qk (2≤ k ≤ N),则染色体被选中。
    轮盘赌计算
    轮盘赌实例计算

3.3 交叉

交叉为两个相互配对的染色体依据交叉概率 PC 按某种方式相互交换其部分基因,形成两个新的个体 。基本的遗传算法在求解 TSP 问题时,收敛速度过慢,并且随着上搜索的进程,群体多样性水平越来越低,容易陷入局部最优解。笔者采用与贪心算法结合的遗传算法4主要针对交叉和变异方式进行了改进,增强其快速收敛的能力,以实现全局最优。
算法采用两种交叉算子,遗传迭代初期采用第一种交叉算子,当连续 GEN 代种群的适应度没有明显提高且最佳个体适应度也不再提高时采用第二种算子,扩展有限个体的覆盖面,体现全局搜索的思想。
假设有 n 个城市 1,2,……,n 染色体编码成推销员所经过的城市顺序号,现在有双亲为:
x 1 = ( r 11 , r 12 , … … , r 1 n ) x_1 = (r_{11}, r_{12},……, r_{1n}) x1=(r11,r12,,r1n) x 2 = ( r 21 , r 22 , … … , r 2 n ) x_2 = (r_{21}, r_{22},……, r_{2n}) x2=(r21,r22,,r2n)
将上述染色体看成环,即 r1n 的下一个城市为 r11

第一种交叉算子设计步骤如下
(1) 随机选择一个父代,假设选择 x1 ,在所选父代中随机选择一个城市作为当前城市,假设选择 r11,从 x2 中找到一个和 r11 相等的基因 r2j,并把 r11 加入子代,作为子代个体的第一个基因。
(2)若 r11 和 r 2j 紧邻着的下一个基因(设为 next(r11) 和 next(r2j) 都不在当前子代中,则比较 distance(r11, next(r11)distance(r2j, next(r2j))。若 distance(r11, next(r11)) > distance(r2j, next(r2j)),则把 next(r2j) 作为子代个体的第二个基因;否则就把 next(r11) 作为子代个体的第二个基因;
若 next(r11) 在子代中,而 next(r2j) 不在子代中则把 next(r2j) 加入子代;若 next(r2j) 在子代中,而 next(r11) 不在子代中,则把 next(r11) 加入到子代中;
若 next(r11) 和 next(r2j) 都在子代中则跳过此基因,继续比较 distance(r11, next(r11) 和 distance(r2j, next(r2j))。
(3)重复步骤(2),直到完整生成子代。
这种交叉算子很大程度上继承了父代优秀的基因,达到遗传的目的。
第二种交叉算子设计步骤如下
(1)随机生成两个 0~N 序号,表示个体染色体要交换的部分;
(2)将种群中 第 k 和 k+1 个体进行交换,并保存每个个体基因冲突的序号;
(3)根据序号消除个体基因冲突;

3.4 变异

遗传算法中的变异运算,是指将个体染色体编码串中的某些基因座上的基因值用该基因座上的其它等位基因来替换,从而形成新的个体。变异决定遗传算法的局部搜索能力,保持种群多样性。笔者为改进算法性能,变异算子采用 LK 算法,也称 r-opt 算法。这种算法从一条初始可行解出发,每一次从路径中选择 r 条边,如果能通过交换边减少路径的长度则交换;否则再选择另外的 r 条边进行同样的操作。如果在当前路径中任意 r 条边都不可能通过交换来获得更短的路径,则当前路径就是算法最终的解。r 条边可能有的交换形式共有 (2r-3)! 种,因此实践中采用的 r-opt 都是受限制的 r-opt 交换。这里采用 r = 2 的交换方式进行变异。

3.5 精英个体保留策略

在进行选择操作时,采用精英个体保留策略,将适应度数值最高个体直接复制纳入交叉配对的父代群体中。当所有父代个体执行完交叉算子后,再次沿用精英个体保留机制,用交叉前的精英个体替换交叉群体中适应度值最差的个体,剔除质量低的个体,使得精英个体得以延续。

三、遗传算法求解 TSP

算法使用的 TSP 数据集来自于 TSPLIB 上的 att48.tsp,这是一个对称 TSP 问题,城市规模为48,其最优值为10628。其距离计算方法如下:

The edge weight type ATT corresponds to a special “pseudo-Euclidean” distance function. Let x[ i ] and y[ i ] be the coordinates of node i. The distance between two points i and j is computed as follows:
double dis = sqrt((pow((double)x[i] - x[j], 2) / 10 + pow((double)y[i] - y[j], 2) / 10 ));
int disInt = (int)dis;
if(disInt < dis) dis = disInt + 1;
else dis = disInt;

代码具体实现如下:

/*********************************************************************************************************************
 * TSP 算例来自TSPLIB,att48.tsp 数据集,其中有 48 个城市,距离为伪欧式距离
 * TSPLIB is a library of sample instances for the TSP (and related problems)from various sources and of various types.
 * 目前最佳解总距离为 10628,其中距离的计算方式为 sqrt((x*x + y*y)/10)
 * 该程序使用遗传算法求解 TSP,解集最优总距离为 10648,相比贪心算法解集有较大改进,接近最优解
**********************************************************************************************************************/
#include<stdio.h>
#include<math.h>
#include<stdlib.h>
#include<time.h>

//城市数量 N
#define N 48
//标识城市是否被访问,访问过置为 1
bool visit[N];
//城市距离矩阵
int distance[N][N];
//种群数量
#define POP_NUM 100
//迭代次数
#define GENERATE_COUNT 5000
#define GEN 10
//交叉概率
#define PC1 0.6
#define PC2 0.8
//变异概率
#define PM 0.2
//种群
int population[POP_NUM][N];
//保留的精英个体
int currentOpt[N];
int currentOptDistance;
int minIndex;
int maxIndex;
//种群平均距离和总距离
int avgDistance[POP_NUM];
int popDistance[POP_NUM];
int selectPop[POP_NUM][N];

/***********************************************************************
 * Function   :init()
 * Description:从文件中读取城市坐标,并计算城市之间的距离矩阵 distance[N][N]
 * Input      :void
 * Output     :void
 * Return     :void
 ***********************************************************************/
void init()
{
	//城市的 x 和 y 坐标
	int x[N] = { 0 };
	int y[N] = { 0 };
	//从 data.txt 文件读取数据
	FILE* fp;
	if ((fp = fopen("..//att48.txt", "r")) == NULL)
		//if ((fp = fopen("..//kroB100.txt", "r")) == NULL)
	{
		printf("can not open the file!");
		exit(0);
	}
	while (!feof(fp))
	{
		int count;
		fscanf(fp, "%d", &count);
		fscanf(fp, "%d%d", &x[count - 1], &y[count - 1]);
	}
	fclose(fp);
	//计算城市之间距离
	for (int i = 0; i < N - 1; i++)
	{
		distance[i][i] = 0;				// 对角线为0
		for (int j = i + 1; j < N; j++)
		{
			double dis = sqrt((pow((double)x[i] - x[j], 2) / 10 + pow((double)y[i] - y[j], 2) / 10));
			int disInt = (int)dis;
			distance[i][j] = dis == disInt ? disInt : disInt + 1;
			distance[j][i] = distance[i][j];
		}
	}
	distance[N - 1][N - 1] = 0;
}

/***********************************************************************
 * Function   :TSPGreedyAlgorithm()
 * Description:采用贪心算法求出初始解,并作为初始种群的第一个个体
 * Input      :distance 数组
 * Output     :打印初始解路径和其总距离
 * Return     :void
 ***********************************************************************/
void TSPGreedyAlgorithm()
{
	int totalDistance = 0;		//总路程
	//默认从 0 开始遍历
	int current = 0;			//当前选取结点	
	visit[0] = 1;
	printf("TSP 路径为:%d ->", 1);

	//遍历 N - 1 次
	for (int i = 1; i < N; i++)
	{
		//设置较大的距离初始值用来选取最近邻
		int min_distance = 0x7fffffff;
		//保存当前最近邻城市
		int temp;
		//循环选取城市
		for (int j = 1; j < N; j++)
		{
			if (!visit[j] && distance[current][j] < min_distance)
			{
				min_distance = distance[current][j];
				temp = j;
			}
		}
		visit[temp] = 1;
		current = temp;
		currentOpt[i] = temp;
		population[0][i] = temp;
		totalDistance += min_distance;
		printf(" %d ->", temp + 1);
	}
	totalDistance += distance[current][0];
	currentOptDistance = totalDistance;
	printf(" %d\n", 1);
}

/***********************************************************************
 * Function   :printTest()
 * Description:打印种群(测试)
 * Input      :population 数组
 * Output     :打印所有种群个体
 * Return     :void
 ***********************************************************************/
void printTest()
{
	for (int i = 0; i < POP_NUM; i++)
	{
		for (int j = 0; j < N; j++)
		{
			printf("%d ", population[i][j]);
		}
		printf("\n");
	}
}

/***********************************************************************
 * Function   :swap(int array, int num)
 * Description:随机交换 num 次种群个体基因用于初始化
 * Input      :种群个体 array,交换次数 num
 * Output     :void
 * Return     :void
 ***********************************************************************/
void swap(int* array, int num)
{
	int first, last, temp;
	for (int i = 0; i < num; i++)
	{
		first = rand() % N;
		last = rand() % N;
		temp = array[first];
		array[first] = array[last];
		array[last] = temp;
	}
}

/***********************************************************************
 * Function   :reverse(int* array, int num)
 * Description:2-opt 交换,变异
 * Input      :种群个体 array,交换次数 num
 * Output     :void
 * Return     :void
 ***********************************************************************/
void reverse(int* array, int num)
{
	int first, last, temp;
	for (int i = 0; i < num; i++)
	{
		first = rand() % N;
		last = rand() % N;
		if (first > last)
		{
			temp = last;
			last = first;
			first = temp;
		}
		int sumBefore = 0;
		int sumAfter = 0;
		int sum = 0;
		for (int j = first; j < last; j++)
		{
			sum += distance[array[j]][array[j + 1]];
		}
		sumBefore = sum + distance[array[(first - 1) % N]][array[first]] + distance[array[last]][array[(last + 1) % N]];
		sumAfter = sum + distance[array[(first - 1) % N]][array[last]] + distance[array[first]][array[(last + 1) % N]];
		if (sumAfter <= sumBefore)
		{
			for (int j = first; j < (last + first) / 2; j++)
			{
				temp = array[j];
				array[j] = array[first + last - j];
				array[first + last - j] = temp;
			}
		}
	}
}

/***********************************************************************
 * Function   :calDistance()
 * Description:计算并更新种群所有个体总距离,同时采用精英个体保留策略
 * Input      :void
 * Output     :当前种群最优个体的总距离
 * Return     :void
 ***********************************************************************/
int calDistance()
{
	int minDistance = 0x7fffffff;
	int maxDistance = 0;
	int sum = 0;
	for (int i = 0; i < POP_NUM; i++)
	{
		popDistance[i] = 0;
		for (int j = 0; j < N - 1; j++)
		{
			popDistance[i] += distance[population[i][j]][population[i][j + 1]];
		}
		popDistance[i] += distance[population[i][N - 1]][population[i][0]];
		sum += popDistance[i];
		//计算最长、最短的个体路径长度
		if (popDistance[i] < minDistance)
		{
			minIndex = i;
			minDistance = popDistance[i];
		}
		if (popDistance[i] > maxDistance)
		{
			maxIndex = i;
			maxDistance = popDistance[i];
		}
	}
	//保存最优个体
	if (popDistance[minIndex] < currentOptDistance)
	{
		currentOptDistance = popDistance[minIndex];
		for (int i = 0; i < N; i++)
		{
			currentOpt[i] = population[minIndex][i];
		}
	}
	//淘汰最差的个体
	for (int i = 0; i < N; i++)
	{
		population[maxIndex][i] = currentOpt[i];
	}
	printf("当前最近距离为:%d\n", popDistance[minIndex]);
	return sum;
	//printf("当前最远距离为:%d\n", popDistance[maxIndex]);
}

/***********************************************************************
 * Function   :initPop()
 * Description:根据贪心算法求解的初始解,初始化种群和种群各个体距离
 * Input      :void
 * Output     :void
 * Return     :void
 ***********************************************************************/
void initPop()
{
	//随机交换得到目标数量种群
	for (int i = 1; i < POP_NUM; i++)
	{
		for (int j = 0; j < N; j++)
		{
			population[i][j] = population[0][(j + i) % N];
		}
		swap(population[i], 1);
	}
	calDistance();
}

/***********************************************************************
 * Function   :select()
 * Description:选择(计算适应度、累计概率、保留精英个体、轮盘赌算法)
 *				轮盘赌选择方法的实现步骤:
 *				(1)计算群体中所有个体的适应度值;
 *				(2)计算每个个体的选择概率;
 *				(3)计算积累概率;
 *				(4)模拟赌盘操作来确定各个个体是否遗传到下一代群体中;
 * Input      :void
 * Output     :void
 * Return     :void
 ***********************************************************************/
void select()
{
	double popFit[POP_NUM];				//种群个体适应度
	double p[POP_NUM];					//种群个体的选择概率
	double sum = 0;
	//适应度
	for (int i = 0; i < POP_NUM; i++)
	{
		popFit[i] = 10000.0 / popDistance[i];	//适应度函数之为距离的倒数,注意分子为 double 结果才为 double
		sum += popFit[i];
	}
	//累计概率
	for (int i = 0; i < POP_NUM; i++)
	{
		p[i] = popFit[i] / sum;
	}
	//保留精英个体
	for (int k = 0; k < N; k++)
	{
		selectPop[0][k] = population[minIndex][k];
	}
	//轮盘赌选择
	for (int i = 1; i < POP_NUM; i++)
	{
		double temp = ((double)rand()) / RAND_MAX;
		for (int j = 0; j < POP_NUM; j++)
		{
			temp -= p[j];
			if (temp <= 0)
			{
				for (int k = 0; k < N; k++)
				{
					selectPop[i][k] = population[j][k];
				}
				break;
			}
		}
	}
}

/***********************************************************************
 * Function   :cross1()
 * Description:第一种交叉算子
 *				(1)随机选择一个父代的基因,在两个父代中找到该基因的位置,作为子代第一个基因;
 *				(2)判断下一个基因是否在子代中,分为 4 种情况;
 *				(3)重复(2)步骤,直到形成个体
 *				(4)将子代逆转作为另一个子代
 * Input      :void
 * Output     :void
 * Return     :void
 ***********************************************************************/
void cross1()
{
	//pos1、pos2代表被选取的基因
	int pos1;
	int pos2;
	for (int k = 0; k < POP_NUM - 1; k += 2)
	{
		if (((double)rand()) / RAND_MAX < PC1)		//交叉概率
		{
			pos1 = 0;
			pos2 = 0;
			bool flag[N] = { 0 };
			//随机选取开始城市
			population[k][0] = rand() % N;
			flag[population[k][0]] = 1;
			for (int i = 0; i < N - 1; i++)
			{
				//找到相同的双亲节点
				for (int j = 0; j < N; j++)
				{
					if (population[k][i] == selectPop[k][j])
					{
						pos1 = j;
						break;
					}
				}
				for (int j = 0; j < N; j++)
				{
					if (population[k][i] == selectPop[k + 1][j])
					{
						pos2 = j;
						break;
					}
				}
				//printf("%d %d, %d, %d\n", pos1, pos2, population[k][i], i);
				bool add = false;
				while (!add)
				{
					if (!flag[selectPop[k + 1][(pos2 + 1) % N]] && !flag[selectPop[k][(pos1 + 1) % N]] && distance[population[k][i]][selectPop[k][(pos1 + 1) % N]] <= distance[population[k][i]][selectPop[k + 1][(pos2 + 1) % N]] || !flag[selectPop[k][(pos1 + 1) % N]] && flag[selectPop[k + 1][(pos2 + 1) % N]])
					{
						population[k][i + 1] = selectPop[k][(pos1 + 1) % N];
						flag[population[k][i + 1]] = 1;
						add = true;
					}
					else if (!flag[selectPop[k + 1][(pos2 + 1) % N]] && !flag[selectPop[k][(pos1 + 1) % N]] && distance[population[k][i]][selectPop[k][(pos1 + 1) % N]] > distance[population[k][i]][selectPop[k + 1][(pos2 + 1) % N]] || flag[selectPop[k][(pos1 + 1) % N]] && !flag[selectPop[k + 1][(pos2 + 1) % N]])
					{
						population[k][i + 1] = selectPop[k + 1][(pos2 + 1) % N];
						flag[population[k][i + 1]] = 1;
						add = true;
					}
					else if (!add && flag[selectPop[k][(pos1 + 1) % N]] && flag[selectPop[k + 1][(pos2 + 1) % N]])
					{
						pos1 = (pos1 + 1) % N;
						pos2 = (pos2 + 1) % N;
					}
				}
			}
			for (int i = 0; i < N; i++)
			{
				population[k + 1][i] = population[k][N - 1 - i];
			}
		}
	}
}

/***********************************************************************
 * Function   :cross2()
 * Description:第二种交叉算子,单点交叉方式扩展种群的覆盖范围
 * Input      :void
 * Output     :void
 * Return     :void
 ***********************************************************************/
void cross2()
{
	//交叉点位置
	int ranPos1;
	int ranPos2;
	int temp;
	for (int k = 0; k < POP_NUM - 1; k += 2)
	{
		ranPos1 = rand() % N;
		ranPos2 = rand() % N;
		if (((double)rand()) / RAND_MAX < PC2)		//交叉概率
		{
			if (ranPos1 > ranPos2)
			{
				temp = ranPos1;
				ranPos1 = ranPos2;
				ranPos2 = temp;
			}
			for (int i = ranPos1; i <= ranPos2; i++)
			{
				temp = population[k][i];
				population[k][i] = population[k + 1][i];
				population[k + 1][i] = temp;
			}
			int count1 = 0;
			int count2 = 0;
			int flag1[N];
			int flag2[N];
			for (int i = 0; i <= ranPos1 - 1; i++)
			{
				for (int j = ranPos1; j <= ranPos2; j++)
				{
					if (population[k][i] == population[k][j])
					{
						flag1[count1] = i;
						count1++;
					}
					if (population[k + 1][i] == population[k + 1][j])
					{
						flag2[count2] = i;
						count2++;
					}
				}
			}
			for (int i = ranPos2 + 1; i < N; i++)
			{
				for (int j = ranPos1; j <= ranPos2; j++)
				{
					if (population[k][i] == population[k][j])
					{
						flag1[count1] = i;
						count1++;
					}
					if (population[k + 1][i] == population[k + 1][j])
					{
						flag2[count2] = i;
						count2++;
					}
				}
			}
			if (count1 == count2 && count1 > 0)
			{
				for (int i = 0; i < count1; i++)
				{
					temp = population[k][flag1[i]];
					population[k][flag1[i]] = population[k + 1][flag2[i]];
					population[k + 1][flag2[i]] = temp;
				}
			}
		}
	}
}

/***********************************************************************
 * Function   :mutate(int param)
 * Description:变异(2-opt交换)
 * Input      :交换次数 param
 * Output     :void
 * Return     :void
 ***********************************************************************/
void mutate(int param)
{
	for (int k = 0; k < POP_NUM; k++)
	{
		if (((double)rand()) / RAND_MAX < PM)	//变异概率
		{
			reverse(population[k], param);
		}
	}
}

int main()
{
	//初始化
	init();
	//当前时间作为随机数种子
	srand((unsigned)time(NULL));
	//贪心算法求解 TSP 
	TSPGreedyAlgorithm();

	initPop();
	for (int i = 0; i < GENERATE_COUNT; i++)
	{
		//计算当前种群平均距离	
		select();
		if (i >= GEN && abs(avgDistance[i] - avgDistance[i - GEN]) < 10 && popDistance[minIndex] == currentOptDistance)
		{
			cross2();
			mutate(N);
		}
		else
		{
			cross1();
			mutate(2);
		}
		//计算种群平均路径长度并更新最短路径
		avgDistance[i] = calDistance() / POP_NUM;
		//printf("%d", avgDistance[i]);
	}
	printf("\n最短路径长度为:%d\n", currentOptDistance);
	printf("TSP 路径为:");
	for (int i = 0; i < N; i++)
	{
		printf("%d -> ", currentOpt[i]);
	}
	printf("%d", currentOpt[0]);
	return 0;
}

运行结果如下:
结果
从结果可以看出,相较于贪心算法和基本遗传算法,该算法采用两种交叉算子和改进的变异方法,TSP 解路径已经接近最优解。


  1. The multiple traveling salesman problem: an overview of formulations and solution procedures. ↩︎

  2. 遗传算法,百度百科. ↩︎

  3. 于莹莹,陈燕,李桃迎.改进的遗传算法求解旅行商问题[J].控制与决策,2014,29(08):1483-1488. ↩︎

  4. 蒋荣. 遗传算法在TSP问题上的应用[D].合肥工业大学,2009. ↩︎

  • 5
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值