|
|
import math
|
|
|
import random
|
|
|
import matplotlib.pyplot as plt
|
|
|
from collections import deque
|
|
|
from typing import List, Tuple
|
|
|
|
|
|
|
|
|
class TSPSolver:
|
|
|
def __init__(self,
|
|
|
cities: List[Tuple[int, int]],
|
|
|
max_iter: int = 1000,
|
|
|
base_tabu_length: int = 50,
|
|
|
candidate_size: int = 50):
|
|
|
|
|
|
# 数据初始化
|
|
|
self.cities = cities
|
|
|
self.n = len(cities)
|
|
|
self.distance_matrix = self._calc_distance_matrix()
|
|
|
|
|
|
# 算法参数
|
|
|
self.max_iter = max_iter
|
|
|
self.base_tabu_length = base_tabu_length
|
|
|
self.candidate_size = candidate_size
|
|
|
|
|
|
# 状态变量
|
|
|
self.current_solution = None
|
|
|
self.best_solution = None
|
|
|
self.best_cost = float('inf')
|
|
|
self.tabu_list = deque(maxlen=base_tabu_length)
|
|
|
self.history = {'best_cost': [], 'current_cost': []}
|
|
|
|
|
|
# 初始化解决方案
|
|
|
self._initialize_solution()
|
|
|
|
|
|
def _calc_distance_matrix(self) -> List[List[float]]:
|
|
|
"""计算距离矩阵"""
|
|
|
matrix = [[0.0] * self.n for _ in range(self.n)]
|
|
|
for i in range(self.n):
|
|
|
for j in range(self.n):
|
|
|
if i != j:
|
|
|
dx = self.cities[i][0] - self.cities[j][0]
|
|
|
dy = self.cities[i][1] - self.cities[j][1]
|
|
|
matrix[i][j] = math.sqrt(dx ** 2 + dy ** 2)
|
|
|
return matrix
|
|
|
|
|
|
def _initialize_solution(self):
|
|
|
"""生成初始解"""
|
|
|
random.seed(100)
|
|
|
self.current_solution = list(range(self.n))
|
|
|
random.shuffle(self.current_solution)
|
|
|
self.best_solution = self.current_solution.copy()
|
|
|
self.best_cost = self._evaluate(self.best_solution)
|
|
|
|
|
|
def _dynamic_tabu_length(self) -> int:
|
|
|
"""动态禁忌长度策略"""
|
|
|
current_iter = len(self.history['best_cost'])
|
|
|
return int(self.base_tabu_length * (1 + math.log(current_iter + 1) / 10))
|
|
|
|
|
|
def _generate_neighborhood(self) -> List[List[int]]:
|
|
|
"""混合邻域生成(交换操作+2-opt)"""
|
|
|
candidates = []
|
|
|
for _ in range(self.candidate_size):
|
|
|
if random.random() < 0.7: # 70%概率使用交换操作
|
|
|
i, j = sorted(random.sample(range(self.n), 2))
|
|
|
new_sol = self.current_solution.copy()
|
|
|
new_sol[i], new_sol[j] = new_sol[j], new_sol[i]
|
|
|
else: # 30%概率使用2-opt
|
|
|
i, j = sorted(random.sample(range(self.n), 2))
|
|
|
new_sol = self.current_solution[:i] + \
|
|
|
self.current_solution[i:j][::-1] + \
|
|
|
self.current_solution[j:]
|
|
|
candidates.append(new_sol)
|
|
|
return candidates
|
|
|
|
|
|
def _evaluate(self, solution: List[int]) -> float:
|
|
|
"""计算路径长度"""
|
|
|
total = 0.0
|
|
|
for i in range(self.n):
|
|
|
total += self.distance_matrix[solution[i]][solution[(i + 1) % self.n]]
|
|
|
return total
|
|
|
|
|
|
def optimize(self):
|
|
|
"""执行优化主循环"""
|
|
|
for iteration in range(self.max_iter):
|
|
|
candidates = self._generate_neighborhood()
|
|
|
|
|
|
best_candidate_cost = float('inf')
|
|
|
best_candidate = None
|
|
|
selected_move = None
|
|
|
|
|
|
for sol in candidates:
|
|
|
cost = self._evaluate(sol)
|
|
|
# 移动表示为排序后的城市索引对
|
|
|
move = tuple(sorted((
|
|
|
self.current_solution.index(sol[0]),
|
|
|
self.current_solution.index(sol[1]))
|
|
|
)) if len(sol) > 1 else (0, 0)
|
|
|
|
|
|
# 破禁条件判断
|
|
|
if (move in self.tabu_list and cost < self.best_cost) \
|
|
|
or (move not in self.tabu_list):
|
|
|
|
|
|
if cost < best_candidate_cost:
|
|
|
best_candidate_cost = cost
|
|
|
best_candidate = sol
|
|
|
selected_move = move
|
|
|
|
|
|
# 更新当前解
|
|
|
if best_candidate is not None:
|
|
|
self.current_solution = best_candidate
|
|
|
current_cost = best_candidate_cost
|
|
|
|
|
|
# 更新最优解
|
|
|
if current_cost < self.best_cost:
|
|
|
self.best_solution = best_candidate.copy()
|
|
|
self.best_cost = current_cost
|
|
|
|
|
|
# 更新禁忌表
|
|
|
self.tabu_list.append(selected_move)
|
|
|
self.tabu_list = deque(
|
|
|
list(self.tabu_list)[-self._dynamic_tabu_length():],
|
|
|
maxlen=self._dynamic_tabu_length()
|
|
|
)
|
|
|
|
|
|
# 记录历史数据
|
|
|
self.history['best_cost'].append(self.best_cost)
|
|
|
self.history['current_cost'].append(current_cost)
|
|
|
|
|
|
# 打印进度
|
|
|
if iteration % 100 == 0:
|
|
|
print(f"Iteration {iteration}: Best Cost = {self.best_cost:.2f}")
|
|
|
|
|
|
# 绘制收敛曲线
|
|
|
plt.figure(figsize=(10, 6))
|
|
|
plt.plot(self.history['best_cost'], label='Best Cost')
|
|
|
plt.plot(self.history['current_cost'], label='Current Cost')
|
|
|
plt.xlabel('Iteration')
|
|
|
plt.ylabel('Tour Length')
|
|
|
plt.title('Convergence Curve')
|
|
|
plt.legend()
|
|
|
plt.savefig('convergence.png')
|
|
|
plt.close()
|
|
|
|
|
|
# 绘制收敛曲线(论文规格)
|
|
|
plt.figure(figsize=(12, 8), dpi=300) # 提高分辨率和尺寸
|
|
|
plt.plot(self.history['best_cost'],
|
|
|
color='#2c7bb6',
|
|
|
linewidth=2.5,
|
|
|
linestyle='-',
|
|
|
label='Best Solution Length')
|
|
|
|
|
|
plt.plot(self.history['current_cost'],
|
|
|
color='#d7191c',
|
|
|
linewidth=1.2,
|
|
|
linestyle='--',
|
|
|
alpha=0.7,
|
|
|
label='Current Solution Length')
|
|
|
|
|
|
# 标注最优解
|
|
|
min_idx = self.history['best_cost'].index(min(self.history['best_cost']))
|
|
|
plt.scatter(min_idx, self.best_cost,
|
|
|
color='#fdae61',
|
|
|
s=120,
|
|
|
zorder=5,
|
|
|
label=f'Optimal Point ({self.best_cost:.2f})')
|
|
|
|
|
|
# 图表格式设置
|
|
|
plt.xlabel('Iteration', fontsize=14, fontweight='bold')
|
|
|
plt.ylabel('Tour Length', fontsize=14, fontweight='bold')
|
|
|
plt.title('Tabu Search Convergence Profile\n(50-City TSP Instance)',
|
|
|
fontsize=16,
|
|
|
pad=20)
|
|
|
|
|
|
plt.grid(True, linestyle=':', alpha=0.7)
|
|
|
plt.legend(fontsize=12,
|
|
|
loc='upper right',
|
|
|
frameon=True,
|
|
|
shadow=True)
|
|
|
|
|
|
# 坐标轴范围优化
|
|
|
plt.xlim(0, len(self.history['best_cost']))
|
|
|
buffer = 0.1 * (max(self.history['best_cost']) - self.best_cost)
|
|
|
plt.ylim(self.best_cost - buffer, max(self.history['best_cost']) + buffer)
|
|
|
|
|
|
# 保存多种格式
|
|
|
plt.savefig('convergence.pdf', bbox_inches='tight') # 矢量格式用于论文
|
|
|
plt.savefig('convergence.png', dpi=300, bbox_inches='tight') # 位图格式用于预览
|
|
|
print(f"convergence saved to convergence.png/.pdg")
|
|
|
plt.close()
|
|
|
|
|
|
# 输出最终结果
|
|
|
print(f"\nBest Tour Length: {self.best_cost:.2f}")
|
|
|
print("Optimal Route:", self.best_solution)
|
|
|
|
|
|
|
|
|
def plot_optimal_route(cities, solution, save_path='optimal_route.png'):
|
|
|
"""在坐标图上绘制TSP最优路径
|
|
|
|
|
|
Args:
|
|
|
cities: 城市坐标列表 [(x1,y1), (x2,y2), ...]
|
|
|
solution: 最优路径顺序 [idx1, idx2, ..., idxn]
|
|
|
save_path: 图片保存路径
|
|
|
"""
|
|
|
plt.figure(figsize=(12, 8), dpi=300)
|
|
|
|
|
|
# 提取坐标
|
|
|
x = [city[0] for city in cities]
|
|
|
y = [city[1] for city in cities]
|
|
|
|
|
|
# 绘制城市散点
|
|
|
plt.scatter(x, y, c='red', s=100, edgecolors='black', zorder=10, label='Cities')
|
|
|
|
|
|
# 添加城市标签
|
|
|
for i, (xi, yi) in enumerate(zip(x, y)):
|
|
|
plt.text(xi + 3, yi + 3, str(i), fontsize=8, ha='center', va='center',
|
|
|
bbox=dict(facecolor='white', alpha=0.7, edgecolor='none'))
|
|
|
|
|
|
# 绘制路径连线
|
|
|
route_x = [x[i] for i in solution] + [x[solution[0]]] # 闭合路径
|
|
|
route_y = [y[i] for i in solution] + [y[solution[0]]]
|
|
|
plt.plot(route_x, route_y,
|
|
|
color='#2c7bb6',
|
|
|
linewidth=1.5,
|
|
|
linestyle='-',
|
|
|
marker='o',
|
|
|
markersize=6,
|
|
|
markerfacecolor='yellow',
|
|
|
markeredgecolor='black',
|
|
|
label='Optimal Route')
|
|
|
|
|
|
# 标注起点和终点
|
|
|
start_x, start_y = x[solution[0]], y[solution[0]]
|
|
|
plt.scatter(start_x, start_y,
|
|
|
c='green', s=200,
|
|
|
edgecolors='black',
|
|
|
marker='*',
|
|
|
zorder=11,
|
|
|
label='Start/End')
|
|
|
|
|
|
# 图表美化
|
|
|
plt.title(f'TSP Optimal Route (Total Length: {solver.best_cost:.2f})',
|
|
|
fontsize=16, pad=20)
|
|
|
plt.xlabel('X Coordinate', fontsize=12)
|
|
|
plt.ylabel('Y Coordinate', fontsize=12)
|
|
|
plt.grid(True, linestyle='--', alpha=0.3)
|
|
|
plt.legend(fontsize=10, loc='upper right')
|
|
|
|
|
|
# 保存图像
|
|
|
plt.savefig(save_path, bbox_inches='tight')
|
|
|
plt.close()
|
|
|
print(f"Optimal route saved to {save_path}")
|
|
|
|
|
|
# 实验配置
|
|
|
if __name__ == "__main__":
|
|
|
# 固定城市坐标(与论文实验一致)
|
|
|
cities = [(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),
|
|
|
(50, 150), (110, 150), (170, 150), (70, 130), (130, 130),
|
|
|
(190, 130), (30, 110), (90, 110), (150, 110), (10, 90),
|
|
|
(80, 90), (160, 90), (40, 70), (120, 70), (200, 70),
|
|
|
(20, 50), (100, 50), (180, 50), (60, 30), (140, 30),
|
|
|
(90, 170), (170, 170), (30, 150), (150, 150), (50, 130),
|
|
|
(130, 130), (10, 110), (70, 110), (190, 110), (110, 90)] # 保持原有坐标数据
|
|
|
|
|
|
import matplotlib.pyplot as plt
|
|
|
|
|
|
# 提取x,y坐标
|
|
|
x = [city[0] for city in cities]
|
|
|
y = [city[1] for city in cities]
|
|
|
|
|
|
plt.figure(figsize=(10, 6))
|
|
|
plt.scatter(x, y, c='red', s=50, edgecolors='black', alpha=0.7)
|
|
|
|
|
|
# 添加城市标签
|
|
|
for i, (xi, yi) in enumerate(zip(x, y)):
|
|
|
plt.text(xi + 2, yi + 2, str(i), fontsize=8, ha='center', va='center')
|
|
|
|
|
|
plt.title('50-City TSP Problem Coordinates', fontsize=14)
|
|
|
plt.xlabel('X Coordinate', fontsize=12)
|
|
|
plt.ylabel('Y Coordinate', fontsize=12)
|
|
|
plt.grid(True, linestyle='--', alpha=0.5)
|
|
|
plt.tight_layout()
|
|
|
plt.savefig('city_coordinates.png', dpi=300)
|
|
|
plt.show()
|
|
|
# 初始化求解器
|
|
|
solver = TSPSolver(
|
|
|
cities=cities,
|
|
|
max_iter=1000,
|
|
|
base_tabu_length=int(math.sqrt(len(cities))),
|
|
|
candidate_size=2 * len(cities)
|
|
|
)
|
|
|
|
|
|
# 执行优化
|
|
|
solver.optimize()
|
|
|
plot_optimal_route(cities, solver.best_solution)
|