In [1]:
import JuMP as J
import SCIP
import GLPK
import Plots
import Combinatorics
import Random
In [2]:
function tsp_mtz(n, dists)
    m = J.Model(GLPK.Optimizer)
    
    # variable: connection i->j is used.
    J.@variable(m, tour[i=1:n, j=1:n], Bin)
    # variable: sequence number of visited nodes
    J.@variable(m, sequence[i=1:n] >= 0, Int)
        
    # facility 1 is the start.
    J.fix(sequence[1], 1, force=true)

    # Miller/Tucker/Zemlin (1960)
    
    J.@objective(m, Min, sum(tour[i,j] * dists[i,j] for i = 1:n for j = 1:n))
    
    # no self-visiting.
    J.@constraint(m, facility_no_self[i=1:n], tour[i,i] == 0)
    # for each facility: exactly one incoming edge
    J.@constraint(m, facility_incoming[i=1:n], sum(tour[:, i]) == 1)
    # for each facility: exactly one outgoing edge
    J.@constraint(m, facility_outgoing[i=1:n], sum(tour[i, :]) == 1)

    # reformulated:
    J.@constraint(m, monoton[i=1:n, j=2:n], sequence[i] - sequence[j] + n * tour[i,j] <= n-1)

    J.optimize!(m)
    
    J.value.(tour), J.objective_value(m)
end
Out[2]:
tsp_mtz (generic function with 1 method)
In [3]:
function tsp_dfj_subsets(n, dists)
    m = J.Model(GLPK.Optimizer)
    
    # variable: connection i->j is used.
    J.@variable(m, tour[i=1:n, j=1:n], Bin)
    
    J.@objective(m, Min, sum(tour[i,j] * dists[i,j] for i = 1:n for j = 1:n))
    
    # no self-visiting.
    J.@constraint(m, facility_no_self[i=1:n], tour[i,i] == 0)
    # for each facility: exactly one incoming edge
    J.@constraint(m, facility_incoming[i=1:n], sum(tour[:, i]) == 1)
    # for each facility: exactly one outgoing edge
    J.@constraint(m, facility_outgoing[i=1:n], sum(tour[i, :]) == 1)

    vertices = collect(1:n)
    for ps in Combinatorics.powerset(vertices)
        if length(ps) < n && length(ps) > 0
           J.@constraint(m, sum(tour[i,j] for i in ps for j in ps if i != j) <= length(ps) - 1) 
        end
    end

    J.optimize!(m)
    
    J.value.(tour), J.objective_value(m)
end
Out[3]:
tsp_dfj_subsets (generic function with 1 method)
In [4]:
function tsp_dfj_short(n, dists)
    m = J.Model(GLPK.Optimizer)
    
    # variable: connection i->j is used.
    J.@variable(m, tour[i=1:n, j=i:n], Bin)
    
    J.@objective(m, Min, sum(tour[i,j] * dists[i,j] for i = 1:n for j = i:n))
    
    J.@constraint(m, enterexit[i=1:n], sum(tour[k, l] for k in 1:n for l in 1:n if (i == k || i == l) && k < l) == 2)
    
    for ps in Combinatorics.powerset(collect(1:n), 2)
        if length(ps) == n
            continue
        end
        J.@constraint(m, sum(tour[i,j] for i in 1:n for j in 1:n if (in(i, ps) + in(j, ps)) == 1 && i < j) >= 2)
    end
    
    J.optimize!(m)
    
    J.value.(tour), J.objective_value(m)
end
Out[4]:
tsp_dfj_short (generic function with 1 method)
In [5]:
function run(n, solverfunc; asym=false)
    Random.seed!(1)
    facilities = rand(n, 2) .* 50

    dists = zeros(n, n)
    for i = 1:n
        for j = 1:n
            dists[i,j] = sqrt(sum((facilities[i, :] .- facilities[j, :]).^2))
        end
    end

    @time tour, obj = solverfunc(n, dists[1:n, 1:n])
    println("n = $n, obj = $obj")

    Plots.scatter(facilities[:,1], facilities[:,2], legend=false)
    for i in 1:n
        for j in 1:n
            if (asym || i < j) && tour[i,j] > 0
                Plots.plot!([facilities[i,1],facilities[j,1]], [facilities[i,2],facilities[j,2]],
                    color=:red, arrow=true)
            end
        end
    end
    Plots.current()
end
Out[5]:
run (generic function with 1 method)
In [9]:
run(12, tsp_dfj_short, asym=false)
  0.118678 seconds (286.95 k allocations: 28.615 MiB, 24.58% gc time)
n = 12, obj = 190.56658600378748
Out[9]:
In [10]:
run(12, tsp_dfj_subsets, asym=true)
  0.092210 seconds (145.42 k allocations: 31.570 MiB)
n = 12, obj = 190.56658600378773
Out[10]:
In [11]:
run(12, tsp_mtz, asym=true)
  0.546728 seconds (14.85 k allocations: 942.938 KiB)
n = 12, obj = 190.5665860037875
Out[11]: