1.遗传算法是什么?
遗传算法的概念是由Holland于1973年受生物进化论的启发而首次提出的,它是一种通过模拟生物界自然选择和遗传机制的随机搜索算法。该算法通过数学的方式,利用计算机仿真运算,将问题的求解过程转换成类似生物进化中的染色体基因的交叉、变异等过程。在求解较为复杂的组合优化问题时,相对一些常规的优化算法,通常能够较快地获得较好的优化结果。遗传算法已被人们广泛地应用于组合优化、机器学习、信号处理、自适应控制和人工生命等领域。
如果你能看懂并理解这段话,我觉得你已经不需要再学习本文章了~~
解释的再直白点,遗传算法是一种启发式搜索算法,不同于以前那些朴素的类似于dfs、bfs之类的算法,遗传算法的核心是在给定的解空间中搜索全局最优解,如果不能得知所有的解空间,那么显然不能应用遗传算法这一类的智能算法。
2.TSP问题是什么?
旅行商问题,即TSP问题(Traveling Salesman Problem)又译为旅行推销员问题、货郎担问题,是数学领域中著名问题之一。假设有一个旅行商人要拜访n个城市,他必须选择所要走的路径,路径的限制是每个城市只能拜访一次,而且最后要回到原来出发的城市。 路径的选择目标是要求得的路径路程为所有路径之中的最小值。非常难懂的数学式子就不再罗列了~~
由tsp问题的定义可以得知,如果要用暴力求解算法,那么tsp问题的时间复杂度就是O(n!),显然这是普通的计算机无法承受的,再进一步思考,可以采用一跳的贪心算法,给定一个起点,遍历其余所有未被访问过的城市,寻找出路径最短的那个城市,更新出发点城市。当然这样的求解方法也是劣于遗传算法的。
接下来,具体介绍遗传算法解决tsp问题
第一步:编码+种群初始化
编码的原理很简单,就是用一种方法(染色体)来表示所有的解空间。
针对于TSP问题,这是一个离散化的问题,一个既定的解就是一个所走过的城市的序列,而城市的编号为从1~city_num,所以显然可以用一个数组来维护城市序列(解空间),然后需要生成初始的种群(即一开始状态下的解空间),即从1到city_num的全排列问题。
具体生成方式采用不断交换的思路(简单易懂)
int chrom[sizepop+5][lenchrom+5]; // 种群
void init()
{/*种群初始化,生成sizepop个路径,一个路径就是一个城市的顺序,包含lenchrom个城市*/
int num = 0;
for(int i=0;i<sizepop;i++) //chrom的下标从0开始
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++)
{ //交换,进而产生不同的路径顺序
swap(chrom[num][j],chrom[num][i]);
num++;
if(num>=sizepop)
break;
}
if(num>=sizepop) break;
}
/*如果经过上述方式后,还是无法生成足够的染色体,则需要通过随机交换的方式进行补充*/
while(num<sizepop)
{
double r1 = ((double)rand()/(RAND_MAX+1.0)); //0~1之间的小数(不会等于1)
double r2 = ((double)rand()/(RAND_MAX+1.0));
int p1 = (int)(lenchrom*r1); //位置1,范围为 [0,lenchrom-1],因为下标从0开始,所以不会等于lenchrom
int p2 = (int)(lenchrom*r2); //位置2
swap(chrom[num][p1],chrom[num][p2]);
num++;
}
}
}
第二步:确定适应度函数
进化论中的适应度,是表示某一个体对环境的适应能力,也表示该个体繁殖后代的能力。遗传算法的适应度函数也叫评价函数,是用来判断群体中的个体的优劣程度的指标,它是根据所求问题的目标函数来进行评估的。遗传算法在搜索进化过程中一般不需要其他外部信息,仅用评估函数来评估个体或解的优劣,并作为以后遗传操作的依据。
简单来说就是,遗传算法选择后代的依据就是适应度函数,一个种群中谁的适应度高,我就选择谁留下来! 而针对TSP问题,一个个体的适应度显然就是它从第一个城市出发,游历一周后回到出发点城市所走过的距离的倒数(距离越小,适应度越大)
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开始的,所以下面定位到数组时要注意减1)
for(int i=0;i<lenchrom-1;i++)
{
int index1 = *(arr+i); //下标为i的城市
int index2 = *(arr+i+1);// city_pos储存某个城市的x坐标,y坐标
double dis = distance(city_pos[index1-1],city_pos[index2-1]);
path+=dis;
}
/*
注意:tsp问题要求最后要回到起始位置
*/ int last_index = *(arr+lenchrom-1);//最后一个城市的序号
int first_index = *arr;//第一个城市的序号
double last_dis = distance(city_pos[last_index-1],city_pos[first_index-1]);
path+=last_dis;
return path; //返回走完这条路径所需要的总长度
}
第三步:选择操作
个体被选中的概率与适应度成正比,适应度越高,个体被选中的概率越大。最简单直接的选择算子就是轮盘赌算法,求出每个个体的适应度概率,再求出每个个体的累积适应度概率,生成一个随机数,与累积适应度概率进行比较,从而确定选择哪个个体保留下来!不懂轮盘赌算法的可以去查阅资料,这里只通过代码来介绍。
void choice()
{ // 选择操作
double pick;//随机数选择概率
double 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];
}
第四步:交叉操作
交叉操作是遗传算法最重要的操作,是产生新个体的主要来源,直接关系到算法的全局寻优能力。
我采用的交叉算子是Partial-Mapped Crossover (PMX)部分交叉,但是注意:编写代码的时候要合理地变通,因为会存在多对多映射的关系,所以我采用单个交换,交换多次的思想,然后只需要解决一个冲突,从而避免了出现多对多映射的状况。
void cross()
{
double pick;
double pick1;
int choice1,choice2;//选择的个体的序号
int pos1;
int move = 0; //当前移动的位置
while(move<sizepop-1)
{
pick = ((double)rand())/RAND_MAX; //0-1之间的随机数
if(pick<pcross)
{
move+=2;
continue; //本次不进行交叉操作
}
//采用部分映射方法进行交叉
choice1 = move; //用于选取交叉的两个父代
choice2 = move+1; //注意避免下标越界
int cross_num = lenchrom/2;
while(cross_num--)
{
pick1 = ((double)rand()/(RAND_MAX+1.0)); //0~1之间的小数(不会等于1)
pos1 = (int)(pick1*lenchrom); //杂交点的第一个位置,[0,lenchrom-1]
swap(chrom[choice1][pos1],chrom[choice2][pos1]);//单点交换,执行多次
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;
}
}
第五步:变异操作
变异操作比较简单,对于某个给定的染色体(即城市序列),随机选择两个position,交换这两个位置上的城市编号即可,代码实现比较简单。
遗传算法引入变异的目的有两个:一是使遗传算法具有局部的随机搜索能力。当遗传算法通过交叉算子已接近最优解邻域时,利用变异算子的这种局部随机搜索能力可以加速向最优解收敛。显然,此种情况下的变异概率应取较小值,否则接近最优解的积木块会因变异而遭到破坏。二是使遗传算法可维持群体多样性,以防止出现未成熟收敛现象。此时收敛概率应取较大值。
void mutation()
{
double pick,pick1,pick2;
int pos1,pos2,temp;
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)); //0~1之间的小数(不会等于1)
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);
}
swap(chrom[i][pos1],chrom[i][pos2]);
}
}
第六步: 进化逆转操作
参考资料:https://www.cnblogs.com/lyrichu/p/6152928.html
具体思路为:对于给定的一个染色体(城市序列),随机生成一个区间(即两个随机数之间),然后逆转这个区间的城市序号,操作即完成!
void reverse()
{
double pick1,pick2;
double dis,reverse_dis;//逆转前的距离,逆转后的距离。如果逆转后的距离变大了,显然不保留此次逆转结果
int n;
int flag,pos1,pos2,temp;
int reverse_arr[lenchrom];//暂时储存逆转后的城市序列
for(int i=0;i<sizepop;i++)
{ //对所有的个体都要进行一遍逆转操作
flag = 0; //用于控制本次逆转是否有效(如果无效则不会进行逆转)
int re_num = 0;
while(flag == 0)
{
pick1 = ((double)rand())/(RAND_MAX+1.0);//[0,1) 之间的浮点数
pick2 = ((double)rand())/(RAND_MAX+1.0);
pos1 = (int)(pick1*lenchrom); // 选取进行逆转操作的位置
pos2 = (int)(pick2*lenchrom);//得到的结果为 [0,lenchrom-1]
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)
swap(pos1,pos2);// 交换使得pos1 <= pos2
if(pos1<pos2)
{//如果pos1==pos2,也就没有逆转的必要了
for(int j=0;j<lenchrom;j++)
reverse_arr[j] = chrom[i][j]; //先复制一遍chrom数组
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;// 防止因一直没有更好的逆转路径而陷入死循环
}
}
}
最后确定终止条件,可以设置终止代数maxgen=200,然后综合调用这些函数即可求得结果。虽然有时候遗传算法因为算子的原因无法做到逐代收敛,但是对于全局最优解的求解结果还是不错的!