遗传算法的C语言实现
- 遗传算法求解TSP问题
- 换位表达、启发式交叉、启发式变异、最优选择策略
前言
本文设计遗传算法对TSP问题进行求解。首先选取100个城市作为旅行过程中要经过的点,城市的坐标已知,求解一个通过每个城市一次且总距离最短的路径。本文采用换位表达对染色体编码,基因的值表示城市的值,基因的顺序表示城市访问的顺序;采用启发式交叉和启发式变异产生新的子代染色体;采用最优选择策略选择下一轮迭代的染色体。本文使用C语言对设计的遗传算法求解,最终求解出问题的近似最优解。
代码
/************************************************************************
* 遗传算法求解TSP问题
* 换位表达、启发式交叉、启发式变异、最优选择策略
* */
//#define GET_ALGORITHM_EXCUTE_TIME //是否获取算法执行时间,不需要获取算法执行时间则每次迭代会打印迭代结果
//头文件
#include <stdio.h>
#include <math.h>
#include <stdlib.h>
#include <time.h>
//宏定义
#define CITY_COUNTS 100 //基因个数
#define CHORO_COUNTS 50 //种群大小
#define ITERA_COUNTS 20000 //迭代次数
#define CROSS_RATE 1.0 //交叉概率
#define MUTA_RATE 0.05 //变异概率
#define CROSS_NUM (int)(CHORO_COUNTS*CROSS_RATE/2) //交叉种群大小
#define MUTA_NUM (int)(CHORO_COUNTS*MUTA_RATE) //变异种群大小
#define LAMDA (int)(CITY_COUNTS * 0.05) //启发式变异基因个数
#define BETA 100 //启发式变异随机生成BETA条染色体选其中最优解
/**
* @brief: 数组复制
* @param: src 原数组
* @param: dst 目标数组
* @param: n 数组元素个数
* @return: none
* @author:
* @date: 2018-12-10
* */
void ArrayCopy(int *src, int *dst, int n)
{
for(int i = 0; i < n; i++){
dst[i] = src[i];
}
}
/**
* @brief: 生成1到n随机排列的数组,用于生成初始染色体
* @param: arr 随机排列保存到该数组中
* @param: n 1到n的排列组合
* @return: none
* @author:
* @date: 2018-12-10
* */
void RandomArray1ToN(int *arr, int n)
{
//产生1-n的顺序排列的数组
for(int i = 0; i < n; i++)
{
arr[i] = i+1;
}
//随机打乱顺序
for(int i = 0; i < n/2; i++)
{
int p= 0, q = 0;
do{
p = rand()%n;
q = rand()%n;
}while(p == q);
int temp = arr[p];
arr[p] = arr[q];
arr[q] = temp;
}
}
/**
* @brief: 判断数字是否在一个数组之中
* @param: num 要判断的数字
* @param: arr 参与判断的数组
* @param: n 数组大小
* @return: 在数组中返回1,不在数组中返回0
* @author:
* @date: 2018-12-10
* */
int IsNumInArray(int num, const int *arr, int n)
{
for(int i = 0; i < n; i++)
{
if(num == arr[i])
{
return 1;
}
}
return 0;
}
/**
* @brief: 返回数字在数组中的索引
* @param: num 要检索的数字
* @param: arr 参与检索的数组
* @param: n 数组大小
* @return: 在数组中返回索引标号,不在数组中返回-1
* @author:
* @date: 2018-12-10
* */
int PosNumInArray(int num, const int *arr, int n)
{
for(int i = 0; i < n; i++)
{
if(num == arr[i])
{
return i;
}
}
return -1;
}
/**
* @brief: 计算按染色体表达的城市顺序的距离
* @param: c 染色体
* @param: dis 城市间距离矩阵
* @param: n 城市个数
* @return: 返回距离
* @author:
* @date: 2018-12-10
* */
float DistanceCal(const int *c, const float dis[][CITY_COUNTS], int n)
{
float d = 0;
for(int i = 0; i < n-1; i++)
{
d += dis[c[i]-1][c[i+1]-1];
}
d += dis[c[n-1]-1][c[0]-1];
return d;
}
/**
* @brief: 启发式交叉,随机选择一个城市作为出发点
* @param: p1 父代染色体1号
* @param: p2 父代染色体2号
* @param: c 要交叉产生的子代染色体
* @param: dis 城市间距离矩阵
* @param: n 城市个数
* @return: none
* @author:
* @date: 2018-12-10
* */
void Crossover(const int *p1, const int *p2, int *c, const float dis[][CITY_COUNTS], int n)
{
int a = rand()%n+1;//城市编号
c[0] = a;//随机选择城市开始巡回
//printf("%d ", c[0]);
for(int i = 1; i < n; i++)
{
int ap1 = PosNumInArray(a, p1, n);//当前城市在1号父代染色体中的索引
int ap2 = PosNumInArray(a, p2, n);//当前城市在2号父代染色体中的索引
//printf("%d %d ", ap1, ap2);
//父代中在当前城市之后的城市索引
if(++ap1 == n){
ap1 = 0;
}
if(++ap2 == n){
ap2 = 0;
}
int f1 = IsNumInArray(p1[ap1], c, i);//标识1号父代中下一个城市是否已在子代染色体中
int f2 = IsNumInArray(p2[ap2], c, i);//标识2号父代中下一个城市是否已在子代染色体中
if(f1 == 0 && f2 == 0){
a = dis[a-1][p1[ap1]-1] < dis[a-1][p2[ap2]-1] ? p1[ap1] : p2[ap2];//子代染色体中没有,选择距离最小的城市
}else if(f1 == 0 && f2 != 0){
a = p1[ap1];//1号父代中的城市不在子代染色体中
}else if(f1 != 0 && f2 == 0){
a = p2[ap2];//2号父代中的城市不在子代染色体中
}else{
//1号父代和2号父代中的城市均在染色体中,随机选择一个城市使巡回继续
int *temp = (int*)calloc(n, sizeof(int));//标识城市是否在子代染色体中
int count = 0;//不在子代染色体中的城市个数
for(int j = 0; j < n; j++){
if(IsNumInArray(j+1, c, i) == 1){
temp[j] = 0;
}else{
temp[j] = 1;
count++;
}
}
int *t = (int*)calloc(count, sizeof(int));//不在子代染色体中的城市编号
int k = 0;
for(int j = 0; j < n; j++){
if(temp[j] == 1){
t[k++] = j+1;
}
}
k = rand()%count;//随机选择一个城市使巡回继续
a = t[k];
free(temp);
free(t);
}
//选择的城市编号插入到子代染色体中
c[i] = a;
//printf("%d ", c[i]);
}
}
/**
* @brief: 启发式变异,随机生成一定数量的染色体,选择其中最优解
* @param: p 参与变异的父代染色体
* @param: c 要生成的子代染色体
* @param: dis 城市间距离矩阵
* @param: n 城市个数
* @param: lamda 变异的基因个数
* @param: beta 随机生成染色体的个数
* @return: none
* @author:
* @date: 2018-12-12
* */
void Mutation(const int *p, int *c, const float dis[][CITY_COUNTS], int n, int lamda, int beta)
{
int *pC = (int*)calloc(n, sizeof(int));//每次变异产生的染色体
//用父代染色体初始化子代和每次变异的染色体
for(int i = 0; i < n; i++)
{
c[i] = p[i];
pC[i] = p[i];
}
float dMin = DistanceCal(c, dis, n);//最小距离
//printf("%f ", dMin);
int *pLamda = (int*)calloc(lamda, sizeof(int));//参与变异的基因在父代中的位置索引
int *pCity = (int*)calloc(lamda, sizeof(int));//参与变异的基因城市编号
for(int i = 0; i < lamda; i++)
{
int t = rand()%n;//随机产生变异基因的索引
while (IsNumInArray(t, pLamda, i) == 1) {
t = rand()%n;//判断是否已在变异基因中,如果已在变异基因中则继续随机产生变异基因索引
}
pLamda[i] = t;//保存城市索引
pCity[i] = p[t];//保存城市编号
//printf("%d ", pLamda[i]);
}
//随机产生beta条染色体,选择其中适值最优的染色体
for(int i = 0; i < beta; i++)
{
int *pNum = (int*)calloc(lamda, sizeof(int));//选择出的基因顺序
RandomArray1ToN(pNum, lamda);//打乱选择出的基因的顺序
for(int j = 0; j < lamda; j++)
{
pC[pLamda[j]] = pCity[pNum[j]-1];//将打乱原来顺序的基因插入到变异的索引位置
}
float dDis = DistanceCal(pC, dis, n);//计算新产生染色体的适值
//如果适值小于最小适值,则将变异复制到子代染色体中
if(dDis < dMin)
{
//变异的染色体复制到子代染色体中
for(int j = 0; j < lamda; j++)
{
c[pLamda[j]] = pC[pLamda[j]];
}
//最小距离更新
dMin = dDis;
}
//printf("%f ", dMin);
free(pNum);
}
//释放动态分配的内存
free(pC);
free(pLamda);
free(pCity);
}
/**
* @brief: 主函数
* @param:
* @return:
* @author:
* @date: 2018-12-10
* */
int main(int argc, char *argv[])
{
//变量
float position[CITY_COUNTS][2] = {0}; //位置矩阵
float distance[CITY_COUNTS][CITY_COUNTS] = {0}; //距离矩阵
int choro[CHORO_COUNTS][CITY_COUNTS] = {0}; //染色体数组,初始种群
int choroNew[CHORO_COUNTS][CITY_COUNTS] = {0}; //染色体新种群
int cross[CROSS_NUM][CITY_COUNTS] = {0}; //交叉子代数组
int mutate[MUTA_NUM][CITY_COUNTS] = {0}; //变异子代数组
//printf("%d %d", sizeof(cross), sizeof(mutate));
//设置随机数种子
time_t t;
srand((unsigned int)time(&t));
#ifdef GET_ALGORITHM_EXCUTE_TIME
//算法计时
clock_t begin, end;
#endif
//从数据文件中读取位置信息
FILE *fp;
fp = fopen("data.txt", "r");
if(fp == NULL)
{
printf("读取文件错误:没有数据文件,检查数据文件名称是否正确。\n");
return 0;
}
printf("正在读取数据文件...\n");
for(int i = 0; i < CITY_COUNTS; i++)
{
fscanf(fp, "%f%f", &position[i][0], &position[i][1]);
//printf("%f\t%f\n",position[i][0], position[i][1]);
}
fclose(fp);
printf("数据文件读取成功。\n");
//计算两两城市之间的距离
printf("正在计算城市间距离...\n");
for(int i = 0; i < CITY_COUNTS-1; i++)
{
distance[i][i] = 0;
for(int j = i+1; j < CITY_COUNTS; j++)
{
distance[i][j] = sqrt(pow(position[i][0] - position[j][0], 2) +
pow(position[i][1] - position[j][1], 2));
distance[j][i] = distance[i][j];
//printf("%f", distance[j][i]);
}
}
printf("城市间距离计算完成。\n");
//生成染色体种群
printf("正在生成初始种群...\n");
for(int i = 0; i < CHORO_COUNTS; i++)
{
RandomArray1ToN(choro[i],CITY_COUNTS);
}
printf("初始种群生成成功。\n");
//开始迭代
printf("开始迭代...\n");
#ifdef GET_ALGORITHM_EXCUTE_TIME
begin = clock();
#endif
float disMin = 0;//最小距离
int cityOrder[CITY_COUNTS] = {0};//城市顺序
float disTrend[ITERA_COUNTS] = {0};//距离迭代变化趋势
for(int i = 0; i < ITERA_COUNTS; i++){
//交叉
int *pCross = (int*)calloc(CHORO_COUNTS, sizeof(int));//1-50随机排列,相邻编号的染色体做交叉
RandomArray1ToN(pCross, CHORO_COUNTS);
for(int j = 0; j < CROSS_NUM; j++){
Crossover(choro[pCross[j*2]-1], choro[pCross[j*2+1]-1], cross[j], distance, CITY_COUNTS);
}
free(pCross);
//变异
for(int j = 0; j < MUTA_NUM; j++){
t = rand()%CHORO_COUNTS;//随机选择要变异的染色体
Mutation(choro[t], mutate[j], distance, CITY_COUNTS, LAMDA, BETA);
}
//选择
float *pDis = (float*)calloc(CHORO_COUNTS+CROSS_NUM+MUTA_NUM, sizeof(float));//每条染色体的适值,即距离
int *pPos = (int*)calloc(CHORO_COUNTS+CROSS_NUM+MUTA_NUM, sizeof(int));//染色体编号,与适值一一对应
//计算初始种群染色体适值,编号0到CHORO_COUNTS-1
for(int j = 0; j < CHORO_COUNTS; j++){
pDis[j] = DistanceCal(choro[j], distance, CITY_COUNTS);
pPos[j] = j;
}
//计算交叉子代染色体适值,编号CHORO_COUNTS到CHORO_COUNTS+CROSS_NUM-1
for(int j = 0; j < CROSS_NUM; j++){
pDis[j+CHORO_COUNTS] = DistanceCal(cross[j], distance, CITY_COUNTS);
pPos[j+CHORO_COUNTS] = j+CHORO_COUNTS;
}
//计算变异子代染色体适值,编号CHORO_COUNTS+CROSS_NUM到CHORO_COUNTS+CROSS_NUM+MUTA_NUM-1
for(int j = 0; j < MUTA_NUM; j++){
pDis[j+CHORO_COUNTS+CROSS_NUM] = DistanceCal(mutate[j], distance, CITY_COUNTS);
pPos[j+CHORO_COUNTS+CROSS_NUM] = j+CHORO_COUNTS+CROSS_NUM;
}
//对所有计算适值的染色体的编号进行选择排序,得到适值由小到大排列的染色体编号
for(int j = 0; j < CHORO_COUNTS+CROSS_NUM+MUTA_NUM-1; j++){
int t = j;
for(int k = j+1; k < CHORO_COUNTS+CROSS_NUM+MUTA_NUM; k++){
if(pDis[j] > pDis[k]){
t = k;
}
}
float tempF = pDis[j];
pDis[j] = pDis[t];
pDis[t] = tempF;
int tempI = pPos[j];
pPos[j] = pPos[t];
pPos[t] = tempI;
}
//复制排名前CHORO_COUNTS的染色体
for(int j = 0; j < CHORO_COUNTS; j++){
if(pPos[j] < CHORO_COUNTS){
ArrayCopy(choro[pPos[j]], choroNew[j], CITY_COUNTS);//染色体编号在初始染色体中
}else if(pPos[j] < CHORO_COUNTS+CROSS_NUM){
ArrayCopy(cross[pPos[j]-CHORO_COUNTS], choroNew[j], CITY_COUNTS);//染色体编号在交叉子代中
}else{
ArrayCopy(mutate[pPos[j]-CHORO_COUNTS-CROSS_NUM], choroNew[j], CITY_COUNTS);//染色体编号在变异子代中
}
}
#ifndef GET_ALGORITHM_EXCUTE_TIME
//每迭代100次输出一次最小距离,即最优适值
if(i%100 == 0){
printf("第%d次迭代结果:%f\n", i, pDis[0]);
}
#endif
//保存每次迭代的最小距离,即最优适值
disTrend[i] = pDis[0];
//复制选择后的染色体,作为新一轮迭代的初始染色体
for(int j = 0; j < CHORO_COUNTS; j++){
ArrayCopy(choroNew[j], choro[j], CITY_COUNTS);
}
//迭代次数到达设定值,保存最小距离和城市顺序
if(i == ITERA_COUNTS - 1){
disMin = pDis[0];
ArrayCopy(choro[0], cityOrder, CITY_COUNTS);
}
//释放动态分配的内存空间
free(pDis);
free(pPos);
}
#ifdef GET_ALGORITHM_EXCUTE_TIME
end = clock();
printf("算法执行时间:%fs\n", (double)((end-begin)/CLOCKS_PER_SEC));
#endif
printf("迭代结束。\n");
//显示最终迭代结果
printf("遗传算法求解最短路径:%f\n", disMin);
printf("城市顺序:\n%d", cityOrder[0]);
for(int i = 1; i < CITY_COUNTS; i++)
{
printf("->%d", cityOrder[i]);
}
printf("\n");
//保存适值收敛过程,保存到distance.txt中
FILE *fpDis;
fpDis = fopen("distance.txt", "w");
if(fpDis == NULL)
{
printf("保存收敛过程失败。\n");
}
for(int i = 0; i < ITERA_COUNTS; i++){
fprintf(fpDis, "%f\n", disTrend[i]);
}
fclose(fpDis);
//保存最终迭代的城市顺序,保存到city.txt中
FILE *fpCity;
fpCity = fopen("city.txt", "w");
if(fpCity == NULL)
{
printf("保存城市顺序失败。\n");
}
for(int i = 0; i < CITY_COUNTS; i++){
fprintf(fpCity, "%d\n", cityOrder[i]);
}
fclose(fpCity);
printf("保存求解结果成功。\n");
return 0;
}
结果
1、路径图
2、收敛过程
总结
遗传算法是一种启发式算法,可以用于求解NP难等难以用数学方法求解的问题的近似最优解,本文利用遗传算法求解了经典的100个城市的TSP问题,在实验过程中,学到了很多,现总结如下:
1、 利用遗传算法求解需要先确定适合问题的编码方法,好的编码方式可以使问题求解事半功倍
2、 交叉算子和变异算子的选择同样非常重要,交叉算子和变异算子是产生优化染色体的关键。
3、 算法的设计还要考虑资源的消耗情况,用有限的资源解决复杂问题并减少计算时间应该是我们的追求。
4、 遗传算法的参数选择非常重要,可以通过对照试验确定参数范围,选出对算法求解更有利的参数。
5、 程序的编写离不开对算法的理解,只有深入理解了算法的求解过程,才能更好的编写求解程序。
通过本次实验,初步认识了遗传算法,希望在以后的学习中,可以将遗传算法与实际问题联系起来,学有所用,更加深刻的理解遗传算法。