如果你对遗传算法感兴趣或者正在做有关GA的研究,不妨关注博客右侧专栏 → 智能计算-深入遗传算法 ,一步一步深入算法,分享算法每一个流程模块(如选择策略,交叉机制等等)的众多参考观点。代码和Demo咱从来不缺。
遗传算法入门系列文章:
(上篇)遗传算法入门(上)代码中的进化学说与遗传学说
(中篇)遗传算法入门(中)实例,求解一元函数最值(MATLAB版)
写在之前
说明: 本文是一个遗传算法求解TSP问题的实例,用C++写明关键之处的算子,源码下载 请带好大挪移令和灵石进传送阵
TSP问题说明与明确目标函数
旅行商问题,即TSP问题(Traveling Salesman Problem)又译为旅行推销员问题、货郎担问题,是数学领域中著名问题之一。假设有一个旅行商人要拜访n个城市,他必须选择所要走的路径,路径的限制是每个城市只能拜访一次,而且最后要回到原来出发的城市。路径的选择目标是要求得的路径路程为所有路径之中的最小值。
正如上述百度百科中定义的一样,TSP问题属于组合优化的问题,它的求解目标就是求最短路径。本文的实际应用背景也正如定义中说的那样,一个商人拜访所有的城市,每个城市只能去一次,最终回到原点,问怎么走路径最短?
我们用数学建模抽象出实际应用中的模型,即目标函数:
不难理解, 是各个不重复城市的距离之和,目标是求 的最小值。为此,我们需要设计模型:
在date.txt中存放cityNum个城市数据,每个城市都有城市编号和横纵坐标 。具体排布如下图所示:
从date.txt中获取数据计算各个城市之间的距离放入到数组
myDistance[][]
中 C++实现代码:
void initDistance() {
int sum[cityNum * 3];
int x[cityNum];
int y[cityNum];
int i;
ifstream infile("E:\\data_C.txt",ios::in); //打开文件
if (!infile) {
cerr << "open error!" << endl;
exit(1);
}
for (i = 0; i <(3*cityNum);i++) { //读取所有数据
infile >> sum[i];
}
for (i = 0; i < cityNum; i++) { //读取数据将横纵坐标分别给x[]和y[]
x[i] = sum[2*i+i+1];
y[i] = sum[2*i+i+2];
}
infile.close(); //关闭文件
//计算各个城市之间的距离矩阵
for (int i = 0; i < cityNum - 1; i++) {
myDistance[i][i]=0 ;//对角线全为0
for (int j = i + 1; j < cityNum;j++) {
double d=
sqrt(((x[i] - x[j]) * (x[i] - x[j]) + (y[i] - y[j]) * (y[i] - y[j])) / 10.0);
int dd =(int) round(d); //四舍五入
//cout << dd << endl;
myDistance[i][j] = dd;
myDistance[j][i] = dd;
}
myDistance[cityNum - 1][cityNum - 1] = 0;
}
编码与初始种群
从目标函数上,我们的解变量应该是所有城市的编号。每个个体城市有cityNum(本文定义48个)个,如果采用精度为20的二进制编码,则基因个数变为 ,将会是一个很庞大数字,增加算法的开销,所以本文采用实数编码最为合适,即每个个体是所有城市的序号组成的,基因就是cityNum个城市编号,染色体长度也就是cityNum。需要注意的是,个体的基因不能有重复。
说到这儿,我们也定义种群规模 , 那么初始种群实际上就是一个 的二维矩阵,矩阵中的元素是定义在 中的城市编号,并且规定每一行(一个个体)中的所有列(个体中的基因)都不能有重复元素。
附上C++代码: 生成初始种群
//------------------------------初始化种群(城市编号(0~cityNum-1))<城市编号为实数编码>
void initGroup() {
int i, j,k;
for (i = 0; i < scale;i++) {
oldGroup[i][0] = (int)(rand()% cityNum) ;//随机产生40以内的整数,赋值给第一列
for (j = 1; j < cityNum;) {
oldGroup[i][j] = (int)(rand()%cityNum);//随机产生40以内的整数,赋值给每一行的所有列
//保证每一行中没有重复的数值,即保证每一个染色体都是不同的基因
for (k = 0; k < j; k++) {
if (oldGroup[i][j] == oldGroup[i][k]) { //每一行中,在添加列值时如果数值与前列数值有重复,则重新生成随机数
break;
}
}//end_for3
if (k==j) {
j++;
}
}//end_for2
}//end_for1
}
适应度函数的设计
本文是求解路径之和的最小值,值越小,使用度越大,所以,适应度函数要取目标函数的倒数。这在代码中会融入到其他算子中,也比较简单,易于理解。这里就不在给出具体代码。大家只是注意一下即可。
选择:随机遍历抽样
本文采取的选择策略是精英策略(上代最好的直接复制到下代中)和随机遍历抽样策略。而不在采用中篇中讲述的轮盘赌选择,所谓的随机遍历抽样也非常好理解:
在中篇我们讲述轮盘赌选择时举了四个个体例子,用求解的累积概率画出了一个轮盘,而此时我们用累积概率画一条横线:
然后进行抽取,在 中随机产生一个数做为抽取的第一个位置,假设为图上我红色箭头处 。从第一个位置开始,每隔 就抽取一个,规则是:箭头落入0.0~0.1区域就选中个体A,落入0.1~0.5区域就选中个体B,落入0.5~0.8区域就选中个体C,落入0.8~1.0区域就选中个体D 。 一共抽取了 次,正好抽出 个个体,组成新种群。 从中可以看出个体适应度高的,选择概率就高,累积概率画成的区域范围就广,箭头落入其中的次数就多,选择该个体的次数就多。
附上C++代码实现:
void susSelect() {
int k,i,g;
int temp[scale];
int newGroup[scale][cityNum];
int maxIndividual; //最有个体
for (g = 0; g < scale;g++) { //一共进行抽样scale次
//打乱种群顺序,模拟随机抽样。对temp进行赋值,1~scale,不能重复
for (k = 0; k < scale;) {
temp[k] = (int)(rand() % scale);
for (i = 0; i < k; i++) {
if (temp[k] == temp[i]) {
break;
}
}//endl_for3
if (k == i) {
k++;
}
}//end_for2
//将样本中适应度最大的那个个体编号给maxIndividual
maxIndividual = temp[0];
for (k = 1; k < sampleSize; k++) {
if (fitness[temp[k]] < fitness[maxIndividual]) {
maxIndividual = temp[k];
}
}//end_for2
//将选择的个体给新种群
for (i = 0; i < cityNum; i++) {
newGroup[g][i] = Group[maxIndividual][i];
}
}//end_for1
}
交叉与变异的注意事项
在考虑交叉与变异时,我们一定要先分析一个问题,假如两个要交叉的个体,其中有个体存在某编号城市基因的前提下又通过交叉得到了相同的基因,这会导致一个个体中出现相同的城市编号,变异也会导致这种情况的发生,这不符合“一个城市只拜访一次”的原则 。所以此时我们使用顺序交叉(OXCrossover)和对换变异 。
对换变异非常简单,就是将个体中某两位基因互换 ,这能解决相同的问题。而顺序交叉的原理很简单,但不易叙述,你最好去百度搜一下关于顺序交叉(OX)的原理解析图,我这儿附上C++的OX实现代码,你也可以从研读代码中明确OX的原理:
void OXCross(int k1, int k2) {
int i = 0, j = 0, k = 0;
bool mark = true;
int g_temp1[cityNum]; //交叉的中间转换数组
int g_temp2[cityNum];
int rand1 = (int)(rand() % cityNum);//产生交叉部分的两个点
int rand2 = (int)(rand() % cityNum);
while (rand1 == rand2) { //保证两点不想等
rand2 = (int)(rand() % cityNum);
}
if (rand1>rand2) { //保证rand1<rand2
int temp = rand1;
rand1 = rand2;
rand2 = temp;
}
/*
cout << "被选中交叉的个体是:" << k1 << endl;
cout << "交叉中的rand1和rand2" << " " << rand1 << " " << rand2 << endl;
*/
//(1)、将个体1的中间部分插入到数组2的第一部分,将个体2的中间部分插入到数组1的第一部分
int flag = rand2 - rand1 + 1;
for (i = 0, j = rand1; i < flag; i++, j++) {
g_temp1[i] = Group[k2][j];
g_temp2[i] = Group[k1][j];
}//endl_for1
//(2)、将个体1从头开始按照顺序插入到数组1的生育部分,如果与数组1第一部分有重复,则个体1中的下一个插入
for (k = 0, j = flag; j < cityNum;) {
for (i = 0; i < flag; i++) { //判断是否有重复
if (Group[k1][k] == g_temp1[i]) {
mark = false;
break;
}
else {
mark = true;
}
}//end_for2
if (mark) {
g_temp1[j] = Group[k1][k]; //如果没有相等的就赋值
k = k + 1;
j = j + 1;
}
else
{
k = k + 1; //如果有相等的则拿个体1的下一个与之对比,j还是保持在原来位置
}
}//end_for1
//(3)、将个体2从头开始按照顺序插入到数组2的生育部分,如果与数组2第一部分有重复,则个体2中的下一个插入
for (k = 0, j = flag; j < cityNum;) {
for (i = 0; i < flag; i++) { //判断是否有重复
if (Group[k2][k] == g_temp2[i]) {
mark = false;
break;
}
else {
mark = true;
}
}//end_for2
if (mark) {
g_temp2[j] = Group[k2][k]; //如果没有相等的就赋值
k = k + 1;
j = j + 1;
}
else
{
k = k + 1; //如果有相等的则拿个体2的下一个与之对比,j还是保持在原来位置
}
}//end_for1
//(4)、将数组1和数组2分别复制到个体1和个体2中
for (k = 0; k < cityNum; k++) {
Group[k1][k] = g_temp1[k];
Group[k2][k] = g_temp2[k];
}
}
注意:C++中的随机数生成函数对代码的影响
代码中需要多次生成随机数,C++语言为我们提供了许多随机数生成函数。 不过需要非常注意: 因为代码执行速度过快,一些随机数生成函数生成的随机数是有时间间隔的,会对算法造成严重影响。说明白点就是: 比如随机数生成函数(后面简称函数)生成一个随机数0.7,该随机数参与了下面的代码执行,当下面的代码在非常短的时间内执行完毕后,再要求函数生成一个新的随机数的时候,你会发现该函数生成的还是0.7。这并不是巧合,而是函数生成一个数后,在极短时间内会丧失随机性,虽然这个时间很短,但是代码执行时间比这个时间还要短,所以造成非随机现象 。
源码中的算法用的是srand(time(NULL));
这个函数,该函数在代码放置的位置是正确的,所以不会有上述问题,只是提醒大家再调试或者用C++编写算法时要注意该问题,否则会严重影响算法性能,得出非常不好的结果。