We choose the classic case of the genetic algorithm—the Traveling Salesman Problem—to introduce the specific implementation of the genetic algorithm.
Traveling Salesman Problem#
Given a series of cities and the distances between each pair of cities, the goal is to find the shortest route that visits each city once and returns to the starting city.
We will assign a coordinate to each city to calculate the distance between each pair of cities. For graph-related problems, the Floyd algorithm can be used to process the graph to obtain the shortest path between each pair of cities.
Global Constants and Variable Definitions#
const int SIZE = 1000, CNT = 1000;//Population size and maximum generations
using Pos = pair<double, double>;//Coordinate type
using Rst = vector<int>;//Chromosome type
vector<Pos> a;//City coordinate array
double ans = 1e9;//Global optimal solution, initialized to a large length that is impossible to achieve
Rst ans_rst;//Chromosome corresponding to this solution
double notans = 1e9;//Current population's optimal solution (therefore not the answer)
double geneSum = 0;//Sum of all solutions participating in the binary tournament in the current population
int geneCnt = 0;//Number of individuals participating in the binary tournament in the current population, seems to remain unchanged
//The above two lines are used to calculate the average solution of the population to observe changes in the population
Utility Functions#
//Get a random floating-point number, 0~1
double randf() {
return static_cast<float>(rand()) / static_cast<float>(RAND_MAX);
}
//Distance between two points
double getDis(Pos p1, Pos p2) {
return sqrt(pow(p1.first - p2.first, 2) + pow(p1.second - p2.second, 2));
}
Main Process#
int main() {
int n;
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
double tx, ty;
scanf("%lf%lf", &tx, &ty);
a.push_back(make_pair(tx, ty));
}
//The above is the data input part
//The entire population
vector<Rst> pool;
//Generate random chromosomes
for (int i = 1; i <= SIZE; ++i)
pool.push_back(randGene(n));
//Reproduce for CNT generations
for (int i = 1; i <= CNT; ++i) {
//Initialize statistics for this round
notans = 1e9;
geneSum = 0;
geneCnt = 0;
printf("%d ", i);
//Fitness array
vector<double> score;
//New population
vector<Rst> new_pool;
for (int i = 1; i <= CNT / 2; ++i) {
//Select two parents
auto win1 = choose(pool);
auto win2 = choose(pool);
//20% probability of crossover
if (randf() < 0.2) {
auto children = oxCross(win1, win2);
win1 = children.first;
win2 = children.second;
}
//Attempt mutation, 1% probability of mutation
bianYi(win1);
bianYi(win2);
//Insert into new population
new_pool.push_back(win1);
new_pool.push_back(win2);
}
//Output results for this round
printf("%lf %lf %lf\n", ans, notans, geneSum / geneCnt);
//Population change
pool = new_pool;
}
//Output the chromosome of the optimal solution
for (int v : ans_rst)
printf("%d ", v);
return 0;
}
Gene Encoding and Generation#
We use the order in which the traveling salesman visits as the gene encoding, and the chromosome is a decimal sequence of length equal to the number of cities.
//Randomly generate a chromosome
Rst randGene(int n) {
Rst ret;
unordered_map<int, bool> mp;
for (int i = 1; i <= n; ++i) {
int newr = rand() % n;
while (mp[newr])
newr = rand() % n;
ret.push_back(newr);
mp[newr] = true;
}
return ret;
}
Fitness Evaluation#
The inverse of the total distance is taken as the fitness. The total fitness produced by this method does not equal 1.
//Total distance traveled
double getValue(Rst &g) {
int len = g.size();
double s = 0;
for (int i = 1; i < len; ++i)
s += getDis(a[g[i - 1]], a[g[i]]);
s += getDis(a[g[0]], a[g[len - 1]]);
if (s < ans) {
ans = s;
ans_rst = g;
}
if (s < notans)
notans = s;
geneSum += s;
geneCnt++;
return s;
}
//Fitness
double getShiYingDu(Rst &g) { return 1.0 / getValue(g); }
Selection#
We use a binary tournament selection method, where two individuals are randomly drawn each time, and the one with higher fitness is retained. Correspondingly, the method of selecting multiple individuals for comparison is called n-tournament selection.
//Selection, binary tournament
Rst &choose(vector<Rst> &pool) {
int len = pool.size();
int i = rand() % len;
int j = rand() % len;
int big = getShiYingDu(pool[i]) > getShiYingDu(pool[j]) ? i : j;
return pool[big];
}
Crossover#
The crossover method we use is a type of order crossover. A segment is randomly selected from individual 2 and placed in front of individual 1, and the subsequent duplicate genes are removed. The same operation is performed using the same segment from another individual.
For example, from 1 5 4 3 2 and 5 3 2 1 4, randomly selecting positions 1 to 3 results in offspring 2 1 3 5 4 and 5 4 3 2 1.
//Order Crossover
pair<Rst, Rst> oxCross(Rst &r1, Rst &r2) {
int len = r1.size();
int i = rand() % len, j = i + rand() % (len - i);
Rst s1, s2;
unordered_map<int, bool> mp1, mp2;
for (int p = i; p <= j; ++p) {
s1.push_back(r2[p]);
mp1[r2[p]] = 1;
s2.push_back(r1[p]);
mp2[r1[p]] = 1;
}
for (int p = 0; p < len; ++p) {
if (!mp1[r1[p]]) {
s1.push_back(r1[p]);
mp1[r1[p]] = 1;
}
if (!mp2[r2[p]]) {
s2.push_back(r2[p]);
mp2[r2[p]] = 1;
}
}
return {s1, s2};
}
Mutation#
With a certain probability, two genes are randomly selected for swapping.
//Mutation
void bianYi(Rst &r) {
double rd = randf();
if (rd < 0.01) {
int len = r.size();
int i = rand() % len;
int j = rand() % len;
int t = r[i];
r[i] = r[j];
r[j] = t;
}
}
Running Test#
We randomly generated a set of test data containing 174 city coordinates. The following figure shows the optimal solution (red) and the average solution of the population (green) after 1000 generations of reproduction.
Data: https://gist.github.com/zhufengning/3a617fc3f3765cd192d42fb27ee374d0
Complete Code#
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <ctime>
#include <iostream>
#include <unordered_map>
#include <utility>
#include <vector>
using namespace std;
const int SIZE = 1000, CNT = 1000;
using Pos = pair<double, double>;
using Rst = vector<int>;
vector<Pos> a;
double ans = 1e9;
double notans = 1e9;
double geneSum = 0;
int geneCnt = 0;
Rst ans_rst;
//Get a random floating-point number, 0~1
double randf() {
return static_cast<float>(rand()) / static_cast<float>(RAND_MAX);
}
//Randomly generate a solution
Rst randGene(int n) {
Rst ret;
unordered_map<int, bool> mp;
for (int i = 1; i <= n; ++i) {
int newr = rand() % n;
while (mp[newr])
newr = rand() % n;
ret.push_back(newr);
mp[newr] = true;
}
return ret;
}
//Distance between two points
double getDis(Pos p1, Pos p2) {
return sqrt(pow(p1.first - p2.first, 2) + pow(p1.second - p2.second, 2));
}
//Total distance traveled
double getValue(Rst &g) {
int len = g.size();
double s = 0;
for (int i = 1; i < len; ++i) {
s += getDis(a[g[i - 1]], a[g[i]]);
}
s += getDis(a[g[0]], a[g[len - 1]]);
if (s < ans) {
ans = s;
ans_rst = g;
}
if (s < notans)
notans = s;
geneSum += s;
geneCnt++;
return s;
}
//Fitness
double getShiYingDu(Rst &g) { return 1.0 / getValue(g); }
//Selection, binary tournament
Rst &choose(vector<Rst> &pool) {
int len = pool.size();
int i = rand() % len;
int j = rand() % len;
int big = getShiYingDu(pool[i]) > getShiYingDu(pool[j]) ? i : j;
return pool[big];
}
//Order Crossover
pair<Rst, Rst> oxCross(Rst &r1, Rst &r2) {
int len = r1.size();
int i = rand() % len, j = i + rand() % (len - i);
Rst s1, s2;
unordered_map<int, bool> mp1, mp2;
for (int p = i; p <= j; ++p) {
s1.push_back(r2[p]);
mp1[r2[p]] = 1;
s2.push_back(r1[p]);
mp2[r1[p]] = 1;
}
for (int p = 0; p < len; ++p) {
if (!mp1[r1[p]]) {
s1.push_back(r1[p]);
mp1[r1[p]] = 1;
}
if (!mp2[r2[p]]) {
s2.push_back(r2[p]);
mp2[r2[p]] = 1;
}
}
return {s1, s2};
}
//Mutation
void bianYi(Rst &r) {
double rd = randf();
if (rd < 0.01) {
int len = r.size();
int i = rand() % len;
int j = rand() % len;
int t = r[i];
r[i] = r[j];
r[j] = t;
}
}
int main() {
int n;
scanf("%d", &n);
for (int i = 1; i <= n; ++i) {
double tx, ty;
scanf("%lf%lf", &tx, &ty);
a.push_back(make_pair(tx, ty));
}
//The above is the data input part
//The entire population
vector<Rst> pool;
//Generate random chromosomes
for (int i = 1; i <= SIZE; ++i)
pool.push_back(randGene(n));
//Reproduce for CNT generations
for (int i = 1; i <= CNT; ++i) {
//Initialize statistics for this round
notans = 1e9;
geneSum = 0;
geneCnt = 0;
printf("%d ", i);
//Fitness array
vector<double> score;
//New population
vector<Rst> new_pool;
for (int i = 1; i <= CNT / 2; ++i) {
//Select two parents
auto win1 = choose(pool);
auto win2 = choose(pool);
//20% probability of crossover
if (randf() < 0.2) {
auto children = oxCross(win1, win2);
win1 = children.first;
win2 = children.second;
}
//Attempt mutation, 1% probability of mutation
bianYi(win1);
bianYi(win2);
//Insert into new population
new_pool.push_back(win1);
new_pool.push_back(win2);
}
//Output results for this round
printf("%lf %lf %lf\n", ans, notans, geneSum / geneCnt);
//Population change
pool = new_pool;
}
//Output the chromosome of the optimal solution
for (int v : ans_rst)
printf("%d ", v);
return 0;
}