序
关于tsp问题的概述以及如何使用遗传算法进行求解已经在上一篇文章中说明了:遗传算法解决TSP问题.
但是,作为一种演化算法,遗传算法还存在着许多问题,比如早熟的情况,很容易在算法前期就已经收敛了,大量的个体都趋向于一致,种群多样性降低。
这种情况下,就很容易陷入局部最优解。即使扩大迭代次数、种群中的个体数量,也是无济于事的。
一、优化思想
那如何进行优化呢?一种做法就是,进行种群隔离,既然一个种群会出现趋同的情况,那么我们就搞许多个种群。
如果这些种群之间不进行通信,那么是不是可以认为他们就属于两个物种呢?
如果这样的话,那么我们采用少量的个体交换,对于这些种群,他们的物种丰富度是不是就提高了?
所以,这篇文章采用并行计算的方法,我们搞16个种群,这些种群每迭代2000次进行一次个体交换,通过这种多次的交换,来丰富物种多样性。
二、迁移策略
但是,如何在16个种群间进行物种迁移呢?这就要考虑到他们迁移的拓扑结构。
2.1 环形拓扑
环形拓扑主要就是将一个种群中的某个个体迁移到其他种群中去,具体如下:
2.2 随机拓扑
随机拓扑是任意的,即:一个种群可以向其他任意种群中传递个体。如图:
看起来就很乱,而且每一次种群间交流完全都是随机的。
三、替换策略
确定了那两个种群进行交换后,如何确定交换种群中的那两个个体呢?
3.1 最优-最差替换
最优发送:即选择种群中的最优个体给其他种群。
最差替换:接收到个体的种群,用别人给的最优的,替换自己最差的个体(适应度值最小)。
3.2 随机-随机替换
随机发送:随机发送一个个体给其他种群。
随机替换:接收到其他种群发送过来的个体后,随机选择一个自己种群的个体替换掉。
四、代码实现
串行改并行,主要分为两个步骤来进行。
第一步就是写好串行的代码;
第二步把串行的代码改成并行的。
最后可能还要稍微改下bug。
4.1 串行代码
#pragma once
// 解决TSP问题的重构算法(主要是tsp0的代码风格太丑陋了,修改成C++风格)
#include<iostream>
#include<fstream>
#include<vector>
#include<time.h>
#include<string>
using namespace std;
class TSP_M {
private:
int numCity; // 城市数量
vector<vector<double>> cityXY; // 城市坐标
vector<vector<double>> cityDis; // 城市间距离的邻接矩阵
int numColony = 100; // 种群数量
vector<vector<int>> colony; // 种群
vector<double> individualAdaptability; // 个体适应度
int maxGen = 200000; // 最大演化代数
int curGen = 0; // 当前演化代数
double probabilityMutation = 0.02; // 变异概率
vector<int> bestIndividual; // 当前最优个体
double bestAdaptability; // 当前最优个体的适应度
// 用于控制是否记录结果
bool isRecordResult2File = false;
bool isRecordResult2Console = true;
public:
void hello(int myrank) {
cout << "Hello from " << myrank << endl;
}
// 计算种群中每个个体的适应度
void calculateColonyAdaptability() {
// 计算每个个体的适应度
for (int i = 0; i < numColony; i++) {
double sum = 0;
for (int j = 0; j < numCity; j++) {
sum += cityDis[colony[i][j]][colony[i][(j + 1) % numCity]];
}
individualAdaptability[i] = sum;
}
}
// 计算个体的适应度
double calculateIndividualAdaptability(vector<int> path) {
double sum = 0;
for (int i = 0; i < numCity; i++) {
sum += cityDis[path[i]][path[(i + 1) % numCity]];
}
return sum;
}
// 计算距离矩阵
void calculateDistanceMatrix() {
// 初始化距离矩阵
cityDis.resize(numCity);
for (int i = 0; i < numCity; i++) {
cityDis[i].resize(numCity);
}
// 计算距离矩阵
for (int i = 0; i < numCity; i++) {
for (int j = 0; j < numCity; j++) {
cityDis[i][j] = sqrt(pow(cityXY[i][0] - cityXY[j][0], 2) + pow(cityXY[i][1] - cityXY[j][1], 2));
}
}
}
// 初始化 = 读取数据 + 计算数据
void init(string filePath) {
// 读取 tsp 文件
readTspFile(filePath);
// 初始化距离矩阵、种群等必要数据
calculateData();
}
// 进行迭代计算
void evolution() {
for (; curGen < maxGen; curGen++) { // 迭代maxGen次
for (int i = 0; i < numColony; i++) { // 遍历种群中所有个体
vector<int> path = colony[i]; // 用于存放变异后的路径
int posC1 = rand() % numCity; // 随机生成变异点1(在path中的位置)
int posC2 = rand() % numCity; // 随机生成变异点2(在path中的位置)
int C1, C2; // 变异点1和变异点2对应的城市编号
C1 = path[posC1]; // 获取变异点1对应的城市
int j = rand() % numColony; // 用于外变异的另一个 与 i个体 不同的个体
int pos_flag = 0; // 用于标记变异过的点的数量
double distanceChange = 0; // 用于记录距离变化
while (true)
{
// 以 probabilityMutation (default = 0.02)的概率进行内变异
if (rand() / 32768.0 < probabilityMutation) {
posC2 = rand() % numCity;
while (posC1 == posC2) { // 如果两个变异点相同,则重新生成
posC2 = rand() % numCity;
}
C2 = colony[i][posC2]; // 获取变异点1对应的城市
}
else { // 进行外变异(交叉)
j = rand() % numColony;
while (i == j) { // 如果两个个体相同,则重新生成
j = rand() % numColony;
}
// 获取个体 j 中 变异点1 对应城市的位置
int pos = position(colony[j], path[posC1]);
C2 = colony[j][(pos + 1) % numCity]; // 获取变异点2对应的城市
posC2 = position(path, C2); // 获取变异点2在个体 i 中的位置(即变异点2对应的城市在个体 i 中的位置
}
// 如果两个变异点相邻,continue
if ((posC1 + 1) % numCity == posC2 || (posC1 - 1 + numCity) % numCity == posC2)break;
//if (abs(posC1 - posC2) == 1 || abs(posC1 - posC2) == numCity - 1) {
// continue;
//}
// 否则进行倒位操作
int C1_left = path[posC1]; // 变异点1左边的城市
int C1_right = path[(posC1 + 1) % numCity]; // 变异点1右边的城市
int C2_left = path[posC2]; // 变异点2左边的城市
int C2_right = path[(posC2 + 1) % numCity]; // 变异点2右边的城市
// 计算倒位后的路径长度
distanceChange += cityDis[C1_left][C2_left] + cityDis[C1_right][C2_right]
- cityDis[C1_left][C1_right] - cityDis[C2_left][C2_right];
invert(path, posC1, posC2); // 倒位操作
pos_flag++; // 变异点数量加一
if (pos_flag >= numCity)break;
posC1++; // 变异点1的位置加一
if (posC1 >= numCity) posC1 = 0; // 如果变异点1的位置超过了numCity,则变异点1的位置为0
}
// 更新子个体的适应度
individualAdaptability[numColony + i] = individualAdaptability[i] + distanceChange;
distanceChange = 0;
// 记录 产生的 子个体
for (int j = 0; j < numCity; j++) {
colony[numColony + i][j] = path[j];
}
}
// 一轮迭代之后进行选择
selection();
bestIndividual = colony[0]; // 更新最优个体
bestAdaptability = individualAdaptability[0]; // 更新最优个体的适应度
for (int i = 1; i < numColony; i++) {
if (individualAdaptability[i] < bestAdaptability) {
bestIndividual = colony[i];
bestAdaptability = individualAdaptability[i];
}
}
if (isRecordResult2Console) {
// cout << "第" << curGen << "代的最优个体适应度为:" << bestAdaptability << endl;
cout << curGen << ":" << bestAdaptability << endl;
}
// 每 2000 代将最优个体的适应度写入文件
if (isRecordResult2File && (curGen + 1) % 2000 == 0) {
// 创建 outfile.txt 文件
ofstream outfile("outfile.txt", ios::app);
outfile << curGen << ":" << bestAdaptability << endl;
// 关闭文件
outfile.close();
}
}
}
// 获取城市在路径中的位置
int position(vector<int>& path, int city) {
for (int i = 0; i < numCity; i++) {
if (path[i] == city) {
return i;
}
}
return -1;
}
void invert(vector<int>& path, int pos1, int pos2) {
// 如果pos1在pos2的左边,为一段
if (pos1 < pos2) {
for (int i = pos1 + 1, j = pos2; i < j; i++, j--) {
swap(path[i], path[j]);
}
}
// 如果pos1在pos2的右边,为两段
else {
// 右边的段 <= 左边的段
if (numCity - 1 - pos1 <= pos2 + 1) {
int i, j;
for (i = pos2 + 1, j = pos1; i <= numCity - 1; i++, j--) {
swap(path[i], path[j]);
}
for (i = 0; i < j; i++, j--) {
swap(path[i], path[j]);
}
}
// 右边的段 > 左边的段
else {
int i, j;
for (i = pos2 + 1, j = pos1; j >= 0; i++, j--) {
swap(path[i], path[j]);
}
for (j = numCity - 1; i < j; i++, j--) {
swap(path[i], path[j]);
}
}
}
}
// 在父代和子代中进行一个锦标赛选择
void selection() {
for (int i = 0; i < numColony; i++) {
if (individualAdaptability[i] > individualAdaptability[numColony + i]) {
individualAdaptability[i] = individualAdaptability[numColony + i];
for (int j = 0; j < numCity; j++) {
colony[i][j] = colony[numColony + i][j];
}
}
}
}
// 读取tsp文件
bool readTspFile(string filePath) {
fstream input(filePath, ios::in);
if (!input) {
cout << "文件打开失败" << endl;
return false;
}
input >> numCity; // 城市数量
cout << numCity << endl;
// 初始化cityXY
cityXY = vector<vector<double>>(numCity, vector<double>(2));
// 读取城市坐标
double x, y;
for (int i = 0; i < numCity; i++) {
int tmp;
input >> tmp >> x >> y;
cout << tmp << " " << x << " " << y << endl;
cityXY[i][0] = x;
cityXY[i][1] = y;
}
// 关闭文件
input.close();
return true;
}
// 根据tsp数据计算城市之间的距离、并随机初始化种群、同时计算适应度
void calculateData() {
// 初始化cityDis
calculateDistanceMatrix();
// 初始化colony (包括父代和子代)
colony = vector<vector<int>>(2 * numColony, vector<int>(numCity));
// 以时间为种子,随机生成种群
srand((unsigned)time(NULL));
// 建立一个用于随机生成种群的数组
vector<int> tmp(numCity);
for (int i = 0; i < numCity; i++) {
tmp[i] = i;
}
// 随机初始化种群
for (int i = 0; i < numColony; i++) {
int numNeedToRand = numCity; // 当前需要随机的次数
for (int j = 0; j < numCity; j++) {
int randIndex = rand() % numNeedToRand; // 随机生成下标
colony[i][j] = tmp[randIndex]; // 将随机生成的下标对应的值赋给种群
swap(tmp[randIndex], tmp[numNeedToRand - 1]); // 将已经随机过的下标与最后一个下标交换
numNeedToRand--; // 需要随机的次数减一
}
}
// 初始化individualAdaptability
individualAdaptability = vector<double>(2 * numColony); // 后面的numColony个是用于存放子个体的适应度的
// 计算种群中每个个体的适应度
calculateColonyAdaptability();
}
// 替换掉当前种群中最差的个体
void replaceWorstIndividual(vector<int> individual) {
int worstIndex = 0;
for (int i = 1; i < numColony; i++) {
if (individualAdaptability[i] > individualAdaptability[worstIndex]) {
worstIndex = i;
}
}
individualAdaptability[worstIndex] = calculateIndividualAdaptability(individual);
for (int i = 0; i < numCity; i++) {
colony[worstIndex][i] = individual[i];
}
}
// 随机替换掉当前种群中的一个个体
void replaceRandomIndividual(vector<int> individual) {
int randIndex = rand() % numColony;
individualAdaptability[randIndex] = calculateIndividualAdaptability(individual);
for (int i = 0; i < numCity; i++) {
colony[randIndex][i] = individual[i];
}
}
// get 方法
// 获取城市数量
int getNumCity() {
return numCity;
}
// 获取城市坐标
vector<vector<double>> getCityXY() {
return cityXY;
}
// 获取最优个体
vector<int> getBestIndividual() {
int bestIndex = 0;
for (int i = 1; i < numColony; i++) {
if (individualAdaptability[i] < individualAdaptability[bestIndex]) {
bestIndex = i;
}
}
return colony[bestIndex];
}
// 随机获取一个个体
vector<int> getRandomIndividual() {
int randIndex = rand() % numColony;
return colony[randIndex];
}
// 获取最大迭代次数
int getMaxGen() {
return maxGen;
}
// 获取当前迭代次数
int getCurGen() {
return curGen;
}
// set 方法
void setNumCity(int numCity) {
this->numCity = numCity;
}
void setCityXY(vector<vector<double>> cityXY) {
this->cityXY = cityXY;
}
void setMaxGen(int maxGeneration) {
this->maxGen = maxGeneration;
}
};
4.2 并行代码
利用mpi编写的并行代码如下:
#include <stdio.h>
#include <mpi.h>
#include <iostream>
#include <algorithm>
#include "TSP_M.h"
using namespace std;
int main(int argc, char* argv[]){
// 迁移间隔
int migrationInterval = 2000;
// 迁移次数
int migrationTimes = 100;
// tsp文件路径
string tspFilePath = "./pcb442.tsp";
// 当前进程的编号、进程数量
int myrank, numProcess;
MPI_Init(&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &myrank);
MPI_Comm_size(MPI_COMM_WORLD, &numProcess);
// 创建TSP_M对象
TSP_M tsp;
if (myrank == 0) { // 地主进程
// 读取文件
tsp.readTspFile(tspFilePath);
// 给其他进程发送 tsp 中的数据
for (int i = 1; i < numProcess; i++) {
// 发送城市数量
int numCity = tsp.getNumCity();
MPI_Send(&numCity, 1, MPI_INT, i, 0, MPI_COMM_WORLD);
// 发送城市坐标
vector<vector<double>> cityXY = tsp.getCityXY();
for (int j = 0; j < numCity; j++) {
MPI_Send(&cityXY[j][0], 2, MPI_DOUBLE, i, 0, MPI_COMM_WORLD);
}
}
// 计算距离矩阵(后边计算适应度的时候会用到)
tsp.calculateDistanceMatrix();
}
else {
// 接收地主进程发送的数据
// 接收城市数量
int numCity;
MPI_Recv(&numCity, 1, MPI_INT, 0, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
// 接收城市坐标
vector<vector<double>> cityXY(numCity, vector<double>(2));
for (int j = 0; j < numCity; j++) {
MPI_Recv(&cityXY[j][0], 2, MPI_DOUBLE, 0, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
}
// 给TSP_M对象赋值
tsp.setNumCity(numCity);
tsp.setCityXY(cityXY);
// 计算距离矩阵等数据
tsp.calculateData();
// 设置最大迭代次数为2000
tsp.setMaxGen(migrationInterval);
// 进行迭代计算,每2000代通信一次,进行100轮
for (int i = 0; i < migrationTimes; i++) {
// 进行2000代迭代
tsp.evolution();
// 发送当前最优个体
vector<int> sendIndividualTo0 = tsp.getBestIndividual();
// 随机选择一个个体
// vector<int> sendIndividualTo0 = tsp.getRandomIndividual();
// 向 0 号进程发送当前最优个体
MPI_Send(&sendIndividualTo0[0], numCity, MPI_INT, 0, 0, MPI_COMM_WORLD);
// 接收 0 号进程发送的最优个体
vector<int> recvIndividualFrom0(numCity);
MPI_Recv(&recvIndividualFrom0[0], numCity, MPI_INT, 0, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
// 替换掉当前最差个体
tsp.replaceWorstIndividual(recvIndividualFrom0);
// 随机替换一个个体
// tsp.replaceRandomIndividual(recvIndividualFrom0);
// 设置最大迭代次数 + 2000
tsp.setMaxGen(tsp.getMaxGen() + migrationInterval);
}
}
if (myrank == 0) {
// 用于接收其他进程发送的个体
vector<vector<int>> recvIndividuals(numProcess - 1, vector<int>(tsp.getNumCity()));
// 进行100轮分发
for (int i = 0; i < migrationTimes; i++) {
for (int j = 1; j < numProcess; j++) {
MPI_Recv(&recvIndividuals[j - 1][0], tsp.getNumCity(), MPI_INT, j, 0, MPI_COMM_WORLD, MPI_STATUS_IGNORE);
}
// 环形拓扑,将收到的个体按顺序传递给下一个进程
// 1 -> 2 -> 3 -> ... -> numProcess - 1 -> 1
//for (int j = 1; j < numProcess - 1; j++) {
// MPI_Send(&recvIndividuals[j - 1][0], tsp.getNumCity(), MPI_INT, j + 1, 0, MPI_COMM_WORLD);
//}
//MPI_Send(&recvIndividuals[numProcess - 2][0], tsp.getNumCity(), MPI_INT, 1, 0, MPI_COMM_WORLD);
// 随机拓扑,将收到的个体打乱顺序
vector<int> randomOrder(numProcess - 1);
for (int j = 0; j < numProcess - 1; j++) {
randomOrder[j] = j;
}
// 采用随机洗牌算法打乱顺序
random_shuffle(randomOrder.begin(), randomOrder.end());
// 将打乱顺序后的个体传递给农民进程
for (int j = 0; j < numProcess - 1; j++) {
MPI_Send(&recvIndividuals[randomOrder[j]][0], tsp.getNumCity(), MPI_INT, j + 1, 0, MPI_COMM_WORLD);
}
}
// 输出 recvIndividuals 中的最优个体
double bestFitness = tsp.calculateIndividualAdaptability(recvIndividuals[0]);
int bestIndividualIndex = 0;
for (int i = 1; i < numProcess - 1; i++) {
double fitness = tsp.calculateIndividualAdaptability(recvIndividuals[i]);
if (fitness < bestFitness) {
bestFitness = fitness;
bestIndividualIndex = i;
}
}
cout << "best fitness: " << bestFitness << endl;
//cout << "best individual: ";
// for (int i = 0; i < tsp.getNumCity(); i++) {
// cout << bestIndividuals[bestIndividualIndex][i] << " ";
// }
// 将最优个体写入文件
fstream outfile;
outfile.open("bestIndividual.txt", ios::out);
for (int i = 0; i < tsp.getNumCity(); i++) {
outfile << recvIndividuals[bestIndividualIndex][i] << " ";
}
outfile.close();
}
MPI_Finalize();
return 0;
}
五、结果分析与讨论
采用matlab对求解结果进行可视化,代码如下:
clear all; %清除所有变量
close all; %清图
clc; %清屏
C=load('./file/tsp442.txt');
R_best=load('./file/data/并行-随机拓扑-随机替换.txt');
n = size(C,1);
for i=1:n-1 % 绘制历代最优路线的图
plot([ C(R_best(i) + 1,1), C(R_best(i+1) + 1,1)],... % 绘制 n-1 条边
[C(R_best(i) + 1,2), C(R_best(i+1) + 1,2)],'bo-');
hold on;
end
plot([C(R_best(n) + 1,1), C(R_best(1) + 1,1)],... % 连接首尾边
[C(R_best(n) + 1,2), C(R_best(1) + 1,2)],'ro-');
% 串行 51509.1
% 并行(100轮、2000次、环形拓扑、最好-最差) 51617.3
% 并行(100轮、2000次、环形拓扑、随机-随机) 51897.5
% 并行(100轮、2000次、随机拓扑、最好-最差) 51677.7
% 并行(100轮、2000次、随机拓扑、随机-随机) 51451
title(['并行(随机拓扑、最优-最差)遗传算法优化最短距离:',num2str(51677.7)]);
5.1 串行求解结果
最优路线长度:51509.1
5.2 并行求解结果
种群交流方式 | ||
---|---|---|
拓扑结构 | 环形拓扑 | 随机拓扑 |
替换策略 | 最优-最差 | 随机-随机 |
拓扑结构有2种选择,替换策略也有2种选择,因此,一共有4种组合方式。
5.2.1 环形拓扑、最优-最差
最优路线长度:51617.3
5.2.2 环形拓扑、随机-随机
最优路线长度:51897.5
5.2.3 随机拓扑、最优-最差
最优路线长度:51677.7
5.2.4 随机拓扑、随机-随机
最优路线长度:51451
5.3 结果分析
上述结果是在迭代200000次得到的结果,其中每2000次进行一次种群交流。
由于计算资源有限,没有进行大量实验,但由于迭代次数较大,实验结果应当是较为稳定的。
从有限的实验结果中可以看到,随机拓扑+随机替换策略,找到的解为51451,是更优的。
这也说明,我们通过种群间交流,主要目的就是增加种群的物种多样性,而通过两种随机的策略(随机拓扑+随机替换)相互结合,相比于(环形拓扑+最优替换)增大了种群交流的随机性,较好地利用了多个种群,所以产生了较好地效果。