嗯哼,时隔半年,再次有时间整理关于组合优化问题——旅行商问题(Traveling Salesman Problem, TSP),这次采用的是经典遗传算法(Genetic Algorithm, GA)进行求解,利用C++语言进行编程实现。关于TSP问题以及GA的简单介绍,可参见我的另一篇文章:https://www.cnblogs.com/Alex-Xu-OR/p/9458239.html

  各种启发式算法的整体框架大致都由以下几个操作组成:(1)初始解的产生;(2)解的评价(评价函数);(3)扰动算子;此外,还可以加上程序原始数据的导入等操作。这些操作是多数启发式算法所通用的算子,基为此,此次在采用C++进行实现的时候,采用一个通用的 HeuristicOperator.h 头文件以及对应的 HeuristicOperator.cpp 类文件对这些操作进行集中放置,造好轮子,方便以后取用。

表1 HeuristicOperator中函数功能清单

编号

功能

记号

函数1

获取客户点坐标

getCoord()

函数2

获取距离矩阵

getDM()

函数3

获取初始解

getInitS()

函数4

解的评价

Eval()

函数5

搜索范围内最优评价值及其相对应的位置

bestS()

函数6

产生Sharking操作位置

RandPosition()

函数7

交换算子(swap

Swap()

函数8

翻转算子(flip

Flip()

函数9

插入算子(insert

Insert()

注:函数5可以直接用 STL vector的操作函数max_elementmin_element实现类似的功能,写的时候一时没有想起来,就自己造了个轮子。

  之前在采用Matlab以及Java实现GA to solve TSP 时,考虑到程序的运行效率,通常会选用 array 来放置各种数据,众所周知,array的特点就是固定分配的连续物理地址进行数据的存储,然而对于不定长度的数据进行存储时,一般的方式是采用“多次少量”,即预先分配一定内存空间,等不够用了再分配一定的内存空间(多说一句,Matlab 中还可以采用以下两种方式:用 array=[]; 以及用cell实现存储不定长度数组,但效率不高)。而在C++ STL 中有一种神奇的数据类型vector容器,它既有着 array 的连续内存分配方式,又能不用指定数据存储长度,对于一组不同规模的数据集进行测试时,再也不用担心使用array时提醒必须预分配确定的存储空间了~~

  以下为HeuristicOperator.h头文件:

 1 #pragma once
 2 #include<iostream>
 3 #include<vector>
 4 #include <algorithm>        // std::shuffle
 5 #include <random>            // std::default_random_engine
 6 #include<chrono>
 7 using namespace std;
 8 
 9 class HeuristicOperator {
10 public:
11     vector<vector<double>> getCoord(void);        //函数1:获取坐标函数
12     vector<vector<double>> getDM(vector<vector<double>> Coord);        //函数2:获取距离矩阵函数
13     vector<int> getInitS(int n);        //函数3:获取初始解函数
14     double Eval(vector<int> S, vector<vector<double>> DM, int n);    //函数4:评价函数
15 
16     vector<double> bestS(vector<double> Eval, int Length);    //函数5:搜索范围内最优评价值及其相应的位置函数
17 
18     vector<int> RandPosition(int n);        //函数6:产生Sharking操作位置函数
19     vector<int> Swap(vector<int> S, vector<int> RP);    //函数7:交换算子
20     vector<int> Flip(vector<int> S, vector<int> RP);    //函数8:翻转算子
21     vector<int> Insert(vector<int> S, vector<int> RP);    //函数9:插入算子
22 };

  对应的HeuristicOperator.cpp类文件比较容易实现,在此不再赘述。本文所用算例为31城市的TSP问题,与Java版遗传算法求解TSP求解算例一致,具体数据如下:

 1 1304    2312
 2 3639    1315
 3 4177    2244
 4 3712    1399
 5 3488    1535
 6 3326    1556
 7 3238    1229
 8 4196    1004
 9 4312    790
10 4386    570
11 3007    1970
12 2562    1756
13 2788    1491
14 2381    1676
15 1332    695
16 3715    1678
17 3918    2179
18 4061    2370
19 3780    2212
20 3676    2578
21 4029    2838
22 4263    2931
23 3429    1908
24 3507    2367
25 3394    2643
26 3439    3201
27 2935    3240
28 3140    3550
29 2545    2357
30 2778    2826
31 2370    2975

  以下为遗传算法的主函数:

  1 /*
  2     文件名:CppGATSP
  3     作者:Alex Xu
  4     地址:Dalian Maritime University
  5     描述:利用遗传算法求解TSP问题
  6     创建时间:2018年12月10日11点27分
  7 */
  8 
  9 #include<iostream>
 10 #include<vector>
 11 #include<numeric>        //accumulate
 12 #include<chrono>        //time
 13 #include "HeuristicOperator.h"
 14 using namespace std;
 15 using namespace chrono;
 16 
 17 //设置算法参数
 18 # define    POP_SIZE    2
 19 # define    MAX_GEN        4000
 20 
 21 int main() {
 22     //计时开始
 23     auto start = system_clock::now();
 24 
 25     //生成距离矩阵
 26     HeuristicOperator ga_dm;
 27     vector<vector<double>> GA_DM;
 28     GA_DM = ga_dm.getDM(ga_dm.getCoord());
 29 
 30     int n = int(GA_DM[0].size());    //城市规模
 31 
 32     //初始化算法
 33     vector<vector<int>> initPop(POP_SIZE, vector<int>(n));        //初始种群
 34     vector<vector<int>> Pop(POP_SIZE, vector<int>(n));        //当前种群
 35     vector<vector<int>> newPop(POP_SIZE, vector<int>(n));        //新种群
 36     vector<double> popFit(POP_SIZE);        //记录种群适应度值
 37     vector<int> bestIndival(n);    //最优个体
 38     vector<double> gs(MAX_GEN + 1);    //记录全局最优解
 39     gs[0] = 1e9;
 40     unsigned int seed = (unsigned)std::chrono::system_clock::now().time_since_epoch().count();
 41 
 42     //生成初始种群
 43     HeuristicOperator s0;
 44     for (int i = 0; i < POP_SIZE; i++) {
 45         initPop[i] = s0.getInitS(n);
 46     }
 47     Pop = initPop;
 48 
 49     //开始进化
 50     for (int gen = 1; gen <= MAX_GEN; gen++) {
 51 
 52         HeuristicOperator eval;            //计算种群的适应度值(这里直接用路径长度表示)
 53         for (int i = 0; i < POP_SIZE; i++) {
 54             popFit[i] = eval.Eval(Pop[i], GA_DM, n);
 55         }
 56 
 57         HeuristicOperator bestEI;        //找出种群中个体的最优适应度值并记录相应的个体编号
 58         vector<double> bestEvalIndex(2);
 59         bestEvalIndex = bestEI.bestS(popFit, POP_SIZE);
 60         double bestEval = bestEvalIndex[0];        //最优适应度值
 61         int bestIndex = int(bestEvalIndex[1]);    //最优适应度值对应的个体编号
 62 
 63         //最优解的更新
 64         if (bestEval < gs[gen - 1]) {        //比上一代优秀则更新
 65             gs[gen] = bestEval;
 66             bestIndival = Pop[bestIndex];
 67         }
 68         else {                                //不比上一代优秀则不更新
 69             gs[gen] = gs[gen - 1];
 70         }
 71         if (gen % 100 == 0) {
 72             cout << "" << gen << "次迭代时全局最优评价值为" << gs[gen] << endl;
 73         }
 74 
 75         //扰动操作(产生新种群)
 76         for (int p = 0; p < POP_SIZE; p++) {
 77             HeuristicOperator shk;
 78             vector<int> randPosition = shk.RandPosition(n);
 79             vector<int> tmpS(n);
 80             double randShk = rand() / double(RAND_MAX);
 81             if (randShk < 0.33) {
 82                 tmpS = shk.Swap(Pop[p], randPosition);        //交换操作
 83             }
 84             else if (randShk >= 0.67) {
 85                 tmpS = shk.Flip(Pop[p], randPosition);        //翻转操作
 86             }
 87             else {
 88                 tmpS = shk.Insert(Pop[p], randPosition);    //插入操作
 89             }
 90 
 91             HeuristicOperator evl;
 92             if (evl.Eval(tmpS, GA_DM, n) > evl.Eval(Pop[p], GA_DM, n)) {
 93                 newPop[p] = Pop[p];
 94             }
 95             else {
 96                 newPop[p] = tmpS;
 97             }
 98         }
 99         Pop = newPop;
100 
101         //选择操作(轮盘赌)
102         vector<double> Cusum(POP_SIZE + 1, 0);        //适用于轮盘赌的累加器Cusum(补充了cus[0]=0;
103         for (int i = 0; i < POP_SIZE; i++) {
104             Cusum[i + 1] = Cusum[i] + popFit[i];
105         }
106 
107         double Sum = accumulate(popFit.begin(), popFit.end(), 0.0);        //计算各个体被选择的概率(归一化)
108         vector<double> cusFit(POP_SIZE + 1);        //放置种群中个个体被选择的概率值
109         for (int i = 0; i < POP_SIZE + 1; i++) {
110             cusFit[i] = Cusum[i] / Sum;
111         }
112 
113         for (int p = 0; p < POP_SIZE; p++) {        //轮盘赌产生新种群
114             double r = rand() / double(RAND_MAX);
115             for (int q = 0; q < POP_SIZE; q++) {
116                 if (r > cusFit[q] && r <= cusFit[q + 1]) {
117                     newPop[p] = Pop[q];
118                 }
119             }
120         }
121         Pop = newPop;
122     }
123 
124     //计时结束
125     auto end = system_clock::now();
126     auto duration = duration_cast<microseconds>(end - start);
127     cout << "花费了"
128         << double(duration.count()) * microseconds::period::num / microseconds::period::den
129         << "" << endl;
130 
131     //输出结果
132     double gs0 = 15377.711;
133     cout << "最优解为" << gs[MAX_GEN] << endl;
134     double e = (gs[MAX_GEN] - gs0) / gs0;
135     cout << "误差为" << e * 100.0 << '%' << endl;
136     cout << "最优路径为" << endl;
137     for (int i = 0; i < n; i++) {
138         cout << bestIndival[i] + 1 << '\t';
139     }
140
141 while (1)
142     {}

143 }

  以上即为C++语言所编写的遗传算法求解TSP示例,运行环境为:Windows10 64位操作系统;CPU:i7-8750H; 内存:8G;Microsoft Visual Studio Community 2017 。求解结果如下:

 

 

  与已知最优解的误差为1.48%,所用时间约为3.6s. 还可以接受。但值得注意的是:本文实验参数最大迭代次数4000代,而种群规模仅为2,这与一般的遗传算法思想上是没问题的,只是实际参数可能不太符合。当然,对于算法参数这些细节都是可以调节的,不必太过于纠结。

  啊哈,这次的利用C++编程遗传算法求解TSP就这些了~~~

 

版权声明:本文为Alex-Xu-OR原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/Alex-Xu-OR/p/10225879.html