遗传算法的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、路径图
遗传算法的C语言设计

2、收敛过程
遗传算法的C语言设计

总结

遗传算法是一种启发式算法,可以用于求解NP难等难以用数学方法求解的问题的近似最优解,本文利用遗传算法求解了经典的100个城市的TSP问题,在实验过程中,学到了很多,现总结如下:
1、 利用遗传算法求解需要先确定适合问题的编码方法,好的编码方式可以使问题求解事半功倍
2、 交叉算子和变异算子的选择同样非常重要,交叉算子和变异算子是产生优化染色体的关键。
3、 算法的设计还要考虑资源的消耗情况,用有限的资源解决复杂问题并减少计算时间应该是我们的追求。
4、 遗传算法的参数选择非常重要,可以通过对照试验确定参数范围,选出对算法求解更有利的参数。
5、 程序的编写离不开对算法的理解,只有深入理解了算法的求解过程,才能更好的编写求解程序。
通过本次实验,初步认识了遗传算法,希望在以后的学习中,可以将遗传算法与实际问题联系起来,学有所用,更加深刻的理解遗传算法。

相关文章: