C语言遗传算法解决TSP问题

 参考资料:

遗传算法-交叉算子讲解_遗传算法交叉操作_张叔zhangshu的博客-CSDN博客
https://www.cnblogs.com/lyrichu/p/6152928.html

1.问题背景:什么是TSP问题

      旅行商问题( TravelingSalesmanProblem TSP )是一个经典的组合优化问题。经典的TSP可以描述为:一个商品推销员要去若干个城市推销商品,该推销员从一个城市出发,需要经过所有城市后,回到出发地。应如何选择行进路线,以使总的行程最短。

 2.算法的基本原理:

       遗传算法是一种基于自然选择和群体遗传机理的搜索算法,它模拟了自然选择和自然遗传过程中的繁殖、杂交和突变现象。

        在利用遗传算法求解问题时,问题的每一个可能解都被编码成一个“染色体”,即个体,若干个个体构成了群体(所有可能解)。在遗传算法开始时,总是随机的产生一些个体(即初始解),根据预定的目标函数对每一个个体进行评估给出一个适应度值。基于此适应度值,选择一些个体用来产生下一代,选择操作体现了“适者生存"的原理,“好”的个体被用来产生下一代,“坏”的个体则被淘汰,然后选择出来的个体经过交叉和变异算子进行再组合生成新的一代,这一代的个体由于继承了上一代的一些优良性状,因而在性能上要优于上一代,这样逐步朝着最优解的方向进化。 

        因此,遗传算法可以看成是一个由可行解组成的群体初步进化的过程。

3.功能模块划分:

 ①编码:种群初始化:

生成一个数组chrom[sizepop][lenchrom]:

Sizepop表示一个路径,也就是一种解法。Lenchrom表示城市个数。

对于个sizepop个体的种群,随机给出sizepop个问题的解(相当于是染色体)作为初始种群。这里具体采用的方法是:1,2..., lenchrom作为第一个个体,然后2,3.. lenchrom分别与1交换位置得到lenchrom -1个解,从2开始,3,4,..., lenchrom分别与2交换位置得到lenchrom -2个解,依次类推。(如果经过上面的循环还是无法产生足够的初始个体,则随机再补充一部分具体方式就是选择两个基因位置,然后交换

最终我们通过交换列得到sizepop个不同的行即达到目的。

②确定适应度函数:

由题意可得,距离越短的走法适应度越高。因此我们可以用距离的倒数反应其适应度。所以首先需要定义函数double distance()作为辅助函数。distence[i]表示第i种走法下的总路程。

fit[i]=1distence[𝑖]1/(distence[i])

然后算出总适应度:

fit_sum=fit[0]+fit[1]+…+fit[sizepop]

最后算出每个走法适应度占总适应度的大小为轮盘赌算法做铺垫:

fit_pro[i]= 𝑓𝑖𝑡[𝑖]𝑓𝑖𝑡_𝑠𝑢𝑚(fit[i])/(fit_sum)

③选择:

选择:个体被选中的概率与适应度成正比,适应度越高,个体被选中的概率越大。最简单直接的选择算子就是轮盘赌算法。

轮盘赌算法:该算法本质就是转盘,看转盘停下时停在转盘的哪个区域。而转盘停在哪个区域与转盘的区域面积正相关。

 通过此步选出适应度高的走法。并用他们作为父代进行下一步的交叉操作。

④交叉(最重要的一步:是产生新个体的主要来源,直接关系到算法的全局寻优能力)

这里的交叉我们选用PMX交叉算子:部分匹配交叉保证了每个染色体中的基因仅出现一次,通过该交叉策略在一个染色体中不会出现重复的基因,所以PMX经常用于旅行商(TSP)或其他排序问题编码。

第一步,随机选择一对染色体(父代)中几个基因的起止位置(两染色体被选位置相同) 

 第二步:交换这两组基因的位置。

第三步:做冲突检测 

 

⑤变异:

遗传算法引入变异的目的有两个:一是使遗传算法具有局部的随机搜索能力。二是使遗传算法可维持群体多样性。

显然我们的目的是第一个,由于通过选择和交叉两步,我们已经得到了适应度较高的解,如果这里变异的概率太高的话就会把我们已得到的优质解破坏。所以这里我们选择一个较小的变异概率来增强算法的局部随机搜索能力。

其中pos1pos2为随机位置,由一个01的随机数乘lenchrom得到。注意其中的强制转换关系。 

⑥进化逆转:

为改善遗传算法的局部搜索能力,在选择、交叉、变异之后引进连续多次的进化逆转操作。这里的 “进化” 是指逆转算子的单方向性,即只有经逆转后,适应度值有提高的才接受下来,否则逆转无效 

 ⑦迭代--解码:

求出第一代sizepop个走法中路程最短的一种。并定义为best_result[lenchrom]

然后求出经过了(选择交叉变异进化逆转)之后的种群的最短路程,并与之前的比较。如果比之前的小则更新best_result[lenchrom]如果比之前的大则说明不是最优解,不操作。

将迭代了一次的种群再一次经历(选择交叉变异进化逆转)后再求最短路程,并与之前的比较。。。以此类推进行maxgen次。

最后经历过多次更新的best_result[lenchrom]即为最优解。

4.代码源文件 :

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#define lenchrom 20   //染色体长度(城市数目)
#define sizepop 200  //种群数目
#define pcross 0.6   //交叉概率
#define maxgen 200  //最大进化代数
#define pmutation 0.1  //变异概率 
int chrom[sizepop + 5][lenchrom + 5];

double city_pos[lenchrom][2] = { {88.0, 16.0}, {42.0, 76.0}, {5.0, 76.0}, {69.0, 13.0}, {73.0, 56.0}, {100.0, 100.0}, {22.0, 92.0}, {48.0, 74.0}, {73.0, 46.0}, {39.0, 1.0}, {51.0, 75.0}, {92.0, 2.0}, {101.0, 44.0 }, {55.0, 26.0}, {71.0, 27.0}, { 42.0, 81.0}, {51.0, 91.0}, {89.0, 54.0}, { 33.0, 18.0}, {40.0, 78.0} }; // 定义1维数组存放20个城市的X、Y坐标
int best_result[lenchrom];  //最优解
double min_distance;  // 最短路径长度




// 函数声明
void init(void); // 种群初始化函数
double distance(double *, double *); // 计算两个城市之间的距离
double *mini(double *); // 计算距离数组的最小值
double path_len(int *); // 计算某一个方案的路径长度,适应度函数为路线长度的倒数
void Choice(int[sizepop][lenchrom]); // 选择操作
void Cross(int[sizepop][lenchrom]); //交叉操作
void Mutation(int[sizepop][lenchrom]); // 变异操作
void Reverse(int[sizepop][lenchrom]); // 逆转操作







double *mini(double *arr)
{
	static double best_index[2];
	double min_dis = *arr;
	double min_index = 0;
	for (int i = 1; i < sizepop; i++) 
	{
		double dis = *(arr + i);
		if (dis < min_dis) 
		{
			min_dis = dis;
			min_index = i;
		}
	}
	best_index[0] = min_index;
	best_index[1] = min_dis;
	return best_index;
}



//种群初始化
void init(void)
{
	int num = 0;
	for (int i = 0; i < sizepop; i++)
		for (int j = 0; j < lenchrom; j++)
			chrom[i][j] = j + 1;
	num++;
	while (num < sizepop) 
	{
		for (int i = 0; i < lenchrom - 1; i++) 
		{
			for (int j = i + 1; j < lenchrom; j++) 
			{
				int temp = chrom[num][j];
				chrom[num][j] = chrom[num][i];
				chrom[num][i] = temp;// 交换第num个个体的第i个元素和第j个元素,进而产生不同路径顺序
				num++;
				if (num >= sizepop)
					break;
			}
			if (num >= sizepop)
				break;
		}// 如果经过上面的循环还是无法产生足够的初始个体,则随机再补充一部分
		// 具体算式就是选择两个基因位置,然后交换
		while (num < sizepop) {
			double r1 = ((double)rand() / RAND_MAX + 1.0);//0~1之间的随机小数
			double r2 = ((double)rand() / RAND_MAX + 1.0);
			int p1 = (int)(lenchrom * r1);//位置1
			int p2 = (int)(lenchrom * r2);//位置2
			int temp = chrom[num][p1];
			chrom[num][p1] = chrom[num][p2];
			chrom[num][p2] = chrom[num][p1];//交换基因位置
			num++;
		}
	}
}




//距离函数
double distance(double *city1, double *city2)
{ //计算两个城市之间距离
	double x1, x2, y1, y2, dis;
	x1 = *city1;
	x2 = *city2;
	y1 = *(city1 + 1);
	y2 = *(city2 + 1);
	dis = sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
	return dis;
}
// 计算路径长度
double path_len(int *arr)
{
	double path = 0;// 初始化路径长度
	int index = *arr;// 定位到第1个数字(城市序号)
	for (int i = 0; i < lenchrom - 1; i++)
	{
		int index1 = *(arr + i);
		int index2 = *(arr + i + 1);
		double dis = distance(city_pos[index1 - 1], city_pos[index2 - 1]);
		path += dis;
	}
	int last_index = *(arr + lenchrom - 1);// 最后1个城市序号
	int first_index = *arr; // 第1个城市序号
	double last_dis = distance(city_pos[last_index - 1], city_pos[first_index - 1]);
	path += last_dis;
	return path;// 返回总的路径长度
}



// 选择操作
void choice(int chrom[sizepop + 5][lenchrom + 5]) 
{
	double pick;//随机数选择概率
	int choice_arr[sizepop + 5][lenchrom + 5];//中间变量,存储选择到个体
	double fit_pro[sizepop];//每个个体适应度占总适应度和的概率
	double sum = 0;//该种群所有个体的适应度之和
	double fit[sizepop + 5];// 适应度函数数组(距离的倒数)
	for (int j = 0; j < sizepop; j++) 
	{
		double path = path_len(chrom[j]);//第j个个体的路径长度
		double fitness = 1 / path;//适应度
		fit[j] = fitness;
		sum += fit[j];//总适应度
	}
	for (int j = 0; j < sizepop; j++)
		fit_pro[j] = fit[j] / sum;// 适应度的概率数组
	// 开始轮盘赌
	for (int i = 0; i < sizepop; i++) 
	{
		pick = ((double)rand()) / RAND_MAX;// 0到1之间的随机数

		while (pick < 0.0001)
			pick = ((double)rand()) / RAND_MAX;

		for (int j = 0; j < sizepop; j++)
		{
			pick -= fit_pro[j];
			if (pick <= 0) 
			{
				for (int k = 0; k < lenchrom; k++)
					choice_arr[i][k] = chrom[j][k];// 选中一个个体
				break;
			}
		}
	}
	//轮盘赌结束后,把数组重新转移到chrom中
	for (int i = 0; i < sizepop; i++)
		for (int j = 0; j < lenchrom; j++)
			chrom[i][j] = choice_arr[i][j];
}




//交叉操作
void cross(int chrom[sizepop + 5][lenchrom + 5])
{
	double pick;
	double pick1;
	int choice1, choice2;//选择的个体的序号
	int pos1;
	int move = 0;// 当前移动的位置
	while (move < sizepop - 1) 
	{
		pick = ((double)rand()) / RAND_MAX;// 用于决定是否进行交叉操作

		if (pick > pcross) 
		{
			move += 2;
			continue;// 本次不进行交叉
		}
		// 采用部分映射杂交
		choice1 = move;// 用于选取杂交的两个父代
		choice2 = move + 1;// 注意避免下标越界

		int cross_num = lenchrom / 2;

		while (cross_num--) 
		{
			pick1 = ((double)rand() / RAND_MAX);
			pos1 = (int)(pick1 * lenchrom);//

			int temp = chrom[choice1][pos1];
			chrom[choice1][pos1] = chrom[choice2][pos1];//单点交换,执行多次
			chrom[choice2][pos1] = temp;

			for (int i = 0; i < lenchrom; i++) 
			{
				//解决冲突,因为一个染色体中,一个城市的序号只能出现一次
				if (i == pos1)
					continue;
				if (chrom[choice1][i] == chrom[choice1][pos1])
					chrom[choice1][i] = chrom[choice2][pos1];
				if (chrom[choice2][i] == chrom[choice2][pos1])
					chrom[choice2][i] = chrom[choice1][pos1];
			}
		}
		move += 2;
	}
}




// 变异操作
// 变异策略采取随机选取两个点,将其对换位置
void mutation(int chrom[sizepop + 5][lenchrom + 5]) 
{
	double pick, pick1, pick2;
	int pos1, pos2;

	for (int i = 0; i < sizepop; i++) 
	{
		pick = ((double)rand()) / RAND_MAX;//0~1之间随机数

		if (pick > pmutation)
			continue;
		pick1 = ((double)rand() / ( RAND_MAX + 1.0)); //0~1之间随机数(不会小于1)
		pick2 = ((double)rand() / (RAND_MAX + 1.0));
		pos1 = (int)(lenchrom * pick1);//变异位置的选取
		pos2 = (int)(lenchrom * pick2);

		while (pos1 > lenchrom - 1)
		{
			pick1 = ((double)rand() / (RAND_MAX + 1.0));
			pos1 = (int)(pick1 * lenchrom);
		}
		while (pos2 > lenchrom - 1) 
		{
			pick2 = ((double)rand() / RAND_MAX + 1.0);
			pos2 = (int)(pick2 * lenchrom);
		}
		int temp = chrom[i][pos1];
		chrom[i][pos1] = chrom[i][pos2];
		chrom[i][pos2] = temp;
	}
}




//进行逆转操作
void reverse(int chrom[sizepop + 5][lenchrom + 5])
{
	double pick1, pick2;
	double dis, reverse_dis;//逆转前的距离,逆转后的距离。如果逆转后的距离变大了,显然不保留此次逆转结果
	int n;
	int flag, pos1, pos2;
	int reverse_arr[lenchrom];//暂时储存逆转后的城市序列

	for (int i = 0; i < sizepop; i++) 
	{
		//对所有个体都进行一遍逆转操作
		flag = 0;// 用于控制本次逆转是否有效
		int re_num = 0;
		int temp;
		while (flag == 0) 
		{
			pick1 = ((double)rand() / (RAND_MAX + 1.0));
			pick2 = ((double)rand() / (RAND_MAX + 1.0));
			pos1 = (int)(pick1 * lenchrom);// 选取进行逆转操作的位置
			pos2 = (int)(pick2 * lenchrom);

			while (pos1 > lenchrom - 1) 
			{
				pick1 = ((double)rand() / (RAND_MAX + 1.0));
				pos1 = (int)(pick1 * lenchrom);
			}
			while (pos2 > lenchrom - 1) 
			{
				pick2 = ((double)rand() / (RAND_MAX + 1.0));
				pos2 = (int)(pick2 * lenchrom);
			}

			if (pos1 > pos2) 
			{
				temp = pos1;
				pos1 = pos2;
				pos2 = temp;// 交换使得pos1 <= pos2
			}


			if (pos1 < pos2) {
				for (int j = 0; j < lenchrom; j++)
					reverse_arr[j] = chrom[i][j];// 复制数组

				n = 0;// 逆转进行的元素数目

				for (int j = pos1; j <= pos2; j++) 
				{
					reverse_arr[j] = chrom[i][pos2 - n]; // 逆转数组
					n++;
				}

				reverse_dis = path_len(reverse_arr);// 逆转之后的距离
				dis = path_len(chrom[i]); // 原始距离
				if (reverse_dis < dis) 
				{
					for (int j = 0; j < lenchrom; j++)
						chrom[i][j] = reverse_arr[j];// 更新个体
					flag = 1;
				}
			}
			re_num++;
			if (re_num == 10)
				break;
		}
	}
}




//主函数
int main()
{
	time_t start, finish;
	start = clock();// 开始计时
	srand((unsigned)time(NULL));// 初始化随机数种子
	init();// 初始化种群

	int best_fit_index = 0;//最短路径出现代数
	double distance_arr[sizepop];
	double dis;
	for (int i = 0; i < sizepop; i++) 
	{
		dis = path_len(chrom[i]);
		distance_arr[i] = dis;
	}
	double *best_index = mini(distance_arr);//计算最短路径及序号
	min_distance = *(best_index + 1);// 最短路径
	int index = (int)(*best_index);// 最短路径序号
	for (int j = 0; j < lenchrom; j++)
		best_result[j] = chrom[index][j]; // 最短路径序列

	//进化
	double *new_arr;
	double new_min_dis;
	int new_index;
	for (int i = 0; i < maxgen; i++) 
	{
		choice(chrom);//选择
		cross(chrom);//交叉
		mutation(chrom);//变异
		reverse(chrom);//逆转操作
		for (int j = 0; j < sizepop; j++)
			distance_arr[j] = path_len(chrom[j]);// 距离数组
		new_arr = mini(distance_arr);
		new_min_dis = *(new_arr + 1);//新的最短路径
		if (new_min_dis < min_distance) 
		{
			min_distance = new_min_dis;// 更新最短路径
			new_index = (int)(*new_arr);
			for (int j = 0; j < lenchrom; j++)
				best_result[j] = chrom[new_index][j];// 更新最短路径序列
			best_fit_index = i + 1;// 最短路径代数
		}
	}
	finish = clock();// 计算结束
	double duration = ((double)(finish - start)) / CLOCKS_PER_SEC;// 计算耗时
	printf("规模为: % d的TSP问题,种群数为: % d, 进化代数为: % d\n", lenchrom, sizepop, maxgen);
	printf("得到最短路径为:%d-->%d-->%d-->%d-->%d-->%d-->%d-->%d-->%d-->%d-->%d-->%d-->%d-->%d-->%d-->%d-->%d-->%d-->%d-->%d\n",
	       best_result[0], best_result[1], best_result[2],
	       best_result[3], best_result[4], best_result[5], best_result[6], best_result[7], best_result[8], best_result[9],
	       best_result[10], best_result[11],
	       best_result[12], best_result[13], best_result[14], best_result[15], best_result[16], best_result[17], best_result[18],
	       best_result[19]);
	printf("最短路径 :% lf, 最短路径在第 :% d代\n", min_distance, best_fit_index);
	printf("程序耗时:%lf秒", duration);
	return 0;
}

 

 

 

 

  • 3
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值