笔者是大二的一只小菜狗,初学遗传算法,收敛速度结果等方面还需要改进和完善,以下是主要思路,还请大佬们指教
0、旅行商问题背景
此文是面向的任意出发点,走封闭路线的最优寻找(其实如果封闭了,一旦收敛,就是轨迹确定,其实就无所谓从哪里出发,走得什么方向了)
并把从出发点到终点再到出发点的每两个城市间的距离之和来作为个体的适应度
1、bug总结
基本上都是一些低端错误,看着图一乐就好
(1)最大错误:以i为循环变量进行,每一generation的迭代,在循环体里面的小循环中不小心也用i做了循环变量,导致总循环次数出错(迭代80代能跑出9000个输出真有我的)
(2)宏定义yyds:在量都只有一组,比如这里是只有10个城市,的情况下,定义宏常量,并且在代码中使用宏常量,可以起到“变量”的作用,就是每次改一下宏定义就好了(比如可以改轮的generation次数从70->3000等)
(3)当函数定义得多了,最好在每个函数前加注释提醒一下哪些是被放在其他函数中调用的,哪些是一开始就需要在主函数里面手动调用的(比如这里的初始化种群,自己忘了,结果报错),以及函数如果有返回值,记得return啊喂(init_sig)那里最后忘记返回指针了
(4)有必要时可以设置一些临时变量,观察什么地方跑了多久,配合<time.h>库,用clock()函数
类似于(具体的使用还可以在ga函数里面看到)
clock_t st, ed; double time; st = clock(); fun(); ed = clock(); time = (double)(ed-st)/CLOCKS_PER_SEC
2、主要思路
(1)基本定义
#define city_num 10 #define generation 3000 #define popular 288 #define group 72 typedef struct _sig { int gene[city_num]; int fitness; }sig, * psig; psig best; //定义一个种群 psig popu[popular]; int city[city_num][2];//存位置 int length[city_num][city_num];//存不同城市之间的距离 void get_city(void) { int i,j; for (i = 0; i < city_num; i++) scanf("%d %d", &city[i][0], &city[i][1]); for (i = 0; i < city_num; i++) { for (j = i+1; j < city_num; j++) { length[i][i] = 0; length[i][j] = (int)pow(city[i][0] - city[j][0], 2) + (int)pow(city[i][1] - city[j][1], 2); length[j][i] = length[i][j]; } } } //更新个体适应度 void update_fitness(psig p) { int i; p->fitness = 0; for (i = 0; i < city_num - 1; i++) p->fitness += length[p->gene[i]][p->gene[i + 1]]; p->fitness += length[p->gene[i]][p->gene[0]]; }
后面还需要利用随机数初始化一个种群,里面每个个体开始的基因序列都是随机的
如何实现呢?就先给个体的基因序列赋初值0,1,2...9,然后再从下标0开始,到9结束,每次都把下标对应的元素进行一个随机交换,与谁交换是rand()的结果决定的
//初始化个体 psig init_sig(void) { int i, tmp, num; psig new_sig = (psig)malloc(sizeof(sig)); for (i = 0; i < city_num; i++) new_sig->gene[i] = i; for (i = 0; i < city_num; i++) { tmp = new_sig->gene[i]; num = rand() % city_num;//去寻找一个随机交换的下标 new_sig->gene[i] = new_sig->gene[num]; new_sig->gene[num] = tmp; } //更新个体适应度 new_sig->fitness = 0; for (i = 0; i < city_num-1; i++) new_sig->fitness += length[new_sig->gene[i]][new_sig->gene[i + 1]]; new_sig->fitness += length[new_sig->gene[i]][new_sig->gene[0]]; return new_sig; } //初始化种群 psig init_popu(void) { int i; for (i = 0; i < popular; i++) popu[i] = init_sig(); best = NULL; }
(2)交叉算子(选择后剩余的个体进行繁殖)
主要思路
思路参考:哔站用py实现遗传算法的dl的思路:【算法】遗传算法解决旅行商(TSP)问题_哔哩哔哩_bilibili
文字解释
就是先随机一个st,并随机一个ed,从st的下标开始,到,ed之前的部分,是要交换的,用图中办法直接避免交换后有的城市重复或者缺失的问题,如何找到某个城市,它对应的下标,由一开始定义的pos1,pos2两个数组来存位置,下标是城市序号,值是原来gene的下标,每次换了后记得更新pos1,pos2
代码实现
//父母交叉 void cross(psig f,psig m, psig *c1, psig *c2) { int pos1[city_num], pos2[city_num],v1,v2,pos1_p,pos2_p,tmp; //从st开始,ed之前间为端点 int i,st = rand()%(city_num-1),ed = rand()%(city_num-1-st)+st+1; for (i = 0; i < city_num; i++) { pos1[f->gene[i]] = i; (*c1)->gene[i] = f->gene[i]; (*c2)->gene[i] = m->gene[i]; pos2[m->gene[i]] = i; } for (i = st; i < ed; i++) { v1 = (*c1)->gene[i];//先得到要交换的两个值 v2 = (*c2)->gene[i]; pos1_p = pos1[v2];//寻找该值在对方的序列中的位置,也成为一会自己要交换的位置 pos2_p = pos2[v1]; tmp = (*c1)->gene[pos1_p]; (*c1)->gene[pos1_p] = (*c1)->gene[i]; (*c1)->gene[i] = tmp; pos1[tmp] = i; pos1[(*c1)->gene[i]] = pos1_p; tmp = (*c2)->gene[pos2_p]; (*c2)->gene[pos2_p] = (*c2)->gene[i]; (*c2)->gene[i] = tmp; pos2[tmp] = i; pos2[(*c2)->gene[i]] = pos2_p; } update_fitness((*c1)); update_fitness((*c2)); }
(3)变异算子(对繁殖后的子代进行变异)
思路
就是单纯的随机选择两个位置,交换(更高级的交换方法,还请见其他博客)
代码
//对孩子变异 void vary(psig p) { int i = rand() % city_num; int j = rand() % city_num; int tmp; if (i == j && j < city_num - 1) j = i + 1; else if (i == j && j > 0) j = i - 1; tmp = p->gene[i]; p->gene[i] = p->gene[j]; p->gene[j] = tmp; update_fitness(p); }
(4)选择算子
思路
仍然是抄的哔站那个视频的思路,用的锦标赛选择,把个体分组(这里是popular种群规模/group组数来得到每组的个体数),在每组内找最优个体,并且同时看是否需要更新全局best
代码
//种群选择 void judge(void)//每次留18个,需要繁殖3代 { //换一种选择方法,每次留18个 int i,j; psig max; for (i = 0; i < group; i++) { max = popu[i * popular/group]; for (j = 1; j < popular / group; j++) { if (max->fitness > popu[i * popular / group + j]->fitness) max = popu[i * popular / group + j]; } popu[i] = max; if (best == NULL || best->fitness > max->fitness) best = max; } }
(5)总的遗传过程
//遗传算法 void ga(void) { init_popu(); int i, tmp, n,k; for (i = 0; i < generation; i++) { /*printf("%d\n", i); st = clock();*/ judge();//留下group个 /*ed = clock(); printf("选择耗时 %lf / 1000s\n", 1e3 * (double)(ed - st) / CLOCKS_PER_SEC);*/ tmp = 3;//3代 int j = 0; //这里交叉选择把适应性排前面的直接两两配对,变异后n个孩子,需要繁殖3代 /*st = clock();*/ while(tmp) { j = 0; while (j < group) { cross(popu[j + (3 - tmp) * group], popu[j + 1 + (3 - tmp) * group], &popu[j + ((3 - tmp) + 1) * group], &popu[j + (1 + (3 - tmp)) * group + 1]); j+=2; } /*ed = clock();*/ tmp--; } /*printf("遗传耗时 %lf / 1000s\n", 1e3* (double)(ed- st) / CLOCKS_PER_SEC);*/ n = rand() % 10; for (k = popular-1; k > popular-n; k--) vary(popu[k]); } judge(); }
最后是一个简单的主函数
int main()
{
srand(time(0));
get_city();
init_popu();
ga();
printf("最优方案为:\n");
int i;
for (i = 0; i < city_num; i++)
printf("%d ", best->gene[i]);
return 0;
}
3、运行结果
测试用的数据(10个城市的坐标)
1 1
9 9
8 0
3 1
7 8
8 1
1 9
1 5
8 5
8 6
可见后面的路径是收敛的,只是出发点和方向不同而已
--------------------------更新线---------------------------------增加了非封闭固定起点的解决方案----------------
0、背景
从封闭的改为不封闭的,并且默认输入的第一个坐标对应的城市为起点,且不回到终点
1、思路
主要过程基本上同前,只是在gene的长度上做了修改,减一,这样可以不在gene中出现0这个城市的点而避免判定问题,其他地方略作相应修改
以下为总代码
#include <stdio.h> #include <stdlib.h> #include <math.h> #include <time.h> #define city_num 10 #define generation 3000 #define popular 288 #define group 72 //调试用 double duration; clock_t st, ed; //定义一个个体,先整个最优个体 typedef struct _sig { int gene[city_num-1]; int fitness; }sig, * psig; psig best; //定义一个种群 psig popu[popular]; int city[city_num][2];//存位置 int length[city_num][city_num];//存不同城市之间的距离 void get_city(void) { int i,j; for (i = 0; i < city_num; i++) scanf("%d %d", &city[i][0], &city[i][1]); for (i = 0; i < city_num; i++) { for (j = i+1; j < city_num; j++) { length[i][i] = 0; length[i][j] = (int)pow(city[i][0] - city[j][0], 2) + (int)pow(city[i][1] - city[j][1], 2); length[j][i] = length[i][j]; } } } //初始化个体(新生代) psig init_sig(void) { int i, tmp, num; psig new_sig = (psig)malloc(sizeof(sig)); for (i = 0; i < city_num-1; i++) new_sig->gene[i] = i+1; for (i = 0; i < city_num-1; i++) { tmp = new_sig->gene[i]; num = rand() % (city_num-1); new_sig->gene[i] = new_sig->gene[num]; new_sig->gene[num] = tmp; } new_sig->fitness = 0; for (i = 0; i < city_num-2; i++) new_sig->fitness += length[new_sig->gene[i]][new_sig->gene[i + 1]]; new_sig->fitness += length[0][new_sig->gene[0]]; return new_sig; } //初始化种群 psig init_popu(void) { int i; for (i = 0; i < popular; i++) popu[i] = init_sig(); best = NULL; } //更新个体适应度 void update_fitness(psig p) { int i; p->fitness = 0; for (i = 0; i < city_num - 2; i++) p->fitness += length[p->gene[i]][p->gene[i + 1]]; p->fitness += length[0][p->gene[0]]; } //种群选择 void judge(void)//每次留18个,需要繁殖3代 { //换一种选择方法,可能这样太慢了,每次留18个 int i,j; psig max; for (i = 0; i < group; i++) { max = popu[i * popular/group]; for (j = 1; j < popular / group; j++) { if (max->fitness > popu[i * popular / group + j]->fitness) max = popu[i * popular / group + j]; } popu[i] = max; if (best == NULL || best->fitness > max->fitness) best = max; } } //父母交叉 void cross(psig f,psig m, psig *c1, psig *c2) { //试着重写的,这次是利用两个记录位置的数组来简化的 int pos1[city_num], pos2[city_num],v1,v2,pos1_p,pos2_p,tmp; //从st开始,ed之前间为端点 int i,st = rand()%(city_num-1-1),ed = rand()%(city_num-1-1-st)+st+1; for (i = 0; i < city_num-1; i++) { pos1[f->gene[i]] = i; (*c1)->gene[i] = f->gene[i]; (*c2)->gene[i] = m->gene[i]; pos2[m->gene[i]] = i; } for (i = st; i < ed; i++) { v1 = (*c1)->gene[i]; v2 = (*c2)->gene[i]; pos1_p = pos1[v2]; pos2_p = pos2[v1]; tmp = (*c1)->gene[pos1_p]; (*c1)->gene[pos1_p] = (*c1)->gene[i]; (*c1)->gene[i] = tmp; pos1[tmp] = i; pos1[(*c1)->gene[i]] = pos1_p; tmp = (*c2)->gene[pos2_p]; (*c2)->gene[pos2_p] = (*c2)->gene[i]; (*c2)->gene[i] = tmp; pos2[tmp] = i; pos2[(*c2)->gene[i]] = pos2_p; } update_fitness((*c1)); update_fitness((*c2)); } //对孩子变异 void vary(psig p) { int i = rand() % (city_num-1); int j = rand() % (city_num-1); int tmp; if (i == j && j < city_num-1-1) j = i + 1; else if (i == j && j > 0) j = i - 1; tmp = p->gene[i]; p->gene[i] = p->gene[j]; p->gene[j] = tmp; update_fitness(p); } //遗传算法 void ga(void) { init_popu(); int i, tmp, n,k; for (i = 0; i < generation; i++) { /*printf("%d\n", i); st = clock();*/ judge();//留下group个 /*ed = clock(); printf("选择耗时 %lf / 1000s\n", 1e3 * (double)(ed - st) / CLOCKS_PER_SEC);*/ tmp = 3;//3代 int j = 0; //这里交叉选择把适应性排前面的直接两两配对,变异后n个孩子,需要繁殖3代 /*st = clock();*/ while(tmp) { j = 0; while (j < group) { cross(popu[j + (3 - tmp) * group], popu[j + 1 + (3 - tmp) * group], &popu[j + ((3 - tmp) + 1) * group], &popu[j + (1 + (3 - tmp)) * group + 1]); j+=2; } /*ed = clock();*/ tmp--; } /*printf("遗传耗时 %lf / 1000s\n", 1e3* (double)(ed- st) / CLOCKS_PER_SEC);*/ n = rand() % 10; for (k = popular-1; k > popular-n; k--) vary(popu[k]); } judge(); } int main() { srand(time(0)); get_city(); init_popu(); ga(); printf("最优方案为:\n0 "); int i; for (i = 0; i < city_num-1; i++) printf("%d ", best->gene[i]); return 0; }