1 引言
标准遗传算法(Sample Genetic Algorithm, SGA)存在显著的局限性,其交叉概率()和变异概率()通常设定为固定值。鉴于此,本文采用自适应遗传算法求解TSP问题。SGA对种群中的优良个体与劣质个体均执行相同概率的交叉与变异操作,这种策略主要引发以下两个问题:
- 缺乏个体层面的自适应性。 固定概率未能体现个体差异。理论上,优良个体应降低交叉变异概率以保护其优良基因不被破坏;而劣质个体应提高概率以促使其尽快改良性状。固定参数无法兼顾不同个体的需求,从而降低了算法的优化效率。
- 难以满足进化过程的动态需求(探索与开发)。 固定的概率设置无法匹配种群不同进化阶段的特点。迭代初期需要较高的概率以维持种群多样性,快速探索潜在的最优解;而在收敛后期则需要较小的概率以进行局部精细搜索,确保种群稳定收敛。因此,静态的交叉变异概率严重制约了算法的整体性能。
2 旅行商问题(Travelling salesman problem,TSP)数学模型
旅行商问题(Traveling Salesman Problem,简称 TSP),是数学领域中著名的组合优化问题,也是算法领域中最经典的NP-hard(非确定性多项式难度)问题之一。简单来说,它的核心就是“寻找最优路线”。以下是关于 TSP 的详细通俗解释,假设有一个旅行商人要去 个城市推销商品:
- 他需要从某一个城市出发。
- 必须经过所有 个城市,且每个城市只能去一次。
- 最后必须回到出发的那个城市。
- 目标:要求走过的总路程最短(或者总花费最少、总时间最短)。
虽然听起来这个问题很简单,但随着城市数量()的增加,可能的路线数量会呈现阶乘级爆炸。
- 如果只有 3 个城市,路线只有 条。
- 如果有 5 个城市,路线有 12 条。
- 如果有 10 个城市,路线就有 181,440 条。
- 如果有 20 个城市,路线约为 条。
- 如果有 50 个城市,路线数量是一个天文数字,即便用超级计算机暴力枚举所有可能,也需要几亿年才能算完。
因此,TSP 问题被认为是极难在有限时间内找到“绝对最优解”的。
刘兴禄 -《运筹优化常用模型、算法及案例实战:Python+Java实现》总结了TSP问题共有3种数学模型:
- Dantzig-Fulkerson-Johnson model,DFJ模型(本文采用)
- Miller-Tucker-Zemlin model,MTZ模型
- 1-tree模型
DFJ模型,也是最常见的模型如下:
另外以下文章总结了TSP几种建模方式的详细介绍,包括各种模型优劣:
3 遗传算法求解TSP问题
3.1初始种群生成
种群规模设置为100,随机城市顺序,采用自然数编码方式构造染色体,用0-19表示20个城市,染色体的编码如图所示,每条染色体表示城市访问顺序。如下图表示为:该旅行商从城市2出发,走到城市8,最后回到城市2。
3.2适应度计算
适应度函数是非负的,任何情况下总希望越大越好。TSP问题的目标函数是总距离越小越好,所以设计适应度为倒数变换法:
3.3 选择操作(精英策略+轮盘赌策略)
精英策略:即选择父代中适应度最大的个体遗传到子代。 轮盘赌选择:计算产生新个体的适应值,根据适应值大小分配个体的概率,根据概率方式选择个体作为下一代种群的父代。基本思想:各个个体被选中的概率与其适应度大小成正比,适应度值越好的个体,被选择的概率就越大。轮盘赌选择步骤如下:
- 计算适应度:适应度为该条染色体(一条可行路径)的总距离的倒数
- 计算每个个体的选择概率:选择概率是个体被遗传到下一代的概率,显然适应度愈大,该个体愈能适应环境,遗传到下一代的概率更高。染色体的选择概率计算公式为 ,其中代表个体,代表种群规模。
- 计算每个个体的累积概率:
- 执行精英策略:在当代种群population中把选择概率最大的个体,直接复制到子代种群offspring,即
offspring[1] = population[argmax(p_select)],其中p_select= - 执行轮盘赌选择:对每个个体,在[0, 1]区间生成一个随机数,若,则个体被选择至下一代。
- 重复步骤5,次。
3.4 交叉
TSP的染色体是城市的一个排列,而非二进制串或可重复编码。若直接截取父代A的中间段并替换到父代B对应位置,会导致子代出现重复城市编号(违反“每个城市仅访问一次”的约束)。选择部分匹配交叉(PMX crossover)作为本文的交叉算子,步骤如下:
- 随机选择两条染色体作为父代染色体,随机选择交叉基因的起止位置(两染色体被选位置相同)。
- 交换这两组基因的位置。
- 重复基因检测根据交换的两组基因建立一个映射关系(映射表),如图所示,以7-5-2这一映射关系为例,可以看到第二步结果中子代1存在两个基因7,这时将其通过映射关系转变为基因2,以此类推至没有冲突为止。最后所有冲突的基因都会经过映射,保证形成的新一对子代基因无冲突。
- 重复基因替换。对非交叉的基因片段进行扫描,若发现重复基因,则根据step3建立的映射表进行替换。保证了每个染色体中的基因仅出现一次,通过该交叉策略在一个染色体中不会出现重复的基因,所以经常用于旅行商(TSP)或其他排序问题编码。
3.5 变异
采用交换方法,即随机选择一条染色体,随机选择2个基因位,将这两个位置的基因交换,如下图。
3.6 停止准则
停止准则有3种,本文选择第三种。
- 当最优个体的适应度达到给定的阈值
- 最优个体的适应度和群体适应度不再上升
- 迭代次数达到预设的代数时,算法终止(本文采用)
4 数值实验
20个城市的TSP问题,每个城市的坐标如下(city20.txt):
60,200
180,200
80,180
140,180
20,160
100,160
200,160
140,140
40,120
100,120
180,100
60,80
120,80
180,60
20,40
100,40
200,40
20,20
60,20
160,20
程序包括以下几个类:
- GeneticAlgorithm遗传算法
- Main运行
- TSP读取文件(20个城市的坐标,)
- Node城市
C:.
│ .gitignore
│ pom.xml
│ project_structure.txt
│
├─.idea
│ │ .gitignore
│ │ compiler.xml
│ │ encodings.xml
│ │ jarRepositories.xml
│ │ misc.xml
│ │ uiDesigner.xml
│ │ workspace.xml
│ │
│ └─artifacts
│ unnamed.xml
│
├─src
│ └─main
│ │ att48.txt
│ │ city20.txt
│ │ eil51.txt
│ │
│ ├─java
│ │ └─org
│ │ └─example
│ │ Main.java
│ │ GeneticAlorithm
│ │ Main
│ │ Node
│ │ TSP
│ │
│ └─resources
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>adaptive_ga-tsp</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-math3</artifactId>
<version>3.6.1</version>
</dependency>
<dependency>
<groupId>org.icepear.echarts</groupId>
<artifactId>echarts-java</artifactId>
<version>1.0.3</version>
</dependency>
</dependencies>
</project>
算法程序:基于自适应遗传算法的TSP问题建模求解(Java)
完整程序:
package org.example;
import org.apache.commons.math3.stat.StatUtils;
import org.icepear.echarts.Bar;
import org.icepear.echarts.render.Engine;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;
public class Main {
public static void main(String[] args) throws IOException {
String filepath = "src/main/att48.txt";
filepath = "src/main/city20.txt";
GeneticAlgorithm ga = new GeneticAlgorithm(filepath, 10000, 100);
ga.runGA();
}
}
class Node {
int id;
double x;
double y;
Node(int id, double x, double y) {
this.id = id;
this.x = x;
this.y = y;
}
}
class TSP {
public double[][] distance;
public TSP(String fileName) throws IOException {
distance = this.readFile_(fileName);
}
public double[][] readFile_(String filepath) throws IOException {
int lineNum = (int) Files.lines(Paths.get(new File(filepath).getPath())).count();
System.out.println(lineNum);
Node[] nodes = new Node[lineNum];
double[][] distance = new double[nodes.length][nodes.length];
// 带缓冲的流读取,默认缓冲区8k
try (BufferedReader br = new BufferedReader(new FileReader(filepath))) {
String line;
while ((line = br.readLine()) != null) {
String[] data = line.split(" ");
int id = Integer.parseInt(data[0]);
int x = Integer.parseInt(data[1]);
int y = Integer.parseInt(data[2]);
Node node = new Node(id, x, y);
nodes[id - 1] = node;
}
} catch (IOException e) {
throw new RuntimeException(e);
}
for (int i = 0; i < nodes.length; i++) {
distance[i][i] = 0;
for (int j = 0; j < nodes.length; j++) {
distance[i][j] = distance[j][i] = Math.sqrt(Math.pow(nodes[i].x - nodes[j].x, 2) + Math.pow(nodes[i].y - nodes[j].y, 2));
}
}
return distance;
}
}
class GeneticAlgorithm {
TSP tsp;
double[][] distance;//距离矩阵
int popsize; //种群规模
int num_city;//基因数量
int[][] parent;//父代种群
double[] fitness;//适应度
int[][] child;//子代种群
private final int GEN;//最大迭代次数
private double pc = .8;//交叉概率
private double pm = .2;//变异概率
private final ThreadLocalRandom rand;
public GeneticAlgorithm(String fileName, int maxgen, int popsize) throws IOException {
this.distance = new TSP(fileName).distance;
this.pc = 0.;
this.pm = 0.;
this.GEN = maxgen;
this.popsize = 100;
this.num_city = distance.length;
this.parent = new int[popsize][];
this.child = new int[popsize][num_city];
this.fitness = new double[popsize];
this.rand = ThreadLocalRandom.current();
}
/**
* 初始化种群
*/
private void initialize_pop() {
for (int i = 0; i < this.popsize; i++) {
parent[i] = rand.ints(0, num_city).distinct().limit(num_city).toArray();
}
}
//适应度函数
private void calc_fitness() {
for (int i = 0; i < parent.length; i++) {
fitness[i] = 1e6 / calc_path_distance(parent[i]);
}
}
//更新交叉概率、变异概率
private void update_pc_pm() {
double fmax = StatUtils.max(fitness); //最大适应度
double favg = StatUtils.sum(fitness) / popsize; //平均适应度
pc = 100 / (fmax - favg);
pm = 100 / (fmax - favg);
}
/**
* @param chrom 染色体
* @return 一条染色体的总距离
*/
private double calc_path_distance(int[] chrom) {
double total_distance = 0;
for (int k = 0; k < chrom.length - 1; k++) {
int i = chrom[k];
int j = chrom[k + 1];
total_distance += distance[i][j];
}
total_distance += distance[chrom[chrom.length - 1]][0];
return total_distance;
}
/**
* 选择操作
*/
private void selection() {
//精英选择1条染色体
double max_fitness = StatUtils.max(fitness);
int elite = 0;
for (int i = 0; i < fitness.length; i++)
if (max_fitness == fitness[i]) {
elite = i;
break;
}
System.arraycopy(parent[elite], 0, child[0], 0, num_city);
//轮盘赌选择99条染色体
this.roulette_select();
}
/**
* 轮盘赌选择
*/
private void roulette_select() {
double[] p_cum = cumsum();
for (int i = 1; i < popsize; i++) {
double r = Math.random();
int selected = 0;
for (int j = 0; j < p_cum.length; j++) {
if (r < p_cum[j]) {
selected = j;//选择第j条染色体
break;
}
}
System.arraycopy(parent[selected], 0, child[i], 0, num_city);
}
}
//累积概率
private double[] cumsum() {
double[] p_cum = new double[popsize];
double[] p_select = calc_select_prob();
for (int i = 0; i < popsize; i++) {
double temp = 0.;
for (int j = 0; j <= i; j++) {
temp += p_select[j];
}
p_cum[i] = temp;
}
return p_cum;
}
//选择概率
private double[] calc_select_prob() {
double[] p_select = new double[popsize];
for (int i = 0; i < popsize; i++)
p_select[i] = fitness[i] / Arrays.stream(fitness).sum();
return p_select;
}
public void crossover() {
// 选择父代染色体
int i = rand.nextInt(1, popsize);
int j = rand.nextInt(1, popsize);
int[] parent1 = this.parent[i];
int[] parent2 = this.parent[j];
// 生成子代染色体
int[] child = new int[num_city];
// 选择交叉位置
int start = rand.nextInt(0, num_city);
int end = rand.nextInt(start, num_city);
int[] arr = Arrays.copyOfRange(parent1, start, end);
System.arraycopy(parent1, start, child, start, end - start);
// int p = 0;
// for (int k = 0; k < start; ) {
// if (!contain(arr, parent[j][p])) {
// child[k] = parent[j][p];
// k++;
// }
// p++;
// }
//
// for (int k = end; k < num_city; ) {
// if (!contain(arr, parent[j][p])) {
// child[k] = parent[j][p];
// k++;
// }
// p++;
// }
for (int k = 0, left = 0, right = end; k < num_city; k++) {
if (!contains(arr, parent2[k])) {
if (left < start) {
child[left] = parent2[k];
left += 1;
} else if (right < num_city) {
child[right] = parent2[k];
right += 1;
}
}
}
this.child[i] = child;
}
boolean contains(int[] arr, int element) {
for (int j : arr) {
if (element == j) {
return true;
}
}
return false;
}
/**
* 变异操作
*/
public void mutation() {
//i从1开始 保留精英
for (int i = 1; i < child.length; i++) {
int r1 = rand.nextInt(0, num_city);
int r2 = rand.nextInt(0, num_city);
int gene1 = child[i][r1];
int gene2 = child[i][r2];
child[i][r1] = gene2;
child[i][r2] = gene1;
}
}
public void runGA() {
initialize_pop();
calc_fitness();
int[] gbest = parent[0];
long start = System.currentTimeMillis();
for (int gen = 0; gen <= GEN; gen++) {
selection();
crossover();
mutation();
for (int i = 0; i < child.length; i++) {
// parent[i] = child[i];
System.arraycopy(child[i], 0, parent[i], 0, num_city);
}
gbest = child[0];
for (int i = 0; i < child.length; i++) {
if (isRepetition(child[i])) {
System.out.println(Arrays.toString(child[i]));
throw new IllegalArgumentException("repetition!");
}
}
calc_fitness();
if (gen % 200 == 0) {
System.out.printf("iteration: %d, path: %s, distance: %s\n", gen, Arrays.toString(gbest), calc_path_distance(gbest));
}
}
long end = System.currentTimeMillis();
System.out.printf("time used: %ss", (end - start) / 1000);
}
static boolean isRepetition(int[] args) {
Set<Object> set = new HashSet<>();
for (int arg : args) set.add(arg);
return set.size() != args.length;
}
}
迭代10000次结果如下,800多就逼近最优解:
Iteration: 0 path: [11, 17, 4, 2, 15, 10, 14, 18, 7, 12, 19, 8, 9, 5, 3, 1, 6, 16, 13, 0] distance: 1729.0 Iteration: 200 path: [15, 18, 17, 14, 11, 8, 4, 2, 9, 12, 13, 10, 7, 5, 3, 1, 6, 16, 19, 0] distance: 1122.0 Iteration: 400 path: [15, 18, 17, 14, 11, 8, 4, 2, 9, 12, 19, 16, 13, 10, 6, 1, 3, 7, 5, 0] distance: 893.0 Iteration: 600 path: [15, 18, 17, 14, 11, 8, 4, 5, 9, 12, 19, 16, 13, 10, 6, 1, 3, 7, 2, 0] distance: 887.0 Iteration: 800 path: [15, 18, 17, 14, 11, 8, 4, 5, 9, 12, 19, 16, 13, 10, 6, 1, 3, 7, 2, 0] distance: 887.0 Iteration: 1000 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 1200 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 1400 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 1600 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 1800 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 2000 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 2200 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 2400 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 2600 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 2800 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 3000 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 3200 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 3400 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 3600 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 3800 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 4000 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 4200 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 4400 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 4600 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 4800 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 5000 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 5200 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 5400 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 5600 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 5800 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 6000 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 6200 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 6400 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 6600 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 6800 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 7000 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 7200 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 7400 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 7600 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 7800 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 8000 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 8200 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 8400 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 8600 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 8800 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 9000 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 9200 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 9400 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 9600 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 9800 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 Iteration: 10000 path: [15, 18, 17, 14, 11, 8, 4, 9, 7, 12, 19, 16, 13, 10, 6, 1, 3, 5, 2, 0] distance: 879.0 time used: 1s
Python实现
import numpy as np
import matplotlib.pyplot as plt
import random
# ==========================================
# 1. 数据初始化
# ==========================================
# 定义城市坐标数据
# 直接把数字写进去,不需要作为字符串
cities = np.array([
[60, 200], [180, 200], [80, 180], [140, 180], [20, 160],
[100, 160], [200, 160], [140, 140], [40, 120], [100, 120],
[180, 100], [60, 80], [120, 80], [180, 60], [20, 40],
[100, 40], [200, 40], [20, 20], [60, 20], [160, 20]
])
# 参数设置
POP_SIZE = 50 # 种群规模 N
CROSS_RATE = 0.8 # 交叉概率
MUTATION_RATE = 0.02 # 变异概率
GENERATIONS = 1000 # 迭代次数
# ==========================================
# 2. 辅助函数
# ==========================================
# 计算路径总距离
def calculate_total_distance(path):
dist = 0
for i in range(len(path) - 1):
# 计算两点之间的欧几里得距离
dist += np.linalg.norm(cities[path[i]] - cities[path[i+1]])
# TSP需要回到起点
dist += np.linalg.norm(cities[path[-1]] - cities[path[0]])
return dist
# ==========================================
# 3.2 适应度计算 (倒数变换法)
# ==========================================
def calculate_fitness(distance):
# f_i = 1 / z_i
return 1.0 / distance
# ==========================================
# 3.4 交叉:部分匹配交叉
# ==========================================
def pmx_crossover(parent1, parent2):
size = len(parent1)
# 步骤1: 随机选择交叉基因的起止位置
start, end = sorted(random.sample(range(size), 2))
child1 = parent1.copy()
child2 = parent2.copy()
# 步骤2: 交换这两组基因的位置
child1[start:end] = parent2[start:end]
child2[start:end] = parent1[start:end]
# 步骤3 & 4: 重复基因检测与替换 (建立映射关系)
def resolve_conflicts(child, mapping_dict):
# 遍历非交叉区域的基因片段
for i in range(len(child)):
if i < start or i >= end:
gene = child[i]
# 如果当前基因在交换后的片段中已经存在(冲突)
while gene in mapping_dict:
# 根据映射关系转变基因
gene = mapping_dict[gene]
child[i] = gene
return child
# 建立映射表:父1片段->父2片段 和 父2片段->父1片段
# 例如:父1片段有7,父2片段有5,则映射为 7->5
map_for_child1 = {parent2[i]: parent1[i] for i in range(start, end)}
map_for_child2 = {parent1[i]: parent2[i] for i in range(start, end)}
child1 = resolve_conflicts(child1, map_for_child1)
child2 = resolve_conflicts(child2, map_for_child2)
return child1, child2
# ==========================================
# 3.5 变异:2-opt (文中描述为交换两个位置基因)
# ==========================================
def mutate_swap(individual):
# 随机选择一条染色体(这里是传入的individual)
# 随机选择2个基因位
idx1, idx2 = random.sample(range(len(individual)), 2)
# 将这两个位置的基因交换
individual[idx1], individual[idx2] = individual[idx2], individual[idx1]
return individual
# ==========================================
# 3.3 选择操作(精英策略+轮盘赌策略)
# ==========================================
def selection(population, fitness_scores):
N = len(population)
# 步骤1 & 2: 计算选择概率 p_i
total_fitness = np.sum(fitness_scores)
# 防止除以0
if total_fitness == 0:
p_select = np.ones(N) / N
else:
p_select = fitness_scores / total_fitness
# 步骤3: 计算累积概率 p_cul
p_cul = np.cumsum(p_select)
# 步骤4: 执行精英策略
# 找到适应度最大的个体的索引
elite_idx = np.argmax(fitness_scores)
new_population = []
new_population.append(population[elite_idx].copy())
# 步骤5 & 6: 执行轮盘赌选择 (重复 N-1 次)
for _ in range(N - 1):
# 在[0, 1]区间生成一个随机数
r = np.random.rand()
# 找到第一个累积概率大于等于r的索引
selected_idx = np.searchsorted(p_cul, r)
new_population.append(population[selected_idx].copy())
return np.array(new_population)
# ==========================================
# 主循环
# ==========================================
def run_ga():
num_cities = len(cities)
# 初始化种群
population = np.array([np.random.permutation(num_cities) for _ in range(POP_SIZE)])
best_distances = []
best_path_history = []
for gen in range(GENERATIONS):
# 计算当前种群所有个体的距离
distances = np.array([calculate_total_distance(ind) for ind in population])
# 3.2 适应度计算
fitness_scores = np.array([calculate_fitness(d) for d in distances])
# 记录当代最优
min_dist_idx = np.argmin(distances)
min_dist = distances[min_dist_idx]
best_distances.append(min_dist)
best_path_history.append(population[min_dist_idx])
if gen % 50 == 0:
print(f"Generation {gen}: Best Distance = {min_dist:.2f}")
# 3.3 选择操作
offspring = selection(population, fitness_scores)
# 交叉与变异
# 跳过索引0(精英个体),对剩下的个体进行操作
for i in range(1, POP_SIZE, 2):
# 确保不成对溢出
p1 = offspring[i]
if i + 1 < POP_SIZE:
p2 = offspring[i+1]
# 3.4 交叉
if np.random.rand() < CROSS_RATE:
c1, c2 = pmx_crossover(p1, p2)
offspring[i] = c1
offspring[i+1] = c2
else:
offspring[i] = p1
offspring[i+1] = p2
# 3.5 变异
if np.random.rand() < MUTATION_RATE:
offspring[i] = mutate_swap(offspring[i])
if np.random.rand() < MUTATION_RATE:
offspring[i+1] = mutate_swap(offspring[i+1])
else:
# 处理奇数情况
if np.random.rand() < MUTATION_RATE:
offspring[i] = mutate_swap(offspring[i])
population = offspring
print(f"Final Best Distance: {best_distances[-1]:.2f}")
return best_path_history[-1], best_distances
# 执行并绘图
best_path, dist_history = run_ga()
# --- 结果可视化 ---
plt.figure(figsize=(12, 5))
# 1. 收敛曲线
plt.subplot(1, 2, 1)
plt.plot(dist_history)
plt.title('Convergence Curve')
plt.xlabel('Generation')
plt.ylabel('Total Distance')
# 2. 最优路径
plt.subplot(1, 2, 2)
# 准备坐标用于绘图
path_coords = cities[best_path]
# 闭合路径
path_coords = np.vstack([path_coords, path_coords[0]])
plt.scatter(cities[:, 0], cities[:, 1], c='red', s=80, zorder=2)
# 标注城市编号
for i, (x, y) in enumerate(cities):
plt.text(x, y+5, str(i), ha='center', va='bottom', fontsize=9)
plt.plot(path_coords[:, 0], path_coords[:, 1], 'b-', zorder=1, alpha=0.6)
plt.title(f'Best Route (Distance: {dist_history[-1]:.2f})')
plt.grid(True, linestyle='--', alpha=0.5)
plt.tight_layout()
plt.show()