旅行商问题(TSP). 一个商人欲从自己所在的城市出发,到若干个城市推销商品,然后回到其所在的城市。如何选择一条周游路线,使得商人经过每个城市一次且仅一次后回到起点,并使他所走过的路径最短?
TSP 即Travelling Salesman Problem. 中文翻译过来就是旅行商问题。
旅行商问题是一个典型的NP难问题。NP指的是Non-deterministic Polynomial,即多项式复杂程度的非确定性问题。由于该问题的组合特性,旅行商问题已成为测试新算法的标准问题,如模拟退火、神经网络和演化算法等都用旅行商问题作为测试用例。旅行商问题的一个实例可由一个距离矩阵所给定。
用遗传算法求解旅行商问题时,适应函数可以取为目标函数或目标函数的一个简单变换,选择策略可以是轮盘赌选择,所以算法设计的重点主要集中在以下三个方面:
采用适当的方法对周游路线编码
设计专门的遗传算子,以避免不可行性
防止过早收敛
下面讨论周游路线常用的几种表示及其相应的遗传算子。
主要有3种编码表示
近邻表示
次序表示
路径表示
近邻表示将一条周游路线表示成n个城市 的一个排列Π=(Π1,Π2,...,Πn),Πi=j当且仅当周游路线中从城市i到达的下一个城市为j.
例如,排列
(2 4 8 3 9 7 1 5 6)
表示周游路线为1−2−4−3−8−5−9−6−7−1.
显然,每一条周游路线都对应一个近邻表示,但任一近邻排列却不一定对应于一条周游路线。
例如,排列
(2 4 8 1 9 3 5 7 6)
导致了不完全回路1−2−4−1,因而无法对应一条周游路线。所以周游路线的近邻表示就没有必要再往下介绍。
次序表示将一条周游路线表示为n个城市的有序表,其中,表中的第i个元素在1到n−i+1取值。该表的一个优点是每一个次序表示都对应于一条合法的周游路线。
次序表示的基本思想是:取n个城市的某个排列(Π1,Π2,...,Πn)作为参照排列,通常取(1,2,...,n)作为参照排列,然后将周游路线中的城市按照其在参照排列中的次序记录下来,形成一个具有n个元素的有序表.具体做法如下:
对给定的一条路径Π1−Π2−...−Πn,首先记录城市Π1在参照排列(p1,p2,...,pn)中的次序i1,将Π1从(p1,p2,...,pn)中删除得n−1个城市的参照排列(q1,q2,...,qk),k=n−1,再记录城市Π2在参照排列(q1,q2,...,qk),k=n−1中的次序i2,然后将城市Π2从(q1,q2,...,qk),k=n−1中删除得n−2个城市的参照排列(r1,r2,...,rk),k=n−2,以此类推,直到城市Πn的次序in记录下来为止。有序表(i1,i2,...,in)称为路径Π1−Π2−...−Πn的次序表示。注意ij的取值范围为1≤ij≤n−j+1.
例如,若取(1,2,...,9)作为参照排列,那么路径
1−2−4−3−8−5−9−6−7
的次序表示为
(1 1 2 1 4 1 3 1 1).
虽然次序表示具有可以使用传统杂交算子的优点,但实验表明在次序表示与传统的杂交算子相结合所得到的结果并不理想。
路径表示也许是周游路径最自然的一种表示。例如,一条周游路线
5−1−7−8−9−4−6−2−3−5
可表示为排列(5 1 7 8 9 4 6 2 3),这里将排列看成一个首尾相接的圆形排列。
本次案例对周游路线的编码采用路径表示的方案。
路径表示的杂交算子主要有以下几个:
部分映射杂交
次序杂交
循环杂交
基于位置杂交
部分映射杂交算子通过从一个父体中选择一个子序列,并尽可能多地保持另一个父体中城市的次序和位置的方式产生 后代。具体过程如下:
对给定的两个父体p1,p2,部分映射杂交首先随机地在父体上选择两个杂交点,然后交换父体p1,p2在这两点之间的中间段得到两个后代o1,o2,这一交换同时定义了一个部分映射。这时,o1,o2上除了这两点之间的城市外,其他的城市尚未确定。对o1,o2中尚未确定的城市,分别保留各自父体 中与中间段无冲突的城市,对与中间段有冲突的城市,则执行部分映射,直到与中间段无冲突为止,再将该城市填入相应的位置即可。
例如,给定下列两个父体,随机地选择两个杂交点 (以"|"标记),
p1=(2 6 4 | 7 3 5 8 | 9 1),
p2=(4 5 2 | 1 8 7 6 | 9 3),
将p1,p2在这两点之间 的中间段交换得
o1=(# # # | 1 8 7 6 | # #),
o2=(# # # | 7 3 5 8 | # #),
其中,#表示待定城市。这一交换所确定的部分映射为
1<−>7,
8<−>3,
7<−>5,
6<−>8.
然后,再从各自父体中填入与中间段无冲突的城市得
o1=(2 # 4 | 1 8 7 6 | 9 #),
o2=(4 # 2 | 7 3 5 8 | 9 #),
对o1,o2中与中间段有冲突的#部分,执行部分映射,直到无冲突为止。例如,o1中的第一个#初始为6,但6与中间段中的城市冲突,部分映射将6映射到8,但8仍与中间段冲突,部分映射到3,3与中间段不冲突,这时第一个#填入3.类似地可以确定其他的#部分,最后所得到的o1,o2如下:
o1=(2 3 4 | 1 8 7 6 | 9 5),
o2=(4 1 2 | 7 3 5 8 | 9 6),
部分映射杂交实现起来比较麻烦,跳来跳去的,所以本次案例不适用部分映射杂交。
次序杂交算子通过从一个父体中选择一个子序列,并保持另一个父体中城市的相对次序的方式产生后代。具体操作如下:
对给定的两个父体p1,p2,次序杂交首先随机地再父体上选择两个杂交点,分别保留父体p1,p2在这两个杂交点之间的中间段得到两个后代o1,o2.这时o1,o2上除了这两个中间段中的城市外,其他的城市尚未确定。对o1中尚未确定的城市,首先将p2中的城市从第二个杂交点开始按其相对次序列出,然后将该序列中在o1中间段中出现的城市删除,再将剩余子序列从第二个杂交点填入o1的相应位置即可。同样的方式可得o2.
例如,给定下列两个父体,随机地选择两个杂交点,
p1=(2 6 4 | 7 3 5 8| 9 1),
p2=(4 5 2 | 1 8 7 6 | 9 3),
保留p1,p2上两个杂交点之间的中间段得
o1=(# # # | 7 3 5 8 | # #),
o2=(# # # | 1 8 7 6 | # #),
其中,#表示待定城市。然后从第二个杂交点 开始,将p2中的城市按原次序列出得
9−3−4−5−2−1−8−7−6,
从中删除o1中间段中的城市得
9−4−2−1−6.
将该子序列从第二个杂交点开始依次填入到o1中得
,o1=(2 1 6 | 7 3 5 8 | 9 4),
类似可得
o2=(4 3 5 | 1 8 7 6 | 9 2).
本次案例使用的杂交算子就是次序杂交。
循环杂交算子如下产生后代:后代中的每个城市是某个父体相应位置的城市。具体过程如下:
对给定的两个父体p1=(u1,u2,...,un),p2=(v1,v2,...,vn),先确定后代o1中来自于父体p1中的城市。假定p1中的第一个城市u1在o1中,即将o1中的第一个位置填入u1,那么o1中的城市v1不可能来自p2,必来自于p1.假设v1=u1,若ui还没填入o1中,则在o1的第i个位置上填入ui.同样,o1中的城市vi不可能来自p2,必来自于p1.假设vi=ui,若uj还没有填入o1中,则在o1的第j个位置上填入uj.这样继续下去直到将某个uk填入到o1后,p2中与其相应的城市vk也已经填入到o1为止,这就构成一个循环。再将p2中尚未在o1中出现的城市直接填入到o1中相应的位置即可。类似可得后代o2.
例如,给定下列两个父体:
p1=(2 6 4 7 3 5 8 9 1),
p2=(4 5 2 1 8 7 6 9 3),
先从p1中取第一个城市2作为o1的第一个城市得
o1=(2 # # # # # # # #).
p1中城市2对应p2中的城市4,而4还没有填入到o1中,将4填入到o1中得
o1=(2 # 4 # # # # # #).
p1中城市4对应p2中的城市2,但2已经填入o1中了,这完成了一个循环。将p2中尚未在o1中出现的城市直接填入到o1中相应的位置后得
o1=(2 5 4 1 8 7 6 9 3).
类似可得
o2=(4 6 2 7 3 5 8 9 1).
基于位置杂交类似于次序杂交,唯一的区别在于:在基于位置杂交中,不是选取父体的一个连续的子序列,而是随机地选取一些位置,再按次序杂交的方式产生后代。
例如,给定下列两个父体:
p1=(2 6 4 7 3 5 8 9 1),
p2=(4 5 2 1 8 7 6 9 3),
假定随机地选取的位置为2,3,6,8.保留p1,p2上这些位置的城市得
o1=(# 6 4 # # 5 # 9 #),
o2=(# 5 2 # # 7 # 9 #),
然后从最后一个选取位置的后面开始,将p2中的城市按原次序列出得
3−4−5−2−1−8−7−6−9,
从中删除o1中已经选取的城市得
3−2−1−8−7.
将该子序列从最后一个选取位置的后面开始依次填入o1中得
o1=(2 6 4 1 8 5 7 9 3),
o2=(6 5 2 4 3 7 8 9 1).
路径表示的变异算子有下面几个:
倒位变异
插入变异
移位变异
互换变异
倒位变异的过程如下:
首先在父体上随机地选择两个城市,然后将这两个城市之间的子序列反转。
例如,设有父体
(1 2 3 4 5 6 7 8 9).
假定随机地选择的两个城市为3, 7,则对该个体进行倒位变异后得
(1 2 7 6 5 4 3 8 9).
插入变异的过程如下:
首先在父体中随机地选择一个城市,然后在该城市插入到某个随机的位置上。
例如,设有父体
(1 2 3 4 5 6 7 8 9).
假定随机地选择的城市为2,随机地选择位置为6,那么将城市2插入到第6个位置上得
(1 3 4 5 6 2 7 8 9).
要注意的是,当将所选择的城市插入到所选择的位置上时,有两种城市移动方式。若所选择的城市在要插入的位置的左边,则所选择城市到插入位置之间的城市(包括插入位置上的城市)向左移动一个位置,否则向右移动一个位置。
移位变异的过程如下:
首先在父体中随机地选择一个连续子序列,然后将该子序列插入到某个随机的位置上。
例如,设有父体
(1 2 3 4 5 6 7 8 9).
假定随机地选择的子序列为4 5 6,随机地选择的位置是8,那么经移位变异后得
(1 2 3 7 8 4 5 6 9).
互换变异随机地选择两个城市,并将这两个城市互换。
例如,设有父体
(1 2 3 4 5 6 7 8 9).
随机地选择的两个城市为3,7,那么经互换变异后得
(1 2 7 4 5 6 3 8 9).
本次案例采用的变异算子是互换变异。
本次使用Matlab来实现遗传算法解决旅行商问题。其中,所涉及到的各子函数放到附录中。
程序目录结构如下:
main.m
%% 遗传算法解决旅行商问题%% 清屏
clear; close; clc;
%% 加载数据
city = xlsread('./resources/city.xlsx'); % 加载Excel表格数据%% 参数初始化% 主要的参数
popsize = 200; % 种群大小
pc = 0.9; % 交叉概率
pm = 0.05; % 变异概率
gen = 1000; % 迭代次数% 次要的参数
D = Distance(city); % 城市距离矩阵
[citycount, ~] = size(city); % 记录城市个数
best_fitvalue = zeros(gen, 1); % 记录每一代最优的适应值
minlen = zeros(gen, 1); % 记录每一代路径最小值%% 初始化种群
pop = initpop(popsize, citycount);
%% 开始迭代for it = 1:gen
fitvalue = fitness(pop, D); % 计算适应值
[best_fitvalue(it), best_index] = max(fitvalue); % 记录适应值最高的值与下标
best_solution = pop(best_index, :); % 记录当前代的最优个体
minlen(it) = decode(best_solution, D, citycount); % 记录每一代的最短路径
newpop = parent_selection(pop, fitvalue); % 父体选择
newpop = crossover(newpop, pc); % 交叉
newpop = mutation(newpop, pm); % 变异
pop = newpop; % 更新种群
pop(mod(ceil(rand * citycount), citycount)+1, :) = best_solution; % 保留最优个体end%% 画图figure(1)
fori = 2:citycount
plot([city(best_solution(i-1), 1), city(best_solution(i), 1)], ...
[city(best_solution(i-1), 2), city(best_solution(i), 2)], 'bo-');
hold on;
endplot([city(best_solution(citycount), 1), city(best_solution(1), 1)], ...
[city(best_solution(citycount), 2), city(best_solution(1), 2)], 'ro-');
title(['优化最短距离:', num2str(min(minlen))]);
figure(2)
plot(best_fitvalue);
xlabel('迭代次数')
ylabel('目标函数值')
title('适应值变化曲线')
figure(3)
plot(minlen);
xlabel('迭代次数')
ylabel('目标函数值')
title('最短路径变化曲线')
%% 打印周游路线disp('周游路线:')
words = num2str(best_solution(1));
fori = 2:citycount
words = append(words, '->', num2str(best_solution(i)));
end
words = append(words, '->', num2str(best_solution(1)));
disp(words);
效果图:
从图1中可以看到优化最短路径为0.11798(单位自己换算,一开始用的点是经纬度表示的)周游路线已绘出,没有交叉的小圈,基本上是最优解了;图2是适应值变化曲线,这个是记录每一代的最优个体的适应值,一直在波动,很符合遗传算法的随机性;图3记录的是每一代的最短路径,基本在第800代左右收敛到最优解;命令窗口也将周游路线打印出来,得本次运行的最短路线为
6->7->3->2->5->9->11->13->15->17->19->20->18->16->14->12->10->8->1->4->6。
Distance.m
functionD = Distance(city)%Distance - 计算两个城市之间的距离%% Syntax: D = Distance(city)%% Long 返回的是一个距离矩阵,记录每两个城市之间的距离
[n, ~] = size(city);
D = ones(n, n);
fori = 1:n
forj = i:n
D(i, j) = sqrt((city(i, 1) - city(j, 1)).^2 + (city(i, 2) - city(j, 2)).^2);
D(j, i) = D(i, j);
endendend
decode.m
functionpath_len = decode(code, D, n)%decode - 对种群个体解码%% Syntax: path_len = decode(code, D, n)%% Long 返回的是路径长度,一个1*1的矩阵(变量)
path_len = 0;
forj = 2:n
path_len = path_len + D(code(j-1), code(j));
end
path_len = path_len + D(code(n), code(1));
end
initpop.m
functionpop = initpop(popsize, citycount)%initpop - 初始化种群%% Syntax: pop = initpop(popsize, citycount)%% Long 返回一个popsize*citycount的矩阵
pop = ones(popsize, citycount);
fori = 1:popsize
pop(i, :) = randperm(citycount);
endend
fitness.m
functionfitvalue = fitness(pop, D)%fitness - 计算适应值%% Syntax: fitvalue = fitness(pop, D)%% Long 返回的是px行1列的矩阵,记录的是当前种群pop的每一个个体的适应值
[px, py] = size(pop);
fitvalue = zeros(px, 1);
fori = 1:px
fitvalue(i) = decode(pop(i, :), D, py);
end
fitvalue = 1./fitvalue;
fitvalue = fitvalue./sum(fitvalue); % 适应值归一化end
parent_selection.m
functionnewpop = parent_selection(pop, fitvalue)%parent_selection - 父体选择%% Syntax: newpop = parent_selection(pop, fitvalue)%% Long 返回和pop一样大小的矩阵,新种群,采用轮盘赌选择策略
[px, py] = size(pop);
newpop = zeros(px, py);
%% 设计轮盘
p = cumsum(fitvalue); % 在计算适应值函数中已经做好归一化%% 转动轮盘
r = sort(rand(px, 1));
j = 1;
fori = 1:px
while r(i) > p(j)
j = j + 1;
end% r(i) <= p(j)
newpop(i, :) = pop(j, :);
endend
crossover.m
functionnewpop = crossover(pop, pc)%crossover - 交叉%% Syntax: newpop = crossover(pop, pc)%% Long description
[px, ~] = size(pop);
newpop = pop;
fori = 2:2:px
ifrand <= pc
[newpop(i, :), newpop(i-1, :)] = sub_crossover(pop(i, :), pop(i-1, :));
endendendfunction[newcode1, newcode2] = sub_crossover(code1, code2)%sub_crossover - 子交叉函数%% Syntax: [newcode1, newcode2] = sub_crossover(code1, code2)%% Long 采用次序杂交的方式进行杂交
newcode1 = code1;
newcode2 = code2;
n = length(code1);
front = -1;
rear = -1;
while front < 1 || rear <= front || rear >= n
front = ceil(rand * n);
rear = ceil(rand * n);
end
p2 = code2;
p1 = code1;
fori = front:rear
p2(p2 == code1(i)) = [];
p1(p1 == code2(i)) = [];
endj = 1;
fori = 1:n
ifi < front || i > rear
newcode1(i) = p2(j);
newcode2(i) = p1(j);
j = j + 1;
endendend
mutation.m
functionnewpop = mutation(pop, pm)%mutation - 变异%% Syntax: newpop = mutation(pop, pm)%% Long 采用互换变异算子
[px, py] = size(pop);
newpop = pop;
cnt = ceil(rand*py/2); % 确定变异点的个数
pos = mod(ceil(rand(px, cnt) * py), py) + 1; % 变异点的位置fori = 1:px
ifrand <= pm
forj = 2:2:cnt
temp = newpop(i, pos(i, j));
newpop(i, pos(i, j)) = newpop(i, pos(i, j-1));
newpop(i, pos(i, j-1)) = temp;
endendendend
>>> city
city =
109.417724.3085109.444324.3086109.439424.3089109.422624.3090109.447124.3092109.427724.3093109.434724.3101109.416824.3102109.447024.3167109.417524.3186109.446924.3230109.419724.3240109.446724.3285109.421824.3313109.446524.3350109.420524.3372109.446224.3392109.420924.3397109.441324.3415109.435624.3418