利用Matlab优化工具箱求解旅行商最短路径问题

 前面介绍了利用Matlab二元整数规划求解数独问题,对于另一个问题-旅行商问题也可以用它来求解。

    旅行商问题就是找到经过所有站点的最短闭合路径,如下图为在美国地图框架内产生的200个旅行站点,而旅行商要找到一条最短路径将200个站点都旅行到。这也可以借助二元整数规划求解。
    这个例子比较典型:
    首先,例子的计算规模很大,200个站点,相应就有19900个二元变量,对如此多的变量同时进行优化,计算量可想而知,所以求解中使用了稀疏矩阵表达约束,也就是说Matlab优化函数是 接受稀疏矩阵形式的约束条件的;
    另外,对于常规的求解过程,会出现局部旅行问题,如下图出现的独立闭合圈。为了避免这些闭合圈的出现,通常通过增加不等式约束的方式解决,但相关不等式约束的数量将非常庞大,因为任意几个点之间都要保证不出现闭合。为此,例子使用了不断增加不等式约束的 迭代优化方式,即先不添加不等式约束进行优化,如果优化结果中出现闭合圈,则添加相应的约束条件避免这些圈的存在,然后再进行优化,直到不再有闭合圈存在。

    这个例子包含3个文件:TravellingSalesmanExample.m、updateSalesmanPlot.m和detectSubtours.m。其中第一个是整个问题的求解过程,可以用于发布,第二个用于更新每步的优化结果图,第三个为识别每次结果的闭合圈。下面将注释翻译后的文件粘贴如下
    TravellingSalesmanExample.m文件:
%% 旅行商问题,Travelling Salesman Problem
% 这个例子展示如何使用二元整数规划求解经典的旅行商问题。这个问题就是找到经过所有
% 站点的最短闭合路径。这个例子使用了200个站点,但也可以通过改变nStops来得到不同
% 运算量的求解过程。
% 对于初始情况,求解结果中存在子旅途,即结果没有给出一条经过所有站点的连续路线,
% 而是有一些独立的环存在。对此,可以通过迭代加约束、再优化的方法实现子旅途的去除。
 
clc
clear
close all
 
%% 绘制地图和站点
nStops = 200; % 站点个数,可以自己调整,不过求解复杂度与其平方成正比
stopsLon = zeros(nStops,1); % 分配站点的x坐标
stopsLat = stopsLon; % 分配y坐标
 
% 在代表美国的粗糙多边形内产生随机站点
load('usborder.mat','x','y','xx','yy'); % 加载数据,其中(x,y)为美国的精细轮廓,(xx,yy)为粗糙轮廓
rng(3,'twister'% 初始化随机序列,保证每次运行的重复性
n = 1;
while (n <= nStops)
    xp = rand*1.5;
    yp = rand;
    
    % 是否在(xx,yy)的粗糙多边形内
    if inpolygon(xp,yp,xx,yy)
        stopsLon(n) = xp;
        stopsLat(n) = yp;
        n = n+1;
    end
end
 
figure
plot(x,y,'Color','red'); % 多边形边界
hold on
plot(stopsLon,stopsLat,'*b'% 站点
hold off
pause(1)
 
%% 问题建模
% 将旅行商问题建模为整数线性规划问题:
% 1. 得到所有可能的旅行,即所有站点对;
% 2. 计算各旅行的距离;
% 3. 代价函数就是最小化旅途距离;
% 4. 决定变量是二元的,即如果旅行出现在旅途中,则为1,否则为0;
% 5. 为了保证旅途包含所有站点,可以设定如下线性约束:每个站点出现在两个旅行中,即达到和离开。
 
%% 计算点之间的距离
% 如果有200个站点,就有19900个旅行,即19900个二元变量。
idxs = nchoosek(1:nStops,2); % 得到所有组合
 
%% 计算旅行的距离
% 假设地球是平的,这样可以快速计算
dist = hypot(stopsLat(idxs(:,1)) - stopsLat(idxs(:,2)), ...
    stopsLon(idxs(:,1)) - stopsLon(idxs(:,2)));
lendist = length(dist);
 
%% 小结1
% 基于上面定义的dist向量,旅途长度为dist'*x,其中x为二元解向量。这正是我们要最小化的代价函数。
 
%% 等式约束
% 问题有两类等式约束,第一个保证共有200个旅行,第二个保证每个站点必须有2个旅行与其相关。
% 第一个可以用Aeq*x = beq的形式指定。
Aeq = spones(1:length(idxs)); % 全1向量,即将所有x相加
beq = nStops;
 
% 第二个通过扩展上面的Aeq来完成
Aeq = [Aeq;spalloc(nStops,length(idxs),nStops*(nStops-1))]; % 分配一个稀疏矩阵
for ii = 1:nStops
    whichIdxs = (idxs == ii); % 找到包含站点ii的所有旅行
    whichIdxs = sparse(sum(whichIdxs,2)); % 只要有任何一端为ii,即包括此旅行
    Aeq(ii+1,:) = whichIdxs'; % 加入Aeq矩阵
end
beq = [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');
tic
[xopt,costopt,exitflag,output] = intlinprog(dist,intcon,[],[],Aeq,beq,lb,ub,opts);
toc
 
%% 绘制结果
hold on
lh = zeros(nStops,1); % 用来保存线的句柄
lh = updateSalesmanPlot(lh,xopt,idxs,stopsLon,stopsLat);
title('带有子旅途的结果');
 
%% 小结2
% 从绘制结果可以看到,图中存在许多子旅途。到目前还没有设置约束阻止这些子旅途的出现。
% 而为了阻止它们的出现,需要设置难以想象的大量不等式约束。
 
%% 子旅途约束
% 由于不可能一下添加所有子旅途约束,所以可以通过迭代的方式添加约束。
% 即在当前结果中发现子旅途时,则添加相应的不等式约束来阻止这些特定子旅途的出现。
% 通过这种方式,通过简单的几次迭代就可找到合适的旅途。
 
% 利用不等式约束消除子旅途,比如出现一个5点的子旅途,则有5条线将这些点连起来,
% 所以可以要求这五个点之间的线不多于4条。如果找到这五个点之间的所有线,然后限定解中最多出现4条这种线。
% 这样限定是合理的,因为如果有5个或更多线出现在结果中,则将必然出现子旅途(具有n个节点和边的图必然存在圈)。
 
% 函数detectSubtours通过分析求解结果返回向量元胞数组,每个向量包含了组成子旅途的站点。
tours = detectSubtours(xopt,idxs);
numtours = length(tours); % 子旅途的个数
fprintf('子旅途的数目为: %d\n', numtours);
 
%% 加入线性不等式约束消除子旅途,然后重复优化过程,直到只得到一个子旅途为止
A = spalloc(0,lendist,0); % 分配稀疏线性不等式约束矩阵
b = [];
rowIdx = 0;
while numtours > 1 % 循环直到只有一个子旅途
    % 添加子旅途约束
    b = [b;zeros(numtours,1)];
    A = [A;spalloc(numtours,lendist,nStops)]; % 初始分配的稀疏空间不那么重要,这里设为nStops,少了会自动分配空间
    for ii = 1:numtours % 添加numtours个不等式约束
        rowIdx = rowIdx+1; % 指向约束的指针
        subTourIdx = tours{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; % 子旅途站点减1为b的值
    end
    
    % 再次尝试优化
    tic
    [xopt,costopt,exitflag,output] = intlinprog(dist,intcon,A,b,Aeq,beq,lb,ub,opts);
    toc
    
    % 显示结果
    lh = updateSalesmanPlot(lh,xopt,idxs,stopsLon,stopsLat);
    
    % 检测这次的子旅途数目?
    tours = detectSubtours(xopt,idxs);
    numtours = length(tours);
    fprintf('子旅途的数目: %d\n',numtours);
end
 
title('除去子旅途的结果');
hold off
 
%% 求解效果
% 结果给出了一个可行旅途,但它是否是最小代价的旅途呢?一种检验方式就是检查输出变量output
output  
% output中显示内部计算的上下限间隔为0,所以结果为最优解。  
 
    updateSalesmanPlot.m文件:
function lh = updateSalesmanPlot(lh,xopt,idxs,stopsLat,stopsLon)
% tsp_intlinprog例子的绘图函数
 
if ( lh ~= zeros(size(lh)) ) % 通过lh是否全零来判断是否第一次进入
    set(lh,'Visible','off'); % 移除之前的线
end
 
% 求解结果中的旅行
segments = find(xopt); 
 
% 形成绘制数据
Lat = zeros(3*length(segments),1);
Lon = zeros(3*length(segments),1);
for ii = 1:length(segments)
    start = idxs(segments(ii),1);
    stop = idxs(segments(ii),2);
    
    % 数据之间用NaN分割,这样可以绘制分离的线段,不然需要一个一个绘制线段,产生很多句柄
    Lat(3*ii-2:3*ii) = [stopsLat(start); stopsLat(stop); NaN];
    Lon(3*ii-2:3*ii) = [stopsLon(start); stopsLon(stop); NaN];
end
 
lh = plot(Lat,Lon,'k:','LineWidth',2);
set(lh,'Visible','on');
drawnow;
 
    detectSubtours.m文件:
function subTours = detectSubtours(x,idxs)
% 返回子旅途元胞数组,即图中的子圈
 
x = round(x); % 纠正不确切的整数
r = find(x);
substuff = idxs(r,:); % 旅行线的节点对
unvisited = ones(length(r),1); % 跟踪未访问的旅行
curr = 1; % 正在评价的子旅途
startour = find(unvisited,1); % 第一个未访问的旅行
while ~isempty(startour)
    home = substuff(startour,1);
    nextpt = substuff(startour,2);
    visited = nextpt;
    unvisited(startour) = 0;
    
    while nextpt ~= home
        % 找以nextpt为起点的旅行
        [srow,scol] = find(substuff == nextpt);
        
        % 确定相应旅行的节点
        trow = srow(srow ~= startour);
        scol = 3-scol(trow == srow); % 1变2,2变1
        startour = trow;
        nextpt = substuff(startour,scol); % 子旅途的下一节点位置
        
        visited = [visited,nextpt]; % 将节点加入子旅途
        unvisited(startour) = 0; % 更新访问过的位置
    end
    subTours{curr} = visited; % 保存找到的子旅途
    
    curr = curr + 1;
    startour = find(unvisited,1);
end
end
 
    运行TravellingSalesmanExample.m,得到的结果如下
Elapsed time is 3.394080 seconds.
子旅途的数目为: 28
Elapsed time is 12.648189 seconds.
子旅途的数目: 22
Elapsed time is 1.452217 seconds.
子旅途的数目: 10
Elapsed time is 3.314973 seconds.
子旅途的数目: 5
Elapsed time is 4.305792 seconds.
子旅途的数目: 3
Elapsed time is 8.356620 seconds.
子旅途的数目: 4
Elapsed time is 16.728625 seconds.
子旅途的数目: 1
output =
        relativegap: 0.0100
        absolutegap: 0.0021
      numfeaspoints: 5
           numnodes: 2258
    constrviolation: 2.5580e-13
            message: 'Optimal solution found.
Intlinprog stopped because the objective value is wi...'

from: http://chunqiu.blog.ustc.edu.cn/?p=812
  • 7
    点赞
  • 51
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
Matlab是一种强大的数值计算和科学编程软件,它提供了许多工具和函数来解决各种数学和工程问题旅行问题(Traveling Salesman Problem,TSP)是一个经典的组合优化问题,目标是找到一条最路径,使得旅行能够访问一系列城市并返回起始城市。 在Matlab中,可以使用优化工具箱求解旅行问题。以下是一种常见的求解方法: 1. 定义问题:首先,需要定义城市之间的距离矩阵。可以根据实际情况或者随机生成城市坐标,并计算城市之间的距离。 2. 构建模型:使用Matlab优化工具箱中的函数创建一个旅行问题的模型。可以使用`optimproblem`函数创建一个优化问题对象,并使用`optimvar`函数定义变量。 3. 添加约束:为了保证每个城市只能被访问一次,需要添加约束条件。可以使用`addConstraint`函数添加约束。 4. 添加目标函数:将旅行问题转化为求解路径问题,可以将路径长度作为目标函数。可以使用`addObjective`函数添加目标函数。 5. 求解问题:使用`solve`函数求解旅行问题。可以选择不同的求解算法和参数来获得最优解。 下面是一个示例代码,演示了如何使用Matlab求解旅行问题: ```matlab % 定义城市坐标和距离矩阵 cities = [0 0; 1 1; 2 2; 3 3]; distances = pdist(cities); % 创建优化问题对象 problem = optimproblem; % 定义变量 n = size(cities, 1); x = optimvar('x', n, n, 'Type', 'integer', 'LowerBound', 0, 'UpperBound', 1); % 添加约束条件 for i = 1:n problem.Constraints.rowsum{i} = sum(x(i, :)) == 1; problem.Constraints.colsum{i} = sum(x(:, i)) == 1; end % 添加目标函数 problem.Objective = sum(sum(distances .* x)); % 求解问题 [solution, fval] = solve(problem); % 输出最优解 path = find(round(solution.x)); disp('最优路径:'); disp(path); % 输出最路径长度 disp('最路径长度:'); disp(fval); ``` 这是一个简单的示例,实际应用中可能需要根据具体情况进行调整和优化。希望对你有所帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值