文章目录
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个景区经纬度数据的完整绘制: