DM872 A Cutting Plane Approach for the Traveling Salesman Problem

The reference site on the TSP is https://www.math.uwaterloo.ca/tsp/index.html

Part of this notebook is taken from the beautiful tutorial by Peter Norvig: http://nbviewer.jupyter.org/url/norvig.com/ipython/TSP.ipynb

Consider the Traveling Salesperson Problem:

Given a set of cities and the distances between each pair of cities, what is the shortest possible tour that visits each city exactly once, and returns to the starting city?

Representing an instance of the problem

The only thing that matters about cities is the distance between them. We will ignore the fully general TSP where distances can be defined in any arbitrary way and concentrate on an important special case, the Euclidean TSP, where the distance between any two cities is the Euclidean distance, the straight-line distance between points in a two-dimensional plane. So a city can be represented by a two-dimensional point: a pair of $x$ and $y$ coordinates in the Cartesian plane. Python already has the two-dimensional point as a built-in numeric data type, but in a non-obvious way: as complex numbers, which inhabit the two-dimensional (real $\times$ imaginary) plane. We will use those but you will not have to care about this aspect as all functions to handle data are made available to you.

We will work with three predetermined, historical instances, dantzig42.dat berlin52.dat bier127.dat, and with randomly generated instances. You can download the three instances from these links: dantzig42.dat, berlin52.dat, bier127.dat. In the file tsputil.py you will find the functions to read the instances from the files and to generate instances at random.
The constructor function City, creates a city object, so that City(300, 0) creates a city with x-coordinate of 300 and y-coordinate of 0. Then, distance(A, B) will be a function that uses the $x$ and $y$ coordinates to compute the distance between cities A and B.

Let's import the files:

from tsputil import *
In [1]:
import gurobipy as gp
from gurobipy import GRB
from collections import OrderedDict

%run src/tsputil.py 
%matplotlib inline

We can generate an instance of random points with the function Cities(n,seed). The function returns a frozen set because these are the input data and cannot be modified. We plot the instance with plot_situation. When you generate an instance make sure that you use a seed number different from the one of other groups working at this project.

In [2]:
ran_points = list(Cities(n=20,seed=35))
plot_situation(ran_points)

Alternatively, we can read the dantiz42.dat instance which represents locations in USA.

In [3]:
dantzig42 = read_instance("data/dantzig42.dat")
plot_situation(dantzig42)

Dantzig, Fulkerson and Johnson (DFJ) Formulation

Consider the following formulation of the symmetric traveling salesman problem due to Dantzig, Fulkerson and Johnson, 1954 (DFJ) that we have been discussing during the course. Let $V=\{0..n-1\}$ be the set of nodes and $E$ the set of edges. Let $E(S)$ be the set of edges induced by the subset of vertices $S$ and $\delta(v)$ the set of edges in the cut $(v,V\setminus\{v\})$. (We will assume that an edge between two nodes $i$ and $j$ is present in $E$ in the form $ij$ if $j>i$ and $ji$ if $ j < i $.) $$\begin{aligned} \text{(TSPIP)}\quad \min\; & \sum c_{ij} x_{ij} \\ \text{s.t.}\;&\sum_{ij \in \delta(i)} x_{ij}+\sum_{ji \in \delta(i)}x_{ji}=2 \text{ for all } i \in V\\ &\sum_{ij \in E(S)} x_{ij} \leq |S|-1 \text{ for all } \emptyset \subset S\subset V, 2 \leq |S| \leq n-1\\ &x_{ij} \in \{0,1\} \text{ for all } {ij} \in E\end{aligned}$$

We can generate all subsets of the set of 20 randomly generated cities as follows:

In [28]:
from itertools import chain, combinations
def powerset(iterable):
    "powerset([1,2,3]) --> () (1,) (2,) (3,) (1,2) (1,3) (2,3) (1,2,3)"
    s = list(iterable)
    return chain.from_iterable(combinations(s, r) for r in range(len(s)+1))

subtours = list(powerset(range(len(ran_points))))
# The first element of the list is the empty set and the last element is the full set, hence we remove them.
subtours = subtours[1:(len(subtours)-1)]

print(len(subtours))
import sys
print(sys.getsizeof(subtours)/1024/1024," MB")
1048574
8.000038146972656  MB

Task 1

Implement the DFJ formulation in the model below and solve your random instance:

In [4]:
def solve_tsp(points, subtours=[], vartype=GRB.BINARY,  silent=True):
    points=list(points)
    V = list(range(len(points)))
    E = [(i,j) for i in V for j in V if i<j] # complete graph
    
    cost = {e: distance(points[e[0]],points[e[1]]) for e in E}
    
    m = gp.Model("TSP0")
     
    ######### BEGIN: Write here your model for Task 1
    x = m.addVars(E, lb=0.0, ub=1.0, vtype=vartype)
    
    ## Objective
    m.setObjective(sum(cost[e] * x[e] for e in E), sense=GRB.MINIMIZE)
    
    ## Constraints
    flow_balance=[]
    for v in V:
        m.addConstr(sum(x[(v,i)] for i in V if (v,i) in E) + sum(x[(i,v)] for i in V if (i,v) in E) == 2)
    
    subtour_elimination=[]
    for S in subtours:
        if len(S)>1:
            subtour_elimination=m.addConstr(sum(x[(i,j)] for i in S for j in S if i<j)<=len(S)-1)
                                  
    ######### END
    m.update()
    m.display()
    m.write("tsplp.lp")
    results = m.optimize()

    if m.status == GRB.status.OPTIMAL:
        print('The optimal objective is %g' % m.objVal)
        return {e: x[e].x for e in E}
    else:
        print("Something wrong in solve_tsp")
        exit(0)

If we try to solve the small instance on 20 cities with this model, ie:

solve_tsp(ran_points, [])

we get out of memory (you can be more lucky but if so try increasing your instance with a few more cities).

solve_tsp(ran_points, subtours)
In [5]:
sol = solve_tsp(ran_points, [])
plot_situation(ran_points, sol)
Set parameter Username
Academic license - for non-commercial use only - expires 2023-02-20
Minimize
<gurobi.LinExpr: 366.2594708673074 C0 + 314.97301471713416 C1 + 394.72775428135276 C2
+ 297.1077245714086 C3 + 272.0091910211859 C4 + 305.94934221207274 C5
+ 576.7642499323272 C6 + 520.3777474104749 C7 + 320.09529830973776 C8
+ 633.2456079595025 C9 + 154.98709623707387 C10 + 299.8216136305053 C11
+ 322.02018570269786 C12 + 263.1501472543764 C13 + 527.0085388302547 C14
+ 173.31185764395926 C15 + 560.4462507680821 C16 + 216.08563117431015 C17
+ 243.00205760445732 C18 + 599.7482805310908 C19 + 186.98663053812163 C20
+ 233.13729860320507 C21 + 591.0541430359827 C22 + 566.1174789741084 C23
+ 235.54405108174564 C24 + 388.3979917558792 C25 + 477.2724588743834 C26
+ 611.9493443088243 C27 + 512.2665321880788 C28 + 666.0067567224825 C29
+ 270.667692937299 C30 + 604.8619677248686 C31 + 325.46582001801664 C32
+ 380.66389374354907 C33 + 233.94443784796422 C34 + 582.0867632922088 C35
+ 165.74679484080528 C36 + 687.8909797344344 C37 + 605.0826389841309 C38
+ 74.46475676452586 C39 + 48.25971404805462 C40 + 833.6695988219793 C41
+ 833.1416446199289 C42 + 611.9027700541974 C43 + 942.8637229207623 C44
+ 204.41379601191306 C45 + 284.56457966514387 C46 + 634.0386423554955 C47
+ 408.16663263917104 C48 + 830.9187685929353 C49 + 478.13910109925126 C50
+ 826.319550779237 C51 + 280.2159881234474 C52 + 532.024435529046 C53
+ 118.69709347747316 C54 + 659.8765035974535 C55 + 664.2476947645358 C56
+ 220.22942582679545 C57 + 201.8142710513803 C58 + 361.7084461275407 C59
+ 427.7873303406729 C60 + 549.3241301818081 C61 + 676.5330738404442 C62
+ 131.2288078129189 C63 + 571.6240022952151 C64 + 152.11837495845134 C65
+ 322.1257518423512 C66 + 190.93978108293726 C67 + 589.6617674565649 C68
+ 156.01281998605114 C69 + 568.4804306218465 C70 + 587.4725525503297 C71
+ 336.94213153002994 C72 + 231.19256043393784 C73 + 253.87004549572208 C74
+ 405.4096693469459 C75 + 448.0736546595883 C76 + 564.8734371520757 C77
+ 42.190046219457976 C78 + 453.67940222143653 C79 + 230.2628932329306 C80
+ 203.86760409638407 C81 + 309.142362027594 C82 + 478.44121896007243 C83
+ 100.12492197250393 C84 + 109.00458705944443 C85 + 820.792300158816 C86
+ 792.085853932514 C87 + 551.2349771195584 C88 + 887.4418290795178 C89
+ 139.9428454762872 C90 + 211.85844330590177 C91 + 594.0117843948889 C92
+ 334.79097956784915 C93 + 797.56441746106 C94 + 424.16977732978575 C95
+ 810.249961431656 C96 + 206.49455198624491 C97 + 504.2866248474175 C98
+ 801.1404371269747 C99 + 817.5047400474201 C100 + 614.245879107056 C101
+ 938.6463657842606 C102 + 217.7200036744442 C103 + 320.31546949843056 C104
+ 618.6242801571888 C105 + 430.80041782709543 C106 + 810.0154319517623 C107
+ 475.11051345976335 C108 + 795.4200148349298 C109 + 305.1327579923205 C110
+ 509.25926599326596 C111 + 334.21549934136806 C112 + 580.792561935843 C113
+ 575.1704095309493 C114 + 729.8465592163876 C115 + 872.6282140751581 C116
+ 351.0697936308392 C117 + 783.3677297412754 C118 + 238.01890681204299 C119
+ 536.6600413669719 C120 + 37.05401462729781 C121 + 786.1628330059874 C122
+ 338.031063661315 C123 + 341.67235767618075 C124 + 242.3984323381651 C125
+ 664.1987654309514 C126 + 758.664616283111 C127 + 199.12307751739877 C128
+ 619.697506853142 C129 + 97.16480844420988 C130 + 386.87465670420954 C131
+ 297.1683024819437 C132 + 675.8520548167328 C133 + 322.62206992082855 C134
+ 343.72227160892555 C135 + 411.3052394511891 C136 + 453.1313275420273 C137
+ 230.58620947489467 C138 + 294.0153057240388 C139 + 405.64146730826224 C140
+ 149.25146565444507 C141 + 549.050999452692 C142 + 381.87432487665365 C143
+ 315.15869018638847 C144 + 748.577985249366 C145 + 795.6437645077099 C146
+ 363.40198128243605 C147 + 631.8797353927407 C148 + 337.17947743004765 C149
+ 464.72464965826805 C150 + 538.2378656319156 C151 + 725.5901046734306 C152
+ 505.40478826382326 C153 + 165.8553586713435 C154 + 469.02665169476245 C155
+ 218.0022935659164 C156 + 678.3015553572025 C157 + 287.36040089058895 C158
+ 714.5858940673262 C159 + 104.6900186264192 C160 + 397.93592449036316 C161
+ 576.5500845546725 C162 + 169.57889019568444 C163 + 789.7854138941792 C164
+ 372.54261501202785 C165 + 853.7312223410831 C166 + 86.92525524840292 C167
+ 534.9214895664596 C168 + 455.2636598719472 C169 + 213.243991709028 C170
+ 207.06037766796427 C171 + 320.4886893480018 C172 + 491.0824777977728 C173
+ 142.00352108310554 C174 + 664.2484474953629 C175 + 249.8739682319869 C176
+ 759.2127501563708 C177 + 128.4562182223967 C178 + 451.1429928525988 C179
+ 419.1479452412954 C180 + 201.06217943710845 C181 + 704.2989422113312 C182
+ 301.3171087077533 C183 + 511.0234828263766 C184 + 288.99826989101507 C185
+ 216.31689716709604 C186 + 766.9843544688509 C187 + 318.97648816174524 C188
+ 448.2956613664692 C189>
Subject To
R0: <gurobi.LinExpr: C0 + C1 + C2 + C3 + C4 + C5 + C6 + C7 + C8 + C9 + C10 + C11 + C12
 + C13 + C14 + C15 + C16 + C17 + C18> = 2
R1: <gurobi.LinExpr: C0 + C19 + C20 + C21 + C22 + C23 + C24 + C25 + C26 + C27 + C28 +
 C29 + C30 + C31 + C32 + C33 + C34 + C35 + C36> = 2
R2: <gurobi.LinExpr: C1 + C19 + C37 + C38 + C39 + C40 + C41 + C42 + C43 + C44 + C45 +
 C46 + C47 + C48 + C49 + C50 + C51 + C52 + C53> = 2
R3: <gurobi.LinExpr: C2 + C20 + C37 + C54 + C55 + C56 + C57 + C58 + C59 + C60 + C61 +
 C62 + C63 + C64 + C65 + C66 + C67 + C68 + C69> = 2
R4: <gurobi.LinExpr: C3 + C21 + C38 + C54 + C70 + C71 + C72 + C73 + C74 + C75 + C76 +
 C77 + C78 + C79 + C80 + C81 + C82 + C83 + C84> = 2
R5: <gurobi.LinExpr: C4 + C22 + C39 + C55 + C70 + C85 + C86 + C87 + C88 + C89 + C90 +
 C91 + C92 + C93 + C94 + C95 + C96 + C97 + C98> = 2
R6: <gurobi.LinExpr: C5 + C23 + C40 + C56 + C71 + C85 + C99 + C100 + C101 + C102 + C103
 + C104 + C105 + C106 + C107 + C108 + C109 + C110 + C111> = 2
R7: <gurobi.LinExpr: C6 + C24 + C41 + C57 + C72 + C86 + C99 + C112 + C113 + C114 + C115
 + C116 + C117 + C118 + C119 + C120 + C121 + C122 + C123> = 2
R8: <gurobi.LinExpr: C7 + C25 + C42 + C58 + C73 + C87 + C100 + C112 + C124 + C125 +
 C126 + C127 + C128 + C129 + C130 + C131 + C132 + C133 + C134> = 2
R9: <gurobi.LinExpr: C8 + C26 + C43 + C59 + C74 + C88 + C101 + C113 + C124 + C135 +
 C136 + C137 + C138 + C139 + C140 + C141 + C142 + C143 + C144> = 2
R10: <gurobi.LinExpr: C9 + C27 + C44 + C60 + C75 + C89 + C102 + C114 + C125 + C135 +
 C145 + C146 + C147 + C148 + C149 + C150 + C151 + C152 + C153> = 2
R11: <gurobi.LinExpr: C10 + C28 + C45 + C61 + C76 + C90 + C103 + C115 + C126 + C136 +
 C145 + C154 + C155 + C156 + C157 + C158 + C159 + C160 + C161> = 2
R12: <gurobi.LinExpr: C11 + C29 + C46 + C62 + C77 + C91 + C104 + C116 + C127 + C137 +
 C146 + C154 + C162 + C163 + C164 + C165 + C166 + C167 + C168> = 2
R13: <gurobi.LinExpr: C12 + C30 + C47 + C63 + C78 + C92 + C105 + C117 + C128 + C138 +
 C147 + C155 + C162 + C169 + C170 + C171 + C172 + C173 + C174> = 2
R14: <gurobi.LinExpr: C13 + C31 + C48 + C64 + C79 + C93 + C106 + C118 + C129 + C139 +
 C148 + C156 + C163 + C169 + C175 + C176 + C177 + C178 + C179> = 2
R15: <gurobi.LinExpr: C14 + C32 + C49 + C65 + C80 + C94 + C107 + C119 + C130 + C140 +
 C149 + C157 + C164 + C170 + C175 + C180 + C181 + C182 + C183> = 2
R16: <gurobi.LinExpr: C15 + C33 + C50 + C66 + C81 + C95 + C108 + C120 + C131 + C141 +
 C150 + C158 + C165 + C171 + C176 + C180 + C184 + C185 + C186> = 2
R17: <gurobi.LinExpr: C16 + C34 + C51 + C67 + C82 + C96 + C109 + C121 + C132 + C142 +
 C151 + C159 + C166 + C172 + C177 + C181 + C184 + C187 + C188> = 2
R18: <gurobi.LinExpr: C17 + C35 + C52 + C68 + C83 + C97 + C110 + C122 + C133 + C143 +
 C152 + C160 + C167 + C173 + C178 + C182 + C185 + C187 + C189> = 2
R19: <gurobi.LinExpr: C18 + C36 + C53 + C69 + C84 + C98 + C111 + C123 + C134 + C144 +
 C153 + C161 + C168 + C174 + C179 + C183 + C186 + C188 + C189> = 2
Binaries
['C0', 'C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7', 'C8', 'C9', 'C10', 'C11', 'C12',
'C13', 'C14', 'C15', 'C16', 'C17', 'C18', 'C19', 'C20', 'C21', 'C22', 'C23', 'C24',
'C25', 'C26', 'C27', 'C28', 'C29', 'C30', 'C31', 'C32', 'C33', 'C34', 'C35', 'C36',
'C37', 'C38', 'C39', 'C40', 'C41', 'C42', 'C43', 'C44', 'C45', 'C46', 'C47', 'C48',
'C49', 'C50', 'C51', 'C52', 'C53', 'C54', 'C55', 'C56', 'C57', 'C58', 'C59', 'C60',
'C61', 'C62', 'C63', 'C64', 'C65', 'C66', 'C67', 'C68', 'C69', 'C70', 'C71', 'C72',
'C73', 'C74', 'C75', 'C76', 'C77', 'C78', 'C79', 'C80', 'C81', 'C82', 'C83', 'C84',
'C85', 'C86', 'C87', 'C88', 'C89', 'C90', 'C91', 'C92', 'C93', 'C94', 'C95', 'C96',
'C97', 'C98', 'C99', 'C100', 'C101', 'C102', 'C103', 'C104', 'C105', 'C106', 'C107',
'C108', 'C109', 'C110', 'C111', 'C112', 'C113', 'C114', 'C115', 'C116', 'C117', 'C118',
'C119', 'C120', 'C121', 'C122', 'C123', 'C124', 'C125', 'C126', 'C127', 'C128', 'C129',
'C130', 'C131', 'C132', 'C133', 'C134', 'C135', 'C136', 'C137', 'C138', 'C139', 'C140',
'C141', 'C142', 'C143', 'C144', 'C145', 'C146', 'C147', 'C148', 'C149', 'C150', 'C151',
'C152', 'C153', 'C154', 'C155', 'C156', 'C157', 'C158', 'C159', 'C160', 'C161', 'C162',
'C163', 'C164', 'C165', 'C166', 'C167', 'C168', 'C169', 'C170', 'C171', 'C172', 'C173',
'C174', 'C175', 'C176', 'C177', 'C178', 'C179', 'C180', 'C181', 'C182', 'C183', 'C184',
 'C185', 'C186', 'C187', 'C188', 'C189']
Gurobi Optimizer version 9.5.1 build v9.5.1rc2 (mac64[arm])
Thread count: 10 physical cores, 10 logical processors, using up to 10 threads
Optimize a model with 20 rows, 190 columns and 380 nonzeros
Model fingerprint: 0xe772cbcd
Variable types: 0 continuous, 190 integer (190 binary)
Coefficient statistics:
  Matrix range     [1e+00, 1e+00]
  Objective range  [4e+01, 9e+02]
  Bounds range     [1e+00, 1e+00]
  RHS range        [2e+00, 2e+00]
Found heuristic solution: objective 9238.7462144
Presolve time: 0.00s
Presolved: 20 rows, 190 columns, 380 nonzeros
Variable types: 0 continuous, 190 integer (190 binary)

Root relaxation: objective 2.853284e+03, 27 iterations, 0.00 seconds (0.00 work units)

    Nodes    |    Current Node    |     Objective Bounds      |     Work
 Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time

     0     0 2853.28422    0    6 9238.74621 2853.28422  69.1%     -    0s
H    0     0                    3258.3766402 2853.28422  12.4%     -    0s
H    0     0                    3239.8002996 2853.62505  11.9%     -    0s
     0     0 2909.32766    0   10 3239.80030 2909.32766  10.2%     -    0s
     0     0 2909.32766    0    6 3239.80030 2909.32766  10.2%     -    0s
H    0     0                    3238.3339722 2909.32766  10.2%     -    0s
     0     0 2909.32766    0   10 3238.33397 2909.32766  10.2%     -    0s
H    0     0                    2980.6834244 2909.32766  2.39%     -    0s
H    0     0                    2944.0600831 2909.32766  1.18%     -    0s

Cutting planes:
  Gomory: 2
  Zero half: 4

Explored 1 nodes (62 simplex iterations) in 0.01 seconds (0.00 work units)
Thread count was 10 (of 10 available processors)

Solution count 7: 2944.06 2980.68 3238.33 ... 9238.75

Optimal solution found (tolerance 1.00e-04)
Best objective 2.944060083052e+03, best bound 2.944060083052e+03, gap 0.0000%
The optimal objective is 2944.06

A Cutting Plane Approach

The number of subtour elimination constraints in the DFJ formulation can be huge ($2^n$) and even though we can remove half of those due to symmetries, there are still exponentially many. A way to deal with large families of constraints is to introduce them only when needed. This idea is similar to the cutting plane algorithm that we saw in the intro classes. We start by solving the relaxed version of (TSPIP) obtained by removing the integrality constraints and the exponentially many subtour elimination constraints. We dub this relaxation TSPLP$_0$. Let $x^*$ be the optimal solution of TSPLP$_0$.

Task 2

Implement the new function solve_tsp(points, subtours=[]) that solves the relaxed version TSPLP$_0$ and solve your random instance with 20 points. Inspect the plot of the solution obtained.

tsplp0 = solve_tsp(ran_points, subtours=[], vartype=po.NonNegativeReals)
plot_situation(ran_points, tsplp0)

Describe what you observe. Are all variables integer? Is the matrix TUM? Why? Is the solution a tour? Is the solution matching your expectations?

Relaxing only the subtour elimination constraints in solve_tsp we obtain:

In [ ]:
tsplp0 = solve_tsp(ran_points, subtours=[], solver="gurobi", vartype=po.Binary, silent=False)
plot_situation(ran_points, tsplp0)
Using license file /Library/gurobi900/gurobi.lic
Academic license - for non-commercial use only
Read LP format model from file /var/folders/w9/sxcp2ljj4wq3fdhy78rf5djh0000gn/T/tmpdqgl6hq3.pyomo.lp
Reading time = 0.02 seconds
x191: 21 rows, 191 columns, 381 nonzeros
Gurobi Optimizer version 9.0.0 build v9.0.0rc2 (mac64)
Optimize a model with 21 rows, 191 columns and 381 nonzeros
Model fingerprint: 0xdc922c71
Variable types: 1 continuous, 190 integer (190 binary)
Coefficient statistics:
  Matrix range     [1e+00, 1e+00]
  Objective range  [4e+01, 9e+02]
  Bounds range     [1e+00, 1e+00]
  RHS range        [1e+00, 2e+00]
Found heuristic solution: objective 8528.3355431
Presolve removed 1 rows and 1 columns
Presolve time: 0.03s
Presolved: 20 rows, 190 columns, 380 nonzeros
Variable types: 0 continuous, 190 integer (190 binary)

Root relaxation: objective 2.853284e+03, 27 iterations, 0.00 seconds

    Nodes    |    Current Node    |     Objective Bounds      |     Work
 Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time

     0     0 2853.28422    0    6 8528.33554 2853.28422  66.5%     -    0s
H    0     0                    3487.9039484 2853.28422  18.2%     -    0s
     0     0 2899.93831    0    8 3487.90395 2899.93831  16.9%     -    0s
H    0     0                    3073.9916054 2899.93831  5.66%     -    0s
     0     0 2925.99947    0    6 3073.99161 2925.99947  4.81%     -    0s
     0     0 2925.99947    0    6 3073.99161 2925.99947  4.81%     -    0s
H    0     0                    2944.0600831 2925.99947  0.61%     -    0s

Cutting planes:
  Gomory: 2
  Clique: 1
  MIR: 1
  Zero half: 2

Explored 1 nodes (64 simplex iterations) in 0.13 seconds
Thread count was 4 (of 4 available processors)

Solution count 4: 2944.06 3073.99 3487.9 8528.34 

Optimal solution found (tolerance 1.00e-04)
Best objective 2.944060083052e+03, best bound 2.944060083052e+03, gap 0.0000%
The optimal objective is 2944.06
In [ ]:
tsplp0 = solve_tsp(ran_points, [[2,7,9],[18,8,12]], po.Binary, solver="glpk", silent=True)
plot_situation(ran_points, tsplp0)
The optimal objective is 3045.72
In [ ]:
tsplp0 = solve_tsp(ran_points, [[2,7,9],[18,8,12],[2,7,9,17],[0,13,5],[1,14,11],[6,3,10],[2,7,9,17,13,5,0],[2,7,9,0,13,5,4,17]], po.Binary, solver="glpk", silent=True)
plot_situation(ran_points, tsplp0)
The optimal objective is 3165.93

Relaxing also the integrality constraints `vartype=po.NonNegativeReals` is more convenient since the solution will be faster:

In [ ]:
tsplp0 = solve_tsp(ran_points, [], vartype=po.NonNegativeReals, silent=False)
plot_situation(ran_points, tsplp0)
GLPSOL: GLPK LP/MIP Solver, v4.65
Parameter(s) specified in the command line:
 --write /var/folders/w9/sxcp2ljj4wq3fdhy78rf5djh0000gn/T/tmpawunl5fv.glpk.raw
 --wglp /var/folders/w9/sxcp2ljj4wq3fdhy78rf5djh0000gn/T/tmpg8ai8bnc.glpk.glp
 --cpxlp /var/folders/w9/sxcp2ljj4wq3fdhy78rf5djh0000gn/T/tmpj_728qez.pyomo.lp
Reading problem data from '/var/folders/w9/sxcp2ljj4wq3fdhy78rf5djh0000gn/T/tmpj_728qez.pyomo.lp'...
21 rows, 191 columns, 381 non-zeros
832 lines were read
Writing problem data to '/var/folders/w9/sxcp2ljj4wq3fdhy78rf5djh0000gn/T/tmpg8ai8bnc.glpk.glp'...
997 lines were written
GLPK Simplex Optimizer, v4.65
21 rows, 191 columns, 381 non-zeros
Preprocessing...
20 rows, 190 columns, 380 non-zeros
Scaling...
 A: min|aij| =  1.000e+00  max|aij| =  1.000e+00  ratio =  1.000e+00
Problem data seem to be well scaled
Constructing initial basis...
Size of triangular part is 19
      0: obj =   9.320749676e+03 inf =   5.500e+01 (19)
     23: obj =   1.013873908e+04 inf =   0.000e+00 (0)
*    87: obj =   2.853284219e+03 inf =   0.000e+00 (0)
OPTIMAL LP SOLUTION FOUND
Time used:   0.0 secs
Memory used: 0.2 Mb (169736 bytes)
Writing basic solution to '/var/folders/w9/sxcp2ljj4wq3fdhy78rf5djh0000gn/T/tmpawunl5fv.glpk.raw'...
221 lines were written
The optimal objective is 2853.28

You should pay attention to the two different outputs of the solver. In particular, in the latter output with continuous variables you find the log of the iterations of the primal simplex, while in the output of the model with interger variables you find the log of the branch and bound process in which the 48 iterations of the LP relaxation at the root is summarized in one single row. In the linear relaxation the variables are not all integer. The matrix is indeed not TUM because although we have only two terms different from zero in each column of the matrix, they are both positive and the graph is not bipartite, hence we are not able to separate the rows in the fashion described by the theorem on sufficient conditions for TUM. The solution is not a tour, since, as expected, there are subtours and fractional variables.

An alternative directed graph formulation. Note that if we had used a directed representation of the problem, by introducing for each edge two oriented arcs then we would have had a TUM matrix, although the number of arcs would have been now double. Let $A=\{ij\mid i\in V,j\in V,i\neq j\}$ and let $\delta^+(i)$ and $\delta^-(i)$ the set of arcs, respectively, outgoing and ingoing $i$: $$\begin{aligned} \text{(TSPIP)}\quad \min\; & \sum_{ij \in A} c_{ij} x_{ij} \\ \text{s.t.}\;&\sum_{ij \in \delta^+(i)} x_{ij}=1 \text{ for all } i \in V\\ &\sum_{ji \in \delta^-(i)} x_{ji}=1 \text{ for all } i \in V\\ &\sum_{ij \in A(S)} x_{ij} \leq |S|-1 \text{ for all } \emptyset \subset S\subset V, 2 \leq |S| \leq n-1\\ &x_{ij} \in \{0,1\} \text{ for all } {ij} \in E \end{aligned}$$

In [ ]:
def solve_tsp_directed(points, subtours=[], vartype=po.Binary, solver="glpk", silent=True):
    points=list(points)
    V = range(len(points))
    A = [(i,j) for i in V for j in V if i!=j] # complete graph
    cost = {a:distance(points[a[0]],points[a[1]]) for a in A}
    m = po.ConcreteModel("TSP0")
    
    ######### BEGIN: Write here your model for Task 1
    m.x=po.Var(A, bounds=(0.0, 1.0), within=vartype)

    m.value = po.Objective(expr=sum(cost[a]*m.x[a] for a in A), sense=po.minimize)
    ## Constraints
    m.mass_balance = po.ConstraintList()
    for v in V:
        m.mass_balance.add(sum(m.x[i,v] for i,j in A if j==v) == 1) # incoming_flow_balance
        m.mass_balance.add(sum(m.x[v,i] for j,i in A if j==v) == 1) # outgoing_flow_balance
    
    m.subtour_elimination = po.ConstraintList()
    for S in subtours:
        m.subtour_elimination.add(sum(m.x[(i,j)] for i in S for j in S if i!=j)<=len(S)-1)
    ######### END
    
    results = po.SolverFactory(solver).solve(m, tee=(not silent))

    if str(results.Solver.status) == 'ok':
        print('Solve TSP: the optimal objective value is %g' % po.value(m.value))   
        return {a : m.x[a]() for a in A}
    else:
        print("Something wrong in solve_tsplp")
        exit(0)
In [ ]:
tsplp0 = solve_tsp_directed(ran_points, [], po.NonNegativeReals, "glpk", False)
plot_situation(ran_points, tsplp0)
GLPSOL: GLPK LP/MIP Solver, v4.65
Parameter(s) specified in the command line:
 --write /var/folders/w9/sxcp2ljj4wq3fdhy78rf5djh0000gn/T/tmp0q0pa373.glpk.raw
 --wglp /var/folders/w9/sxcp2ljj4wq3fdhy78rf5djh0000gn/T/tmp98m_u1pr.glpk.glp
 --cpxlp /var/folders/w9/sxcp2ljj4wq3fdhy78rf5djh0000gn/T/tmpv_njerw0.pyomo.lp
Reading problem data from '/var/folders/w9/sxcp2ljj4wq3fdhy78rf5djh0000gn/T/tmpv_njerw0.pyomo.lp'...
41 rows, 381 columns, 761 non-zeros
1652 lines were read
Writing problem data to '/var/folders/w9/sxcp2ljj4wq3fdhy78rf5djh0000gn/T/tmp98m_u1pr.glpk.glp'...
1987 lines were written
GLPK Simplex Optimizer, v4.65
41 rows, 381 columns, 761 non-zeros
Preprocessing...
40 rows, 380 columns, 760 non-zeros
Scaling...
 A: min|aij| =  1.000e+00  max|aij| =  1.000e+00  ratio =  1.000e+00
Problem data seem to be well scaled
Constructing initial basis...
Size of triangular part is 39
      0: obj =   9.499897478e+03 inf =   1.800e+01 (3)
     41: obj =   8.670322722e+03 inf =   0.000e+00 (0)
*   134: obj =   2.504181871e+03 inf =   0.000e+00 (0)
OPTIMAL LP SOLUTION FOUND
Time used:   0.0 secs
Memory used: 0.3 Mb (310320 bytes)
Writing basic solution to '/var/folders/w9/sxcp2ljj4wq3fdhy78rf5djh0000gn/T/tmp0q0pa373.glpk.raw'...
431 lines were written
Solve TSP: the optimal objective value is 2504.18

As we see, an integer solution is found without need of a branch and bound (see the output of the solver). However, a side effect of considering a directed graph formulation is the fact that subtours of lenght 2 are not eliminated and we actually get 4 of them in the case above.

Task 3

Detect by visual inspection some subtour inequalities to add to TSPLP0. List those subtours in the subtour argument of the function solve_tsp and solve the new problem TSPLP$_1$=TSPLP$_0 \cup c$, where $c$ is the constraint added. Report the visualization produced by plot_situation on the new solution. Is it a tour? If not iterate this procedure until you cannot find anymore subtours. Show your last solution and comment on it.

Solution

</span>

In [ ]:
tsplp0 = solve_tsp(ran_points, [], po.Binary)
plot_situation(ran_points, tsplp0)
The optimal objective is 2944.06
In [ ]:
tsplp1 = solve_tsp(ran_points, [[2,7,9], [0,13,17,4,19,15,5], [12,8,18], [3,6,16,14,1,11,10]], po.Binary)
plot_situation(ran_points, tsplp1)
The optimal objective is 3045.72
In [ ]:
tsplp1 = solve_tsp(ran_points, [[2,7,9], [0,13,17,4,19,15,5], [12,8,18], [3,6,16,14,1,11,10], 
                                [3,6,16,4,19,15,12,8,18,10],[2,7,9,17],[0,13,5],[11,1,14]], po.Binary)
plot_situation(ran_points, tsplp1)
The optimal objective is 3136.08
In [ ]:
tsplp1 = solve_tsp(ran_points, [[2,7,9], [0,13,5], [3,6,16,14,1,11,10], 
                                [3,6,16,14,1,11,18,10],[2,7,9,17],
                                [6,16,14,1,12,8,18,11,10,3],[2,7,9,0,13,17],
                               [2,7,9,0,5,13,17],[11,14,1],[4,19,15,12,8,18,10,3,6,16]], po.Binary)
plot_situation(ran_points, tsplp1)
The optimal objective is 3149.14
In [ ]:
tsplp1 = solve_tsp(ran_points, [[2,7,9], [0,13,5], [3,6,16,14,1,11,10], 
                                [3,6,16,14,1,11,18,10],[2,7,9,17],
                                [6,16,14,1,12,8,18,11,10,3],[2,7,9,0,13,17],
                               [2,7,9,0,5,13,17],[11,14,1],[4,19,15,12,8,18,10,3,6,16],
                               [2,7,9,0,13,5,4,17],[19,15,12,8,18,11,10,3,6,16,14,1]], po.Binary)
plot_situation(ran_points, tsplp1)
The optimal objective is 3180.29
In [ ]:
tsplp1 = solve_tsp(ran_points, [[2,7,9], [0,13,5], [3,6,16,14,1,11,10], 
                                [3,6,16,14,1,11,18,10],[2,7,9,17],
                                [6,16,14,1,12,8,18,11,10,3],[2,7,9,0,13,17],
                               [2,7,9,0,5,13,17],[11,14,1],[4,19,15,12,8,18,10,3,6,16],
                               [2,7,9,0,13,5,4,17],[19,15,12,8,18,11,10,3,6,16,14,1],
                               [6,3,10]], po.Binary)
plot_situation(ran_points, tsplp1)
The optimal objective is 3182.28

We might finally find a tour continuing in this way and that would be an optimal solution since it would satisfy all DFJ constraints. However, in the general case we could have no subtour constraints unsatisfied but a solution not yet integer. This is for example the case for the instance `dantzig42` as we will see below.

Task 4

We can automatize the manual process described above. Let $V'=V\setminus\{0\}$ and $E'=E\setminus\{\delta(0)\}$. A set $S\subseteq V'$ that contains a vertex $k$ forms a subtour in $x^*$ if $$\sum_{e=ij \in E'(S)} x_e^* > |S \setminus \{k\} |=|S|-1.$$

We can find the subtour elimination constraint that is most violated by $x^*$ by solving the following separation problem for $k=\{1..n-1\}$. Let’s represent the unknown set $S \subseteq V'$ by binary variables $z$ with $z_i=1$ if $i \in S$ then the separation problem for $k=\{1..n-1\}$ is

$$ (SEP) \quad \xi_k=\max \left\{ \sum_{e=ij\in E':i < j} x^*_e z_i z_j - \sum_{i \in V' \setminus \{k\}} z_i : z \in \mathbb{B}^n, z_k=1\right\} $$

If $ \xi_k > 0 $ for some $k$ then the solution $z^*$ of SEP gives a subtour $S$, containing $k$, whose corresponding subtour elimination constraint in TSPIP is violated by $x^*$. We insert this constraint in TSPLP$_0$ and solve the new problem TSPLP$_1$.

Rewrite the (SEP) problem for some $k$ as an integer linear program and report the mathematical model in your answers.

Solution

We introduce the variables $y_{e}$ with $y_{e}=1$ if $z_i=z_j=1$ for $e=ij \in E'$ with $i<j$, and we obtain the integer program

$$\begin{aligned} \text{(SEPILP)}\quad \xi_k=\max\; & \sum_{e \in E'} x_e^* y_e-\sum_{i \in V'\setminus\{k\}} z_i \\ \label{sep1}\text{s.t.}\;&y_e \leq z_i \text{ for } e=ij \in E'\\ \label{sep2}&y_e \leq z_j \text{ for } e=ij \in E'\\ \label{sep3}&y_e \geq z_i+z_j-1 \text{ for } e=ij \in E'\\ &z_k=1\\ &y \in \{0,1\}^{|E'|}, z\in \{0,1\}^{|V'|}\end{aligned}$$

End Solution </span>

Task 5

Implement your model in the function solve_separation(points, x_star, k) below and solve the LP relaxation of the (SEP) problem to separate the optimal solution $x^*$ of TSPLP$_0$.

In [ ]:
def solve_separation(points, x_star, k, vartype=po.Reals, solver="glpk", silent=True):
    points = list(points)
    V = list(range(len(points)))
    Vprime = list(range(1, len(points)))
    E = [(i, j) for i in V for j in V if i < j]
    Eprime = [(i, j) for i in Vprime for j in Vprime if i < j]

    m = po.ConcreteModel("SEP")
    #m.setParam("limits/memory", 32000)
    #m.setParam("limits/time", 10)  # maximal time in seconds to run

    # BEGIN: Write here your model for Task 5
    m.y = po.Var(Eprime, bounds=(0.0, 1.0), within=vartype)
    m.z = po.Var(Vprime, bounds=(0.0,1.0), within=vartype)

    m.value = po.Objective(expr=sum(x_star[e]*m.y[e] for e in Eprime)-sum(m.z[v] for v in Vprime if v!=k), sense=po.maximize)

    m.k = po.Constraint(expr=m.z[k]==1)

    m.y_constraints = po.ConstraintList()
    for i, j in Eprime:
        m.y_constraints.add(m.y[i, j] <= m.z[i])
        m.y_constraints.add(m.y[i, j] <= m.z[j])
        m.y_constraints.add(m.y[i, j] >= m.z[i]+m.z[j]-1)

    # END
    results = po.SolverFactory(solver).solve(m, tee=(not silent))
    
    if str(results.Solver.status) == 'ok':
        print(('Separation problem solved for k=%d, solution value %g' % (k, m.value())))
        # BEGIN: Write here the subtour found from the solution of the model
        # it must be a list of points
        subtour = []
        subtour = list(filter(lambda i: m.z[i]() >= 0.99, Vprime))
        # END
        return m.value(), subtour
    else:
        print("Something wrong in solve_separation")
        exit(0)

Try your implementation of solve_separation on your previous TSPLP$_0$ solution. Do you get the expected answer?

Solution

In [ ]:
tsplp0 = solve_tsp(ran_points, [], po.Reals)
plot_situation(ran_points, tsplp0)
S_star = solve_separation(ran_points, tsplp0, 12, vartype=po.Reals)
print(S_star)
The optimal objective is 2853.28
Separation problem solved for k=12, solution value 0.5
(0.5, [1, 3, 6, 8, 10, 11, 12, 14, 16, 18])

This means that by solving for the vertex k=12 we find the set S that contains 12 and violates the most the subtour elimination constraint. We could now solve for all other k's or already insert the elimination constraint for the found subtour.

Task 6

The following procedure cutting_plane_alg implements the cutting plane algorithm that uses your implementation of solve_tsp and solve_separation. Finish the implementation of the function. That is: add the condition for the presence of violated inequalities.

Solution

In [ ]:
def cutting_plane_alg(points):
    Vprime = list(range(1, len(points)))
    subtours = []
    lpsol = []
    found = True
    while found:
        lpsol = solve_tsp(points, subtours, vartype=po.Reals)
        plot_situation(points, lpsol)
        found = False
        tmp_subtours = []
        best_val = float('-inf')
        for k in Vprime:
            value, subtour = solve_separation(points, lpsol, k, vartype=po.Reals, silent=True)
            best_val = value if value > best_val else best_val
            # BEGIN: write here the condition. Include a tollerance
            if value > 0.0001:
                # END
                found = True
                tmp_subtours += [subtour]
                subtours += tmp_subtours
        print('*'*60)
        print("********** Subtours found: ", tmp_subtours, " with best value : ", best_val)
        print('*'*60)
    plot_situation(points, lpsol)
    return lpsol

We added the condition `value>0`.

Run the cutting plane algorithm thus implemented until termination. Report the plot of the last solution found by solving the TSPLP$_n$ problem, where $n$ is the number of iterations in which violated inequalities were found. Is the solution optimal?

Solution

In [ ]:
sol = cutting_plane_alg(ran_points)
The optimal objective is 2853.28
Separation problem solved for k=1, solution value 0.5
Separation problem solved for k=2, solution value 1
Separation problem solved for k=3, solution value 0.5
Separation problem solved for k=4, solution value 0.5
Separation problem solved for k=5, solution value 0
Separation problem solved for k=6, solution value 0.5
Separation problem solved for k=7, solution value 1
Separation problem solved for k=8, solution value 0.5
Separation problem solved for k=9, solution value 1
Separation problem solved for k=10, solution value 0.5
Separation problem solved for k=11, solution value 0.5
Separation problem solved for k=12, solution value 0.5
Separation problem solved for k=13, solution value 0
Separation problem solved for k=14, solution value 0.5
Separation problem solved for k=15, solution value 0.5
Separation problem solved for k=16, solution value 0.5
Separation problem solved for k=17, solution value 0.5
Separation problem solved for k=18, solution value 0.5
Separation problem solved for k=19, solution value 0.5
************************************************************
********** Subtours found:  [[1, 3, 6, 10, 11, 14, 16], [2, 7, 9], [1, 3, 6, 10, 11, 14, 16], [1, 3, 4, 6, 8, 10, 11, 12, 14, 15, 16, 18, 19], [1, 3, 6, 10, 11, 14, 16], [2, 7, 9], [1, 3, 6, 8, 10, 11, 14, 16, 18], [2, 7, 9], [1, 3, 6, 10, 11, 14, 16], [1, 3, 6, 10, 11, 14, 16], [1, 3, 6, 8, 10, 11, 12, 14, 16, 18], [1, 3, 6, 10, 11, 14, 16], [1, 3, 6, 8, 10, 11, 12, 14, 15, 16, 18], [1, 3, 6, 10, 11, 14, 16], [1, 3, 4, 6, 8, 10, 11, 12, 14, 15, 16, 17, 18, 19], [1, 3, 6, 10, 11, 14, 16, 18], [1, 3, 6, 8, 10, 11, 12, 14, 15, 16, 18, 19]]  with best value :  1.0
************************************************************
The optimal objective is 3179.61
Separation problem solved for k=1, solution value 1
Separation problem solved for k=2, solution value 0
Separation problem solved for k=3, solution value 0
Separation problem solved for k=4, solution value 0
Separation problem solved for k=5, solution value 0
Separation problem solved for k=6, solution value 0
Separation problem solved for k=7, solution value 0
Separation problem solved for k=8, solution value 0
Separation problem solved for k=9, solution value 0
Separation problem solved for k=10, solution value 0
Separation problem solved for k=11, solution value 1
Separation problem solved for k=12, solution value 0
Separation problem solved for k=13, solution value 0
Separation problem solved for k=14, solution value 1
Separation problem solved for k=15, solution value 0
Separation problem solved for k=16, solution value 0
Separation problem solved for k=17, solution value 0
Separation problem solved for k=18, solution value 0
Separation problem solved for k=19, solution value 0
************************************************************
********** Subtours found:  [[1, 11, 14], [1, 11, 14], [1, 11, 14]]  with best value :  0.999999999999996
************************************************************
The optimal objective is 3180.29
Separation problem solved for k=1, solution value 0
Separation problem solved for k=2, solution value 0
Separation problem solved for k=3, solution value 1
Separation problem solved for k=4, solution value 0
Separation problem solved for k=5, solution value 0
Separation problem solved for k=6, solution value 1
Separation problem solved for k=7, solution value 0
Separation problem solved for k=8, solution value 0
Separation problem solved for k=9, solution value 0
Separation problem solved for k=10, solution value 1
Separation problem solved for k=11, solution value 0
Separation problem solved for k=12, solution value 0
Separation problem solved for k=13, solution value 0
Separation problem solved for k=14, solution value 0
Separation problem solved for k=15, solution value 0
Separation problem solved for k=16, solution value 0
Separation problem solved for k=17, solution value 0
Separation problem solved for k=18, solution value 0
Separation problem solved for k=19, solution value 0
************************************************************
********** Subtours found:  [[3, 6, 10], [3, 6, 10], [3, 6, 10]]  with best value :  1.0
************************************************************
The optimal objective is 3182.28
Separation problem solved for k=1, solution value 0
Separation problem solved for k=2, solution value 0
Separation problem solved for k=3, solution value 0
Separation problem solved for k=4, solution value 0
Separation problem solved for k=5, solution value 0
Separation problem solved for k=6, solution value 0
Separation problem solved for k=7, solution value 0
Separation problem solved for k=8, solution value 0
Separation problem solved for k=9, solution value 0
Separation problem solved for k=10, solution value 0
Separation problem solved for k=11, solution value 0
Separation problem solved for k=12, solution value 0
Separation problem solved for k=13, solution value 0
Separation problem solved for k=14, solution value 0
Separation problem solved for k=15, solution value 0
Separation problem solved for k=16, solution value 0
Separation problem solved for k=17, solution value 0
Separation problem solved for k=18, solution value 0
Separation problem solved for k=19, solution value 0
************************************************************
********** Subtours found:  []  with best value :  0.0
************************************************************

We find the same optimal solution as eariler. In the `cutting_plane` function above we are reconstructing the model from scrtach everytime. This can be improved by working incrementally and letting the solver restart from the previously solution found. In the next task, you will find a link to a more efficient implementation.

End Solution

Task 7 (Optional)

Reflecting upon the process implemented you may wonder whether we are gaining anything in efficiency. After all we circumvented having to solve the original integer programming formulation of the TSP by having to solve many separation problems, which are in principle all integer programming problems and hence possibly hard to solve. It turns out that the SEP problems are actually solvable efficiently. In task 5, you were given the tip to implement the LP relaxation of the SEP problem. Why are the solutions to SEP always integer? It turns out that the SEP corresponds to one problem treated during the course. Which one? In efficient implementations the SEP problem is solved by specialized algorithms for this problem. [Hint: It may help to consider the other way to formulate subtour elimination constraints, that is, cut-set constraints.]

Solution

Solutions to the LP relaxation of (SEP) are always integer because the matrix is TUM. (Since $x^*\geq 0$ then we wish $y_e$ as large as possible. Thus by: $y_e=\min(z_i,z_j)$ and $\min(z_i,z_j)\geq z_i+z_j-1$ is always satisfied and can be dropped. We are left with a matrix with a block that contains only two consecutive ones on each column (for the variables $y$) concatenated with two identity matrices (for the variables $z$). Such a matrix can be shown to be TUM (consecutive ones can be rearranged and a partition satisfying the sufficiency conditions found + concatation with identity matrices preserve TUM properties).

The separation problem can be solved by running a max flow algorithm from any vertex $k$ as source to any other vertex as target and $x^*$ as capacity on the arcs oriented from source to tank. This can be better seen from the dual of the (SEP) problem that gives an arc-incidence matrix. Another way to see this is the following. Consider the alternative cut set constraints in place of the subtour elimination constraints : $$\sum_{e\in \delta(S)} x_e\geq 2, \forall \emptyset \subset S \subset V$$ which imposes that every solution must cross every demarcation line separating the set of cities into two nonempty parts at least twice. Let $N_{kt}(x)$ be the network with arcs obtained from $E$ oriented from a source vertex $k\in V$ to an arbitrary fixed tank vertex $t\in V$ and capacity $x^*$ on these arcs. Any nonempty proper subset $S$ of $V$ that generates a cut $N_{kt}(x)$ with $k$ in $S$ and $t$ in $V\setminus S$ with capacity less than 2 corresponds to a set $S$ violating the cut separation constraint. One such set, if it exists, can be found by solving a global minimum cut problem in $N_{kt}(x)$ from any vertex $k$ to an arbitrary fixed vertex $t$ (the choice does not matter). If we use the push-relabel algorithm the complexity of each single max flow-min cut problem is $O(n^2m)$ and since we need to solve it $n-1$ times for all $k$ the total complexity of the separation problem becomes $O(n^4m)$. The procedure is implemented in Concorde in a very efficient way @AppBixChvCoo06 [page 170].

The problem to solve is the global min cut problem, which can be solved solving $n-1$ min cut problems. The MIP formulation of the min cut problem is the dual the maximum flow problem. Hence for $k=\{2..n\}$ and an arbitrarily chosen $t$: $$\begin{aligned} \min\;& \alpha_{ij}x^*_{ij}\\ &u_i-u_j+\alpha_{ij}\geq 0 \quad \forall ij \in E'\\ &u_k-u_t\geq 1\\ &u_k=1\\ &u_i\in \{0,1\}\quad \forall i \in V'\\ &\alpha_{ij}\geq 0 \quad \forall ij \in E'\\\end{aligned}$$

End Solution

Task 8 (Optional)

In task 6, you run the cutting plane algorithm until no cutting plane can be found anymore. Is this cutting plane algorithm enough to solve your instance to optimality? What about the general case on any arbitrary instance? Write your considerations and conclusions. [Hint: try the whole procedure on the instances dantzig42.dat and berlin52.dat to see what happens in the general case. Report the final plots on these instance.]

Solution

As argued before, the solution found to the random instance is optimal.

For the instance with 20 vertices it would have been possible to generate all cuts at the beginning and solve the IP once. For the instance berlin52.dat, the number of constraints would be simply too large to enumerate. We need therefore to use the dynamic procedure illustrated in this assignment. If we apply the procedure to the berlin52.dat example we finish again with an integer solution that is a tour and hence it is optimal. For the instance dantzig42 however the last solution, TSPLP$_5$ has no subtour elimination constraint to add, that is $\xi\leq 0$ for all $k$. Nevertheless the solution is not integer. See Figure. This is due to the fact that the formulation of the TSP given at the beginning of Task 2 is not tight, that is, it is not the description of the convex hull. We do not know the formulation of the convex hull for the TSP problem. What one could do is to proceed by applying further tighter constraints such as the comb constraints. Alternatively one could reinsert the integrality constraints in the problem TSP$_5$ and continue the cutting plane procedure with branch and bound from there.

In [ ]:
cutting_plane_alg(dantzig42,True)

Task 9

Provide the length of the optimal tour of your problem on 20 nodes.

See above.

Task 10

Already an instance on 127 vertices is challenging for our implementation. Try your cutting plane procedure on the instance bier127.dat, which asks to find the shortest tour among the 127 biergardens in Augsburg, Bavaria. You will have to comment the line for plotting at each iteration, since it is inefficient. Moreover it might be convenient not to wait for all $k$s to be evaluated but to insert a cut as soon as one is found.

Task 11

Experiment ideas:

  • combine the DFJ cutting plane procedure with integrality constraints
  • solve the separation problems in a more efficient way, for example, with network flow algorithms

Keeping the integrality constraints in the DFJ formulation but relaxaing the subtour elimination conditions we can solve the separation problem with an algorithm that determines the connected components of a graph, if we have more than one connected component, each connected components is a subtour to eliminate. Connected components can be found by breath first.

An implementation of this procedure using the module networkx can be found in the repository: tsp_cc.py

An implementation of this approach in Gurobi is available as one of the exercises in the Documentation. The example uses callback functions.

Relaxing the integrality constraints in the DFJ formulation we can solve the separation problem with a max flow - min cut algorithm, as explained above. An implementation using networkx is available in the repository: tsp_maxflow.py.

To Do: time comparison