【建模算法】matlab基于求解器intlinprog求解TSP问题
本案例说明如何使用二元整数规划来求解经典的TSP问题。此问题涉及找到一条历经一系列停留点(城市)的最短回路(路径)。在本例中有 52 个停留点,但你可以很轻松地更改 nStops
变量以得到不同规模的问题。对最初的问题进行求解后得到的解会包含子回路。这意味着找到的最优解并没有给出一条穿过所有点的连续路径,而是有几个独立的环路。然后,你可以使用迭代过程来确定子回路,添加约束,并重新运行优化,直到消除子回路。
一、问题描述
本案例以52个城市为例,假定52个城市的位置坐标如表1所列。寻找出一条最短的遍历52个城市的路径。
城市编号 | X坐标 | Y坐标 |
---|---|---|
1 | 565 | 575 |
2 | 25 | 185 |
3 | 345 | 750 |
4 | 945 | 685 |
5 | 845 | 655 |
6 | 880 | 660 |
7 | 25 | 230 |
8 | 525 | 1000 |
9 | 580 | 1175 |
10 | 650 | 1130 |
11 | 1605 | 620 |
12 | 1220 | 580 |
13 | 1465 | 200 |
14 | 1530 | 5 |
15 | 845 | 680 |
16 | 725 | 370 |
17 | 145 | 665 |
18 | 415 | 635 |
19 | 510 | 875 |
20 | 560 | 365 |
21 | 300 | 465 |
22 | 520 | 585 |
23 | 480 | 415 |
24 | 835 | 625 |
25 | 975 | 580 |
26 | 1215 | 245 |
27 | 1320 | 315 |
28 | 1250 | 400 |
29 | 660 | 180 |
30 | 410 | 250 |
31 | 420 | 555 |
32 | 575 | 665 |
33 | 1150 | 1160 |
34 | 700 | 580 |
35 | 685 | 595 |
36 | 685 | 610 |
37 | 770 | 610 |
38 | 795 | 645 |
39 | 720 | 635 |
40 | 760 | 650 |
41 | 475 | 960 |
42 | 95 | 260 |
43 | 875 | 920 |
44 | 700 | 500 |
45 | 555 | 815 |
46 | 830 | 485 |
47 | 1170 | 65 |
48 | 830 | 610 |
49 | 605 | 625 |
50 | 595 | 360 |
51 | 1340 | 725 |
52 | 1740 | 245 |
二、求解步骤
2.1 问题表示
将要进行整数线性规划的TSP问题表示如下:
- 生成所有可能的行程,意味着经过所有不同停留点对组。
- 计算每次行程的距离。
- 要最小化的代价函数是行程中每次旅行的旅行距离之和。
- 决策变量是与每个行程相关联的二元变量,其中每个 1 表示回路中存在的一次行程,每个 0 表示不在回路中的一次行程。
- 为确保回路包括每个经停留点,应加入这样一个线性约束:每个停留点都正好涉及两段行程。这意味着一段行程到达该停留点,一段行程离开该停留点。
2.2 生成停留点
读取数据,生成停留点
data=importdata('city052.xlsx');
X=data.data.Sheet1(:,2:3); %各城市坐标
nStops = 52; %52城市,可修改, 问题规模为N^2
stopsLon = X(:, 1); %城市x坐标
stopsLat = X(:, 2); %城市y坐标
2.3 计算停留点之间的距离
由于存在52个停留点,因此有 1326 次行程,这意味着有 1326 个二元变量(# 变量 = 52 选择 2)。
生成所有行程,这意味着生成所有停留点对组。
idxs = nchoosek(1:nStops,2);
计算所有行程距离,假设地球表面是平的以便使用毕达哥拉斯法则。
dist = hypot(stopsLat(idxs(:,1)) - stopsLat(idxs(:,2)), ...
stopsLon(idxs(:,1)) - stopsLon(idxs(:,2)));
lendist = length(dist);
根据 dist
向量的定义,一次回路的长度为
dist'*x_tsp
其中 x_tsp
是二元解向量。这是您尝试最小化的旅行距离。
2.4 创建图和绘制地图
将问题表示为图。创建一个图,其中停留点是节点,行程是边。
G = graph(idxs(:,1),idxs(:,2));
使用图论图显示停留点。绘制没有图边的节点。
%绘制停留点
figure;
hGraph = plot(G,'XData',stopsLon,'YData',stopsLat,'LineStyle','none','NodeLabel',{});
for i = 1: size(X, 1)
text(X(i, 1), X(i, 2), [' ',num2str(i)], 'color', [1, 0, 0]);
end
hold on
2.5 约束
创建线性约束,使每个停留点都有两个关联行程,因为每个停留点上必须有一次到达该停留点的行程和一次离开该停留点的行程。
%创建线性约束,使每个停留点都有两个关联行程,因为每个停留点上必须有一次到达该停留点的行程和一次离开该停留点的行程
Aeq = spalloc(nStops,length(idxs),nStops*(nStops-1)); %稀疏矩阵
for ii = 1:nStops
whichIdxs = (idxs == ii); %查找包含站点ii的行程
whichIdxs = sparse(sum(whichIdxs,2)); %包括ii位于两端的行程
Aeq(ii,:) = whichIdxs'; %包含在约束矩阵中
end
beq = 2*ones(nStops,1);
2.6 二元边界
所有决策变量均为二元变量。现在,将 intcon
参数设置为决策变量的数目,设置每个变量的下界为 0,上界为 1。
intcon = 1:lendist;
lb = zeros(lendist,1);
ub = ones(lendist,1);
2.7 使用 intlinprog 进行优化
此问题已可以求解。要隐藏迭代输出,请关闭默认的迭代输出。
opts = optimoptions('intlinprog','Display','off');
[x_tsp,costopt,exitflag,output] = intlinprog(dist,intcon,[],[],Aeq,beq,lb,ub,opts);
创建一个新图,将解行程作为边。为此,在某些值不是精确整数的情况下对解进行舍入,并将生成的值转换为 logical
。
x_tsp = logical(round(x_tsp));
Gsol = graph(idxs(x_tsp,1),idxs(x_tsp,2),[],numnodes(G));
2.8 可视化解
%可视化解
highlight(hGraph,Gsol,'LineStyle','-')
title('具有子回路的解')
从图上可以看出,解有几个子回路。到当前为止指定的约束没有阻止这些子回路的发生。要防止出现任何可能的子回路,您将需要极大数量的不等式约束。
2.9 子回路约束
由于无法添加所有子回路约束,需要采用迭代方法。检测当前解中的子回路,然后添加不等式约束以防止发生这些特定子回路。通过执行此操作,您只需几次迭代即可找到合适的回路。
使用不等式约束消除子回路。具体的工作原理如下:如果您在一个子回路中有五个点,则您有五条连接这些点的线来创建该子回路。可实现一个不等式约束来消除此子回路,即,这五个停留点之间存在的线必须小于或等于四条。
还要找到这五个停留点之间的所有线,并限制解中这些线不超过四条。这是正确的约束,因为如果解中存在五条或更多条线,则该解将有子回路(具有 n 个节点和 n 条边的图始终包含一个环路)。
通过识别 Gsol
中的连通分量来检测子回路,该图是用当前解中的边构建的。conncomp
返回一个向量,该向量包含每条边所属的子回路的编号。
tourIdxs = conncomp(Gsol);
numtours = max(tourIdxs); %子回路数目
fprintf('当前子回路数: %d\n',numtours);
加入线性不等式约束以消除子回路,并反复调用求解器求解,直到只剩下一个子回路。
A = spalloc(0,lendist,0); %分配稀疏线性不等式约束矩阵
b = [];
pic_num = 1;
while numtours > 1 %当子回路大于1时进入循环
%增加子回路约束
b = [b;zeros(numtours,1)]; %分配b
A = [A;spalloc(numtours,lendist,nStops)]; %猜测要分配多少个非零
for ii = 1:numtours
rowIdx = size(A,1) + 1; %索引计数器
subTourIdx = find(tourIdxs == ii); %提取当前子回路
variations = nchoosek(1:length(subTourIdx),2);
for jj = 1:length(variations)
whichVar = (sum(idxs==subTourIdx(variations(jj,1)),2)) & ...
(sum(idxs==subTourIdx(variations(jj,2)),2));
A(rowIdx,whichVar) = 1;
end
b(rowIdx) = length(subTourIdx) - 1;
end
%再次尝试优化
[x_tsp,costopt,exitflag,output] = intlinprog(dist,intcon,A,b,Aeq,beq,lb,ub,opts);
x_tsp = logical(round(x_tsp));
Gsol = graph(idxs(x_tsp,1),idxs(x_tsp,2),[],numnodes(G));
%可视化结果
hGraph.LineStyle = 'none'; %删除之前高亮显示的路径
highlight(hGraph,Gsol,'LineStyle','-')
drawnow;
pause(1);
F=getframe(gcf);
I=frame2im(F);
[I,map]=rgb2ind(I,256);
if pic_num == 1
imwrite(I,map,'TSP052.gif','gif', 'Loopcount',inf,'DelayTime',0.5);
else
imwrite(I,map,'TSP052.gif','gif','WriteMode','append','DelayTime',0.5);
end
%本次多少子回路?
tourIdxs = conncomp(Gsol);
numtours = max(tourIdxs); %子回路数目
fprintf('当前子回路数: %d\n',numtours)
pic_num = pic_num + 1;
end
title('最优解');
hold off
2.10 解质量
此解表示一个可行的回路,因为它是单一环路。但它是否为一次代价最低的回路?找出答案的一种方法是检查输出结构体。
disp(output.absolutegap)
绝对间隙小意味着该解或者是最优的,或者总长度接近最优。
三、求解结果
示例动画(此处为抓取的32城市求解动画)。
当前子回路数: 7
当前子回路数: 1
总距离:7544.3659
最优解轨迹图:
matlab完整源代码
%52城市TSP问题求解
clear,clc;
data=importdata('city052.xlsx');
X=data.data.Sheet1(:,2:3); %各城市坐标
nStops = 52; %52城市,可修改, 问题规模为N^2
stopsLon = X(:, 1); %城市x坐标
stopsLat = X(:, 2); %城市y坐标
%生成所有停留点对组
idxs = nchoosek(1:nStops,2);
%计算所有行程距离,假设地球表面是平的以便使用毕达哥拉斯法则
dist = hypot(stopsLat(idxs(:,1)) - stopsLat(idxs(:,2)), ...
stopsLon(idxs(:,1)) - stopsLon(idxs(:,2)));
lendist = length(dist);
%创建一个图,其中停留点是节点,行程是边。
G = graph(idxs(:,1),idxs(:,2));
%绘制停留点
figure;
hGraph = plot(G,'XData',stopsLon,'YData',stopsLat,'LineStyle','none','NodeLabel',{});
for i = 1: size(X, 1)
text(X(i, 1), X(i, 2), [' ',num2str(i)], 'color', [1, 0, 0]);
end
hold on
%创建线性约束,使每个停留点都有两个关联行程,因为每个停留点上必须有一次到达该停留点的行程和一次离开该停留点的行程
Aeq = spalloc(nStops,length(idxs),nStops*(nStops-1)); %稀疏矩阵
for ii = 1:nStops
whichIdxs = (idxs == ii); %查找包含站点ii的行程
whichIdxs = sparse(sum(whichIdxs,2)); %包括ii位于两端的行程
Aeq(ii,:) = whichIdxs'; %包含在约束矩阵中
end
beq = 2*ones(nStops,1);
%所有决策变量均为二元变量,将intcon参数设置为决策变量的数目,设置每个变量的下界为 0,上界为 1。
intcon = 1:lendist;
lb = zeros(lendist,1);
ub = ones(lendist,1);
%使用 intlinprog 进行优化求解
opts = optimoptions('intlinprog','Display','off');
[x_tsp,costopt,exitflag,output] = intlinprog(dist,intcon,[],[],Aeq,beq,lb,ub,opts);
%创建一个新图,将解行程作为边,在某些值不是精确整数的情况下对解进行舍入,并将生成的值转换为 `logical`。
x_tsp = logical(round(x_tsp));
Gsol = graph(idxs(x_tsp,1),idxs(x_tsp,2),[],numnodes(G));
%可视化解
highlight(hGraph,Gsol,'LineStyle','-')
title('具有子回路的解')
tourIdxs = conncomp(Gsol);
numtours = max(tourIdxs); %子回路数目
fprintf('当前子回路数: %d\n',numtours);
A = spalloc(0,lendist,0); %分配稀疏线性不等式约束矩阵
b = [];
pic_num = 1;
while numtours > 1 %当子回路大于1时进入循环
%增加子回路约束
b = [b;zeros(numtours,1)]; %分配b
A = [A;spalloc(numtours,lendist,nStops)]; %猜测要分配多少个非零
for ii = 1:numtours
rowIdx = size(A,1) + 1; %索引计数器
subTourIdx = find(tourIdxs == ii); %提取当前子回路
variations = nchoosek(1:length(subTourIdx),2);
for jj = 1:length(variations)
whichVar = (sum(idxs==subTourIdx(variations(jj,1)),2)) & ...
(sum(idxs==subTourIdx(variations(jj,2)),2));
A(rowIdx,whichVar) = 1;
end
b(rowIdx) = length(subTourIdx) - 1;
end
%再次尝试优化
[x_tsp,costopt,exitflag,output] = intlinprog(dist,intcon,A,b,Aeq,beq,lb,ub,opts);
x_tsp = logical(round(x_tsp));
Gsol = graph(idxs(x_tsp,1),idxs(x_tsp,2),[],numnodes(G));
%可视化结果
hGraph.LineStyle = 'none'; %删除之前高亮显示的路径
highlight(hGraph,Gsol,'LineStyle','-')
drawnow;
pause(1);
F=getframe(gcf);
I=frame2im(F);
[I,map]=rgb2ind(I,256);
if pic_num == 1
imwrite(I,map,'TSP052.gif','gif', 'Loopcount',inf,'DelayTime',0.5);
else
imwrite(I,map,'TSP052.gif','gif','WriteMode','append','DelayTime',0.5);
end
%本次多少子回路?
tourIdxs = conncomp(Gsol);
numtours = max(tourIdxs); %子回路数目
fprintf('当前子回路数: %d\n',numtours)
pic_num = pic_num + 1;
end
title('最优解');
hold off
%disp(output.absolutegap); %输出解的质量
disp(['总距离:', num2str(costopt)]);
disp('-------------------------');
本案例数据与源代码下载地址:https://download.csdn.net/download/baidu/85467610