基于遗传算法求解TSP问题

文章目录

TSP问题

‌TSP问题,即旅行商问题(Traveling Salesman Problem),是一个经典的组合优化问题。它描述了一个旅行商需要访问一系列城市,每个城市仅访问一次,并最后返回起点,目标是找到访问这些城市的最短路线。TSP问题是一个NP难问题,随着城市数量的增加,解决方案的空间迅速增长,导致精确算法在处理大规模问题时变得不切实际。因此,研究者们开发了各种启发式和近似算法来寻找可行解,尽管这些解可能不是最优的,但在实际应用中通常是可接受的。‌

GA算法

遗传算法是从代表问题可能潜在的解集的一个种群(population)开始的,而一个种群则由经过基因(gene)编码的一定数目的个体(individual)组成。每个个体实际上是染色体(chromosome)带有特征的实体。

染色体作为遗传物质的主要载体,即多个基因的集合,其内部表现(即基因型)是某种基因组合,它决定了个体的形状的外部表现,如黑头发的特征是由染色体中控制这一特征的某种基因组合决定的。因此,在一开始需要实现从表现型到基因型的映射即编码工作。由于仿照基因编码的工作很复杂,我们往往进行简化,如二进制编码( 1 代表接下来位置的基因存在,0 意味着丢失)。

初代种群产生之后,按照适者生存和优胜劣汰的原理,逐代(generation)演化产生出越来越好的近似解,在每一代,根据问题域中个体的适应度(fitness)大小选择(selection)个体,并借助于自然遗传学的遗传算子(genetic operators)进行组合交叉(crossover)和变异(mutation),产生出代表新的解集的种群。

这个过程将导致种群像自然进化一样的后生代种群比前代更加适应于环境,末代种群中的最优个体经过解码(decoding),可以作为问题近似最优解。

GA-TSP

使用遗传算法解决旅行商问题(TSP)通常包括以下几个步骤:

  • 初始化种群:

    生成一个初始种群,其中每个个体代表一个可能的解决方案,即城市访问顺序的一种排列。

    种群大小需要预先设定。

  • 适应度评估:

    计算每个个体的适应度值,对于TSP问题,通常是计算路径的总长度(以最短路径为目标)或其倒数(以转化为最大值优化为目标)。

    适应度函数可以帮助选择哪些个体更有可能被选中进行下一步的遗传操作。

  • 选择操作:

    根据适应度值从当前种群中选择个体进行繁殖,常用的选择方法有轮盘赌选择 、锦标赛选择等。

    目的是让适应度高的个体有更大的机会被选中,从而保留优秀的特征到下一代。

  • 交叉操作:

    对选出的个体进行配对,并通过交叉操作产生新的后代。

    交叉方法需要保证后代仍然是一条有效的路径,常见的交叉算子如顺序交叉、部分匹配交叉等。

    交叉概率需要提前设定。

  • 变异操作:

    对新产生的后代进行变异操作,以增加种群多样性。

    常见的变异操作包括交换两个城市的顺序、逆序一段路径等。

    变异概率通常较小,以避免破坏优秀的解决方案。

  • 替换操作:

    使用新产生的后代替换旧种群中的个体,可以采用精英策略保留最好的个体。

    替换操作确保每一代种群大小保持不变。

  • 终止条件判断:

    判断是否达到预设的终止条件,例如迭代次数、适应度阈值等。

    如果未达到,则返回步骤3继续迭代;否则,输出最优解。

  • 结果输出:

    输出最优路径及其对应的总距离和路线。

数据集

数据来源于华为杯研赛2015F题,通过经纬度查询提取好了31个省市共201个景区的经纬度数据,并用地图汇绘制:


以江苏省为例:

jiansu.tsp 格式

c 复制代码
NAME: st19
TYPE: TSP
COMMENT: 70-city problem (Smith/Thompson)
DIMENSION: 19
EDGE_WEIGHT_TYPE : EUC_2D
NODE_COORD_SECTION
1 120.636 31.3303 0.5 苏州园林
2 120.856 31.1208 0.5 苏州昆山
3 118.836 32.0709 0.5 南京钟山
4 120.24 31.4819 0.5 无锡影视
5 120.112 31.4444 0.5 无锡灵山
6 120.725 31.1641 0.5 苏州吴江
7 118.899 31.8231 0.5 南京夫子庙
8 120.015 31.824 0.5 常州恐龙
9 119.425 32.4148 0.5 扬州瘦西湖
10 120.878 32.0217 0.5 南通濠河
11 120.097 32.6301 0.5 泰州姜堰
12 120.705 31.3176 0.5 苏州金鸡湖
13 119.49 32.2453 1 镇江三山
14 120.226 31.5336 0.5 无锡鼋头渚
15 120.581 31.1743 1 苏州吴中太湖
16 120.708 31.6758 1 苏州常熟
17 119.447 31.3215 1 常州溧阳
18 119.314 31.7877 1 镇江句容
19 119.149 33.5131 0.5 淮安故里
EOF

jiangsu.txt 格式

c 复制代码
1 120.636 31.3303 0.5 苏州园林
2 120.856 31.1208 0.5 苏州昆山
3 118.836 32.0709 0.5 南京钟山
4 120.24 31.4819 0.5 无锡影视
5 120.112 31.4444 0.5 无锡灵山
6 120.725 31.1641 0.5 苏州吴江
7 118.899 31.8231 0.5 南京夫子庙
8 120.015 31.824 0.5 常州恐龙
9 119.425 32.4148 0.5 扬州瘦西湖
10 120.878 32.0217 0.5 南通濠河
11 120.097 32.6301 0.5 泰州姜堰
12 120.705 31.3176 0.5 苏州金鸡湖
13 119.49 32.2453 1 镇江三山
14 120.226 31.5336 0.5 无锡鼋头渚
15 120.581 31.1743 1 苏州吴中太湖
16 120.708 31.6758 1 苏州常熟
17 119.447 31.3215 1 常州溧阳
18 119.314 31.7877 1 镇江句容
19 119.149 33.5131 0.5 淮安故里
python完整代码
python 复制代码
import random
import math
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import rcParams

# 设置字体为 SimHei 显示中文
rcParams['font.sans-serif'] = ['SimHei']
rcParams['axes.unicode_minus'] = False  # 正常显示负号


class GA(object):
    def __init__(self, num_city, num_total, iteration, data):
        self.num_city = num_city
        self.num_total = num_total
        self.scores = []
        self.iteration = iteration
        self.location = data
        self.ga_choose_ratio = 0.2
        self.mutate_ratio = 0.05
        # fruits中存每一个个体是下标的list
        self.dis_mat = self.compute_dis_mat(num_city, data)
        self.fruits = self.greedy_init(self.dis_mat,num_total,num_city)
        # 显示初始化后的最佳路径
        scores = self.compute_adp(self.fruits)
        sort_index = np.argsort(-scores)
        init_best = self.fruits[sort_index[0]]
        init_best = self.location[init_best]

        # 存储每个iteration的结果,画出收敛图
        self.iter_x = [0]
        self.iter_y = [1. / scores[sort_index[0]]]

    def random_init(self, num_total, num_city):
        tmp = [x for x in range(num_city)]
        result = []
        for i in range(num_total):
            random.shuffle(tmp)
            result.append(tmp.copy())
        return result

    def greedy_init(self, dis_mat, num_total, num_city):
        start_index = 0
        result = []
        for i in range(num_total):
            rest = [x for x in range(0, num_city)]
            # 所有起始点都已经生成了
            if start_index >= num_city:
                start_index = np.random.randint(0, num_city)
                result.append(result[start_index].copy())
                continue
            current = start_index
            rest.remove(current)
            # 找到一条最近邻路径
            result_one = [current]
            while len(rest) != 0:
                tmp_min = math.inf
                tmp_choose = -1
                for x in rest:
                    if dis_mat[current][x] < tmp_min:
                        tmp_min = dis_mat[current][x]
                        tmp_choose = x

                current = tmp_choose
                result_one.append(tmp_choose)
                rest.remove(tmp_choose)
            result.append(result_one)
            start_index += 1
        return result
    # 计算不同城市之间的距离
    def compute_dis_mat(self, num_city, location):
        dis_mat = np.zeros((num_city, num_city))
        for i in range(num_city):
            for j in range(num_city):
                if i == j:
                    dis_mat[i][j] = np.inf
                    continue
                a = location[i]
                b = location[j]
                tmp = np.sqrt(sum([(x[0] - x[1]) ** 2 for x in zip(a, b)]))
                dis_mat[i][j] = tmp
        return dis_mat

    # 计算路径长度
    def compute_pathlen(self, path, dis_mat):
        try:
            a = path[0]
            b = path[-1]
        except:
            import pdb
            pdb.set_trace()
        result = dis_mat[a][b]
        for i in range(len(path) - 1):
            a = path[i]
            b = path[i + 1]
            result += dis_mat[a][b]
        return result

    # 计算种群适应度
    def compute_adp(self, fruits):
        adp = []
        for fruit in fruits:
            if isinstance(fruit, int):
                import pdb
                pdb.set_trace()
            length = self.compute_pathlen(fruit, self.dis_mat)
            adp.append(1.0 / length)
        return np.array(adp)

    def swap_part(self, list1, list2):
        index = len(list1)
        list = list1 + list2
        list = list[::-1]
        return list[:index], list[index:]

    def ga_cross(self, x, y):
        len_ = len(x)
        assert len(x) == len(y)
        path_list = [t for t in range(len_)]
        order = list(random.sample(path_list, 2))
        order.sort()
        start, end = order

        # 找到冲突点并存下他们的下标,x中存储的是y中的下标,y中存储x与它冲突的下标
        tmp = x[start:end]
        x_conflict_index = []
        for sub in tmp:
            index = y.index(sub)
            if not (index >= start and index < end):
                x_conflict_index.append(index)

        y_confict_index = []
        tmp = y[start:end]
        for sub in tmp:
            index = x.index(sub)
            if not (index >= start and index < end):
                y_confict_index.append(index)

        assert len(x_conflict_index) == len(y_confict_index)

        # 交叉
        tmp = x[start:end].copy()
        x[start:end] = y[start:end]
        y[start:end] = tmp

        # 解决冲突
        for index in range(len(x_conflict_index)):
            i = x_conflict_index[index]
            j = y_confict_index[index]
            y[i], x[j] = x[j], y[i]

        assert len(set(x)) == len_ and len(set(y)) == len_
        return list(x), list(y)

    def ga_parent(self, scores, ga_choose_ratio):
        sort_index = np.argsort(-scores).copy()
        sort_index = sort_index[0:int(ga_choose_ratio * len(sort_index))]
        parents = []
        parents_score = []
        for index in sort_index:
            parents.append(self.fruits[index])
            parents_score.append(scores[index])
        return parents, parents_score

    def ga_choose(self, genes_score, genes_choose):
        sum_score = sum(genes_score)
        score_ratio = [sub * 1.0 / sum_score for sub in genes_score]
        rand1 = np.random.rand()
        rand2 = np.random.rand()
        for i, sub in enumerate(score_ratio):
            if rand1 >= 0:
                rand1 -= sub
                if rand1 < 0:
                    index1 = i
            if rand2 >= 0:
                rand2 -= sub
                if rand2 < 0:
                    index2 = i
            if rand1 < 0 and rand2 < 0:
                break
        return list(genes_choose[index1]), list(genes_choose[index2])

    def ga_mutate(self, gene):
        path_list = [t for t in range(len(gene))]
        order = list(random.sample(path_list, 2))
        start, end = min(order), max(order)
        tmp = gene[start:end]
        # np.random.shuffle(tmp)
        tmp = tmp[::-1]
        gene[start:end] = tmp
        return list(gene)

    def ga(self):
        # 获得优质父代
        scores = self.compute_adp(self.fruits)
        # 选择部分优秀个体作为父代候选集合
        parents, parents_score = self.ga_parent(scores, self.ga_choose_ratio)
        tmp_best_one = parents[0]
        tmp_best_score = parents_score[0]
        # 新的种群fruits
        fruits = parents.copy()
        # 生成新的种群
        while len(fruits) < self.num_total:
            # 轮盘赌方式对父代进行选择
            gene_x, gene_y = self.ga_choose(parents_score, parents)
            # 交叉
            gene_x_new, gene_y_new = self.ga_cross(gene_x, gene_y)
            # 变异
            if np.random.rand() < self.mutate_ratio:
                gene_x_new = self.ga_mutate(gene_x_new)
            if np.random.rand() < self.mutate_ratio:
                gene_y_new = self.ga_mutate(gene_y_new)
            x_adp = 1. / self.compute_pathlen(gene_x_new, self.dis_mat)
            y_adp = 1. / self.compute_pathlen(gene_y_new, self.dis_mat)
            # 将适应度高的放入种群中
            if x_adp > y_adp and (not gene_x_new in fruits):
                fruits.append(gene_x_new)
            elif x_adp <= y_adp and (not gene_y_new in fruits):
                fruits.append(gene_y_new)

        self.fruits = fruits

        return tmp_best_one, tmp_best_score

    def run(self):
        BEST_LIST = None
        best_score = -math.inf
        self.best_record = []
        for i in range(1, self.iteration + 1):
            tmp_best_one, tmp_best_score = self.ga()
            self.iter_x.append(i)
            self.iter_y.append(1. / tmp_best_score)
            if tmp_best_score > best_score:
                best_score = tmp_best_score
                BEST_LIST = tmp_best_one
            self.best_record.append(1./best_score)
            print("epoch:{}, scores:{}.".format(i, 1./best_score))
        print("bestscore:", 1./best_score)
        return self.location[BEST_LIST], 1. / best_score


# 读取数据
def read_tsp(path):
    lines = open(path, 'r', encoding='utf-8').readlines()  # 指定编码为 utf-8
    assert 'NODE_COORD_SECTION\n' in lines
    index = lines.index('NODE_COORD_SECTION\n')
    data = lines[index + 1:-1]
    tmp = []
    for line in data:
        line = line.strip().split(' ')
        if line[0] == 'EOF':
            continue
        tmpline = []
        for x in line:
            if x == '':
                continue
            try:
                # 尝试将元素转换为浮点数
                tmpline.append(float(x))
            except ValueError:
                # 如果转换失败,则跳过该元素
                continue
        if tmpline == []:
            continue
        # 只保留前三列
        tmpline = tmpline[:3]
        tmp.append(tmpline)
    data = tmp
    return data



data = read_tsp('./data/jiangsu.tsp')
datas = data
names = np.loadtxt('./data/jiangsu_location.txt', usecols=(4), dtype=str, encoding='utf-8')
print(data)

data = np.array(data)
data = data[:, 1:]
Best, Best_path = math.inf, None

model = GA(num_city=data.shape[0], num_total=len(names), iteration=100, data=data.copy())
path, path_len = model.run()
if path_len < Best:
    Best = path_len
    Best_path = path
    
# 根据坐标查节点编号
coordinate_to_node_id = {tuple(row[1:]): row[0] for row in datas}
# print(coordinate_to_node_id[(120.636, 31.3303)])
# print(coordinate_to_node_id)

# 将最优路径中的坐标转换为节点编号
node_ids_path = [int(coordinate_to_node_id[tuple(coord)]) for coord in Best_path]
# print(node_ids_path)

# 打印最优路线的节点编号
print("最短路线的节点编号:", ' -> '.join(map(str, node_ids_path)))
# print("最短路线坐标:", Best_path)

def plot_shortest_path(best_solution):
    x = [datas[i - 1][1] for i in best_solution]  # 调整索引
    y = [datas[i - 1][2] for i in best_solution]
    x.append(x[0])  # 闭合路径
    y.append(y[0])

    plt.figure(figsize=(10, 6))
    plt.plot(x, y, marker='o', linestyle='-', color='b')
    for i, txt in enumerate(best_solution):
        plt.text(x[i]+0.01, y[i], str(txt), fontsize=16)
        plt.text(x[i]+0.05, y[i]+0.045, names[txt-1], fontsize=7)
    plt.title("Shortest Path")
    plt.xlabel("X Coordinate")
    plt.ylabel("Y Coordinate")
    plt.grid(True)
    # plt.show()
    plt.figure(2)
    iterations = range(model.iteration)
    best_record = model.best_record
    plt.plot(iterations, best_record)
    plt.title('收敛曲线')
    plt.show()

    
plot_shortest_path(node_ids_path)

打印结果:

jiangsu.txt:

[[1.0, 120.636, 31.3303], [2.0, 120.856, 31.1208], [3.0, 118.836, 32.0709], [4.0, 120.24, 31.4819], [5.0, 120.112, 31.4444], [6.0, 120.725, 31.1641], [7.0, 118.899, 31.8231], [8.0, 120.015, 31.824], [9.0, 119.425, 32.4148], [10.0, 120.878, 32.0217], [11.0, 120.097, 32.6301], [12.0, 120.705, 31.3176], [13.0, 119.49, 32.2453], [14.0, 120.226, 31.5336], [15.0, 120.581, 31.1743], [16.0, 120.708, 31.6758], [17.0, 119.447, 31.3215], [18.0, 119.314, 31.7877], [19.0, 119.149, 33.5131]]

epoch:1, scores:9.55367973155535.

epoch:2, scores:9.55367973155535.

epoch:3, scores:9.55367973155535.

epoch:4, scores:9.55367973155535.

epoch:5, scores:9.488811166148357.

epoch:6, scores:9.488811166148357.

epoch:7, scores:9.226366228327247.

epoch:8, scores:8.930828074608696.

epoch:9, scores:8.888290577339603.

epoch:10, scores:8.77739926112235.

epoch:11, scores:8.645300381731758.

epoch:12, scores:8.645300381731758.

epoch:13, scores:8.645300381731758.

epoch:14, scores:8.645300381731758.

epoch:15, scores:8.645300381731758.

epoch:16, scores:8.645300381731758.

epoch:17, scores:8.645300381731758.

epoch:18, scores:8.645300381731758.

epoch:19, scores:8.645300381731758.

epoch:20, scores:8.645300381731758.

epoch:21, scores:8.645300381731758.

epoch:22, scores:8.645300381731758.

epoch:23, scores:8.645300381731758.

epoch:24, scores:8.645300381731758.

epoch:25, scores:8.645300381731758.

epoch:26, scores:8.645300381731758.

epoch:27, scores:8.645300381731758.

epoch:28, scores:8.645300381731758.

epoch:29, scores:8.645300381731758.

epoch:30, scores:8.645300381731758.

epoch:31, scores:8.645300381731758.

epoch:32, scores:8.645300381731758.

epoch:33, scores:8.645300381731758.

epoch:34, scores:8.645300381731758.

epoch:35, scores:8.645300381731758.

epoch:36, scores:8.645300381731758.

epoch:37, scores:8.645300381731758.

epoch:38, scores:8.645300381731758.

epoch:39, scores:8.645300381731758.

epoch:40, scores:8.645300381731758.

epoch:41, scores:8.645300381731758.

epoch:42, scores:8.645300381731758.

epoch:43, scores:8.645300381731758.

epoch:44, scores:8.645300381731758.

epoch:45, scores:8.645300381731758.

epoch:46, scores:8.645300381731758.

epoch:47, scores:8.645300381731758.

epoch:48, scores:8.645300381731758.

epoch:49, scores:8.645300381731758.

epoch:50, scores:8.645300381731758.

epoch:51, scores:8.645300381731758.

epoch:52, scores:8.645300381731758.

epoch:53, scores:8.645300381731758.

epoch:54, scores:8.645300381731758.

epoch:55, scores:8.645300381731758.

epoch:56, scores:8.645300381731758.

epoch:57, scores:8.645300381731758.

epoch:58, scores:8.645300381731758.

epoch:59, scores:8.645300381731758.

epoch:60, scores:8.645300381731758.

epoch:61, scores:8.645300381731758.

epoch:62, scores:8.645300381731758.

epoch:63, scores:8.645300381731758.

epoch:64, scores:8.645300381731758.

epoch:65, scores:8.645300381731758.

epoch:66, scores:8.645300381731758.

epoch:67, scores:8.645300381731758.

epoch:68, scores:8.645300381731758.

epoch:69, scores:8.645300381731758.

epoch:70, scores:8.645300381731758.

epoch:71, scores:8.645300381731758.

epoch:72, scores:8.645300381731758.

epoch:73, scores:8.645300381731758.

epoch:74, scores:8.645300381731758.

epoch:75, scores:8.645300381731758.

epoch:76, scores:8.645300381731758.

epoch:77, scores:8.645300381731758.

epoch:78, scores:8.645300381731758.

epoch:79, scores:8.645300381731758.

epoch:80, scores:8.645300381731758.

epoch:81, scores:8.645300381731758.

epoch:82, scores:8.645300381731758.

epoch:83, scores:8.645300381731758.

epoch:84, scores:8.645300381731758.

epoch:85, scores:8.645300381731758.

epoch:86, scores:8.645300381731758.

epoch:87, scores:8.645300381731758.

epoch:88, scores:8.645300381731758.

epoch:89, scores:8.645300381731758.

epoch:90, scores:8.645300381731758.

epoch:91, scores:8.645300381731758.

epoch:92, scores:8.645300381731758.

epoch:93, scores:8.645300381731758.

epoch:94, scores:8.645300381731758.

epoch:95, scores:8.645300381731758.

epoch:96, scores:8.645300381731758.

epoch:97, scores:8.645300381731758.

epoch:98, scores:8.645300381731758.

epoch:99, scores:8.645300381731758.

epoch:100, scores:8.645300381731758.

bestscore: 8.645300381731758

最短路线的节点编号: 19 -> 9 -> 13 -> 3 -> 7 -> 18 -> 17 -> 8 -> 5 -> 14 -> 4 -> 1 -> 15 -> 6 -> 2 -> 12 -> 16 -> 10 -> 11

可以看出针对江苏省的19个景区节点,GA算法在20次迭代内很快就计算出了收敛的最优路线:

包含了31个省份共201个景区经纬度数据的完整绘制:

Reference

干货 | 遗传算法(Genetic Algorithm)
基于遗传算法实现TSP问题

相关推荐
gang_unerry1 天前
量子退火与机器学习(1):少量数据求解未知QUBO矩阵,以少见多
人工智能·python·算法·机器学习·数学建模·矩阵·量子计算
C灿灿数模3 天前
备战美赛!2025美赛数学建模C题模拟预测!用于大家练手模拟!
数学建模
数模竞赛Paid answer4 天前
2023年西南大学数学建模C题天气预报解题全过程文档及程序
算法·数学建模·数据分析
AI Dog5 天前
数学建模问题中的整数规划
算法·数学建模·整数规划·运筹学·malab
C灿灿数模分号15 天前
备战美赛!2025美赛数学建模C题模拟预测!用于大家练手模拟!
数学建模
从以前5 天前
python练习:“互联网 +” 时代的出租车资源配置的数学建模(二)
开发语言·python·数学建模
从以前5 天前
python练习:“互联网 +” 时代的出租车资源配置的数学建模(一)
开发语言·javascript·人工智能·python·算法·数学建模
Yaooooo87 天前
【数学建模】利用Matlab绘图(2)
开发语言·数学建模·matlab
Yangtze207 天前
紊流理论基础(三)——紊流半经验理论
线性代数·算法·数学建模·矩阵
最好Tony8 天前
优化算法之遗传算法思想和应用实例
算法·数学建模