超详细遗传算法(图文并茂)
一.前言
- 遗传算法(GA)主要用来解决TSP问题和复杂多项式求解问题。先从最简单的经典的遗传算法开始,再逐渐延伸(自适应交叉,自适应变异,退火式变异)。
- 本人现为大三在校生,能力有限,文章中肯定存在很多不足之处。可以留言讨论。或者联系作者邮箱:zi.bo@qq.com。
- 实验环境:windows 10 vs2017
- 实验题目取自 https://www.cnblogs.com/lyrichu/p/6152928.html
代码全部编译通过,并且得出理想结果。
二.什么是遗传算法?
- 遗传算法(Genetic Algorithm)是模拟达尔文生物进化论的自然选择和遗传学机理的生物进化过程的计算模型,是一种通过模拟自然进化过程搜索最优解的方法。
- 我自己理解的意思为:先随机构造一个种群,然后根据适应度选择优秀的个体。让这些优秀个体繁衍出后代,为了不使种群陷入恶性循环还需要对部分个体变异。(为了加快进化速度还可以对染色体进行逆转等等)
三.遗传算法的一般步骤
假设有一名商人想去 济南市、德州市、聊城市、菏泽市、泰安市、莱芜市、济宁市、枣庄市、临沂市、日照市、青岛市、威海市、烟台市、潍坊市,商人目前正在济南市。
要求:
- 所走路程最近;
- 每个城市只能访问一次;
- 从某城市出发,最后回到该城市
根据要求,求解一条路径。
- 编码
在本问题来看,我们需要求解出的是一条路径。换个角度说:对这些城市进行排序,看看哪种顺序最符合我们的三个要求。而一个城市就对应大自然中的一个 基因 。而本题目中的14个城市一次排序就构成了一条路。这一条路对应自然中的一条染色体 。根据染色体计算出的路径总长度即是 适应度。N条染色体可以构成一个种群(种群的大小是人为规定的)。 - 选择
选择操作就是根据染色体的适应度选择符合要求的染色体。在本问题中我们选择染色体的概率和适应度成正比,路径越短,染色体的适应度越大,被选到的概率越高。所以我们采用轮盘赌算法来选择优秀个体。
3.交叉
交叉是产生新个体的主要操作。这里我们采用PMX交叉算法。让被选择的两个优秀基因根据交叉算法产生的新的个体。 - 变异
程序再具体执行时,有概率会产生一个值(染色体),这个值并不是我们想要的,但又因为规则的不完善或者其他情况,后代总是围绕着这个值变化。这个现象我们成为“早熟”。为了避免这个现象我们随机产生一个值(染色体),用来打破当前这个局面。
四.具体做法
需要的变量
- 定义一个全局变量
const int evolution_Pop
用来存储进化次数 - 定义一个全局变量
const int lenght
用来保存城市的个数 - 定义一个全局变量
const int pop_Size
用来保存种群大小(规模) - 定义一个二维数组
int city_Pos[lenght][2]
用来保存城市的坐标(坐标稍后在程序中给出) - 定义一个二维数组
int population[pop_Size][lenght]
用来保存解集 - 定义一个一维数组
pop_Fitness[pop_Size]
用来保存染色体的适应度
以上变量都是 全局变量
程序开始
- 程序开始先初始化种群:
void init_Pop()
。在这里我把种群规模设定为300。即初始化300条染色体。我们先把种群数组population[pop_Size][lenght]
都赋值{0,1,2,3,4。。。}0即代表济南,1代表德州,2代表聊城,以此类推。为了加快收敛速度,基因的排序越混乱越好。所以接下来我们把每条染色体打乱。在这里因为种群数和城市数量较少。所以采用了这样的思想: - 先让0排在最前边
- 随后0与1交换,0与2交换,0与3交换。。一直到0与最后一个元素交换完。
- 当0全部交换完毕后,让1与右边的数值依次交换。
- 1也交换完毕后向右,依次用2交换。这样全部交换完成后一共有(N-1)!条染色体(排列)。如果染色体数目不够,可以将popluation数组倒序排列{13,12,11,10。。}然后再按照以上步骤排列。
代码实现
void init() //初始化种群
{
int pop_num = 1; //染色体数量
for (int i = 0; i < pop_Size; i++)
{
for (int j = 0; j < lenght; j++)
{
population[i][j] = j; //初始化种群
}
}
start:for (int i = 0; i < lenght - 1; i++) //构造种群
{
for (int j = 1; (i + j) < lenght; j++) //将第一个数值 与第1,2,3,4。。。。。互换位置构造成新的解集
{
int temp = population[pop_num][i];
population[pop_num][i] = population[pop_num][i + j];
population[pop_num][i + j] = temp;
if (pop_num >= pop_Size)
{
goto end;
}
pop_num++;
}
}
for (int i = pop_num; i < pop_Size; i++) //逆转值再次构造
{
for (int j = 0; j < lenght; j++)
{
population[i][j] = lenght - j - 1;
}
}
goto start;
end:pop_num++;
}
再提供一种办法,用于超大规模的种群构造
/*void permutation(int levels, int lenght, int a[])
{
//递归到底层
int pop_num = 1;//染色体数量
if (levels == lenght - 1)
{
for (int j = 0; j < lenght; j++)
population[pop_num][j] = a[j];
pop_num++;
}
else
{
for (int i = levels; i < lenght; i++)
{
int temp = a[levels];
a[levels] = a[i];
a[i] = temp;
//交换后递归下一层
permutation(levels + 1, lenght, a);
//保证每一层递归后保持上一层的顺序
temp = a[levels];
a[levels] = a[i];
a[i] = temp;
}
}
}*/
- 种群构造完成后,计算每条染色体的适应度存储到
pop_Fitness
数组中。这里分成了两个函数:void path_Pop(int *arr)
和double *distance(double x,double y)
。其中path_Pop函数接收一条染色体。然后将染色体第1 和2 ,2和3, 3和4个元素,一直到12和13两两一组传递给distance函数,distance接收到两个值后,找到city_Pos对应的坐标,计算出两个城市间的距离返回给path_Pop函数,path_Pop函数将距离加在一起就是整条路径的距离(也是染色体的适应度)。
代码实现:
double distance(double* x, double* y) //两个城市之间的距离
{
double x1 = *x;
double y1 = *(x + 1);
double x2 = *y;
double y2 = *(y + 1);
double distances = sqrt((x1 - x2)*(x1 - x2) + (y1 - y2)*(y1 - y2)); //勾股定理求两个点之间的距离
return distances;
}
double path_Pop(int *arr) //染色体总距离
{
double path_Pop_Dis = 0.0; //存储距离
double path_Sum_Dis = 0.0;
int first_Pos = *arr; //存储第一个点得位置
for (int i = 0; i < lenght-1; i++)
{
int x1 = *(arr+i);
int x2 = *(arr + i+1);
path_Pop_Dis = distance(city_Pos[x1], city_Pos[x2]);
path_Sum_Dis += path_Pop_Dis;
}
int last_Pos = *(arr + 13);
path_Sum_Dis += distance(city_Pos[first_Pos], city_Pos[last_Pos]); //总距离包括最后一个点回到第一个点的距离
return path_Sum_Dis;
}
- 种群初始化完成后就是evolution_Pop次选择 ,交叉 , 遗传 。
- 选择 操作
- 选择操作是根据染色体适应度大小实现。本质上来说就是一个轮盘赌算法。
- 首先求出种群适应度的值(也就是全部染色体适应度的和)。
- 在用每条染色体的适应度除以适应度的总和,这样可以将染色体适应度映射为一条区间[0,1]的线段。
- 给出一个[0,1)的随机值pick。从
i = 0
开始,执行到i = pop_Size
每次执行pick-=(pop_Fitness[i]/sum_Fitness)
如果pick<=0则选中这个区间所在位置对应的染色体。 - 执行pop_Size次。
- 选择操作代码
void select()
{
double pop_Fintess_Avg = 0; //适应度平均值
double pop_Fitness_Sum = 0.0; //适应度和
double pop_Fitness_Rec[pop_Size]; //适应度倒数
double pop_Fitness_Rec_Sum = 0.0; //适应度平均值的和
for (int i = 0; i < pop_Size; i++)
{
pop_Fitness_Rec_Sum += 1 / pop_Fitness[i]; //倒数和
}
int postion[pop_Size][lenght];
for (int i = 0; i < pop_Size; i++) //轮盘赌法
{
double pick = ((double)rand()) / RAND_MAX;
while(pick>=1)
pick = ((double)rand()) / RAND_MAX;
double prob = pick;
for (int j = 0; j < pop_Size; j++)
{
pick = pick - ((1 / pop_Fitness