-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathtsp.py
116 lines (87 loc) · 3.16 KB
/
tsp.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
#!/usr/bin/env python3.7
# Copyright 2022, Gurobi Optimization, LLC
# Solve a traveling salesman problem on a randomly generated set of
# points using lazy constraints. The base MIP model only includes
# 'degree-2' constraints, requiring each node to have exactly
# two incident edges. Solutions to this model may contain subtours -
# tours that don't visit every city. The lazy constraint callback
# adds new constraints to cut them off.
import sys
import math
import random
from itertools import combinations
import gurobipy as gp
from gurobipy import GRB
# Callback - use lazy constraints to eliminate sub-tours
def subtourelim(model, where):
if where == GRB.Callback.MIPSOL:
vals = model.cbGetSolution(model._vars)
# find the shortest cycle in the selected edge list
tour = subtour(vals)
if len(tour) < n:
# add subtour elimination constr. for every pair of cities in tour
model.cbLazy(
gp.quicksum(model._vars[i, j] for i, j in combinations(tour, 2))
<= len(tour) - 1
)
# Given a tuplelist of edges, find the shortest subtour
def subtour(vals):
# make a list of edges selected in the solution
edges = gp.tuplelist((i, j) for i, j in vals.keys() if vals[i, j] > 0.5)
unvisited = list(range(n))
cycle = range(n + 1) # initial length has 1 more city
while unvisited: # true if list is non-empty
thiscycle = []
neighbors = unvisited
while neighbors:
current = neighbors[0]
thiscycle.append(current)
unvisited.remove(current)
neighbors = [j for i, j in edges.select(current, "*") if j in unvisited]
if len(cycle) > len(thiscycle):
cycle = thiscycle
return cycle
# Parse argument
if len(sys.argv) < 2:
print("Usage: tsp.py npoints")
sys.exit(1)
n = int(sys.argv[1])
# Create n random points
random.seed(1)
points = [(random.randint(0, 100), random.randint(0, 100)) for i in range(n)]
# Dictionary of Euclidean distance between each pair of points
dist = {
(i, j): math.sqrt(sum((points[i][k] - points[j][k]) ** 2 for k in range(2)))
for i in range(n)
for j in range(i)
}
m = gp.Model()
# Create variables
vars = m.addVars(dist.keys(), obj=dist, vtype=GRB.BINARY, name="e")
for i, j in vars.keys():
vars[j, i] = vars[i, j] # edge in opposite direction
# You could use Python looping constructs and m.addVar() to create
# these decision variables instead. The following would be equivalent
# to the preceding m.addVars() call...
#
# vars = tupledict()
# for i,j in dist.keys():
# vars[i,j] = m.addVar(obj=dist[i,j], vtype=GRB.BINARY,
# name='e[%d,%d]'%(i,j))
# Add degree-2 constraint
m.addConstrs(vars.sum(i, "*") == 2 for i in range(n))
# Using Python looping constructs, the preceding would be...
#
# for i in range(n):
# m.addConstr(sum(vars[i,j] for j in range(n)) == 2)
# Optimize model
m._vars = vars
m.Params.LazyConstraints = 1
m.optimize(subtourelim)
vals = m.getAttr("X", vars)
tour = subtour(vals)
assert len(tour) == n
print("")
print("Optimal tour: %s" % str(tour))
print("Optimal cost: %g" % m.ObjVal)
print("")