611 巡回セールスマン問題¶
Inspired by http://www.gurobi.com/documentation/8.0/examples/tsp_py.html
読み込み¶
In [1]:
import pandas as pd
from geopy.distance import great_circle
import gmaps
google_map_api_key = 'hogehoge'
gmaps.configure(api_key=google_map_api_key)
from ipywidgets.embed import embed_minimal_html
import gurobipy as gp
関数定義¶
In [2]:
def calc_dist_from_latlng(lat1, lng1, lat2, lng2, unit='m'):
if unit == 'm':
return great_circle((lat1, lng1), (lat2, lng2)).meters
elif unit == 'km':
return great_circle((lat1, lng1), (lat2, lng2)).kilometers
else:
print('ERROR: unit')
return False
パラメータ定義¶
Latlag定義¶
In [3]:
IND_BUILDINGS = ['Office', 'Library', 'Class', 'Gym', 'Pool', 'Ramen']
latlng_df = pd.DataFrame([[35.706048, 139.706735],
[35.705613, 139.707174],
[35.705713, 139.705254],
[35.706034, 139.716454],
[35.704950, 139.708149],
[35.708074, 139.708828]],
index=IND_BUILDINGS, columns=['lat', 'lng'])
dist_df = pd.DataFrame(index=IND_BUILDINGS, columns=IND_BUILDINGS)
for key, values in dist_df.iterrows():
for key2, value in values.items():
if key == key2:
#break
pass
dist_df.loc[key, key2] = calc_dist_from_latlng(latlng_df.loc[key].lat, latlng_df.loc[key].lng,
latlng_df.loc[key2].lat, latlng_df.loc[key2].lng)
display(latlng_df)
print('Distance matrix')
display(dist_df)
lat | lng | |
---|---|---|
Office | 35.706048 | 139.706735 |
Library | 35.705613 | 139.707174 |
Class | 35.705713 | 139.705254 |
Gym | 35.706034 | 139.716454 |
Pool | 35.704950 | 139.708149 |
Ramen | 35.708074 | 139.708828 |
Distance matrix
Office | Library | Class | Gym | Pool | Ramen | |
---|---|---|---|---|---|---|
Office | 0 | 62.5369 | 138.815 | 877.558 | 176.656 | 294.05 |
Library | 62.5369 | 0 | 173.719 | 839.227 | 114.828 | 311.75 |
Class | 138.815 | 173.719 | 0 | 1011.91 | 274.824 | 416.005 |
Gym | 877.558 | 839.227 | 1011.91 | 0 | 759.513 | 724.967 |
Pool | 176.656 | 114.828 | 274.824 | 759.513 | 0 | 352.742 |
Ramen | 294.05 | 311.75 | 416.005 | 724.967 | 352.742 | 0 |
Mixed Integer Linear programming of Travelling salesman problem¶
TSP with MTZ[1]
\begin{align}
\text{Minimize } \Sigma_{i=1}^n\Sigma_{j=1}^n c_{ij}\delta_{ij}, \\
\text{Subject to }\Sigma_{i=1,i\neq j}^n\delta_{ij}&=1 &\forall j\in \{1,...,n\}, \\
\Sigma_{j=1,j\neq i}^n\delta_{ji}&=1 &\forall i\in \{1,...,n\}, \\
x_{i} - x_{j} + (n-1)\delta_{ij} &\le n-2 &\forall i\in \{1,...,n\}, \forall j \in \{2,...,n\} \\
\text{where } c \in R^{n\times n}, \delta\in \{0,1\}^{n\times n}, 1\le x_{i}\le n-1 \nonumber.
\end{align}
See reference for more detailed formulation.
[1] C. E. Miller, A. W. Tucker, and R. A. Zemlin, “Integer Programming Formulation of Traveling Salesman Problems,” J. ACM, vol. 7, no. 4, pp. 326–329, 1960.
[2] A. Langevin, F. Soumis, and J. Desrosiers, “Classification of travelling salesman problem formulations,” Oper. Res. Lett., vol. 9, no. 2, pp. 127–132, 1990.
[3] A. Subramanyam and C. E. Gounaris, “A branch-and-cut framework for the consistent traveling salesman problem,” Eur. J. Oper. Res., vol. 248, no. 2, pp. 384–395, 2016.
In [4]:
model = gp.Model('tsp')
delta_var_dict = {}
cont_var_dict = {}
for src in dist_df.keys():
if src != dist_df.keys()[0]:
cont_var_dict[src] = model.addVar(lb=1, ub=len(dist_df.keys())-1, name='u({})'.format(src))
for dest in dist_df.keys():
if src == dest: continue
delta_var_dict[(src, dest)] = model.addVar(obj=round(dist_df.loc[src, dest]), vtype=gp.GRB.BINARY,
name='b_edge({},{})'.format(src, dest))
model.update()
for src in dist_df.keys():
lhs_sd = gp.LinExpr(0)
lhs_ds = gp.LinExpr(0)
for dest in dist_df.keys():
if src == dest: continue
lhs_sd += delta_var_dict[(src, dest)]
lhs_ds += delta_var_dict[(dest, src)]
model.addConstr(lhs_sd == 1, name='eq_src_dest_({}{})'.format(src, dest))
model.addConstr(lhs_ds == 1, name='eq_dest_src_({}{})'.format(dest, src))
for src in dist_df.keys()[1:]:
for dest in dist_df.keys()[1:]:
if src == dest: continue
model.addConstr(cont_var_dict[src] - cont_var_dict[dest]
+ (len(dist_df.keys())-1)*delta_var_dict[(src, dest)]
<= len(dist_df.keys())-2, name='eq_u({},{})'.format(src, dest))
output_file = 'tsp.lp'
model.write(output_file)
print(open(output_file).read())
\ Model tsp
\ LP format - for model browsing. Use MPS format to capture full model detail.
Minimize
63 b_edge(Office,Library) + 139 b_edge(Office,Class)
+ 878 b_edge(Office,Gym) + 177 b_edge(Office,Pool)
+ 294 b_edge(Office,Ramen) + 63 b_edge(Library,Office)
+ 174 b_edge(Library,Class) + 839 b_edge(Library,Gym)
+ 115 b_edge(Library,Pool) + 312 b_edge(Library,Ramen)
+ 139 b_edge(Class,Office) + 174 b_edge(Class,Library)
+ 1012 b_edge(Class,Gym) + 275 b_edge(Class,Pool)
+ 416 b_edge(Class,Ramen) + 878 b_edge(Gym,Office)
+ 839 b_edge(Gym,Library) + 1012 b_edge(Gym,Class)
+ 760 b_edge(Gym,Pool) + 725 b_edge(Gym,Ramen) + 177 b_edge(Pool,Office)
+ 115 b_edge(Pool,Library) + 275 b_edge(Pool,Class)
+ 760 b_edge(Pool,Gym) + 353 b_edge(Pool,Ramen)
+ 294 b_edge(Ramen,Office) + 312 b_edge(Ramen,Library)
+ 416 b_edge(Ramen,Class) + 725 b_edge(Ramen,Gym)
+ 353 b_edge(Ramen,Pool)
Subject To
eq_src_dest_(OfficeRamen): b_edge(Office,Library) + b_edge(Office,Class)
+ b_edge(Office,Gym) + b_edge(Office,Pool) + b_edge(Office,Ramen) = 1
eq_dest_src_(RamenOffice): b_edge(Library,Office) + b_edge(Class,Office)
+ b_edge(Gym,Office) + b_edge(Pool,Office) + b_edge(Ramen,Office) = 1
eq_src_dest_(LibraryRamen): b_edge(Library,Office) + b_edge(Library,Class)
+ b_edge(Library,Gym) + b_edge(Library,Pool) + b_edge(Library,Ramen)
= 1
eq_dest_src_(RamenLibrary): b_edge(Office,Library) + b_edge(Class,Library)
+ b_edge(Gym,Library) + b_edge(Pool,Library) + b_edge(Ramen,Library)
= 1
eq_src_dest_(ClassRamen): b_edge(Class,Office) + b_edge(Class,Library)
+ b_edge(Class,Gym) + b_edge(Class,Pool) + b_edge(Class,Ramen) = 1
eq_dest_src_(RamenClass): b_edge(Office,Class) + b_edge(Library,Class)
+ b_edge(Gym,Class) + b_edge(Pool,Class) + b_edge(Ramen,Class) = 1
eq_src_dest_(GymRamen): b_edge(Gym,Office) + b_edge(Gym,Library)
+ b_edge(Gym,Class) + b_edge(Gym,Pool) + b_edge(Gym,Ramen) = 1
eq_dest_src_(RamenGym): b_edge(Office,Gym) + b_edge(Library,Gym)
+ b_edge(Class,Gym) + b_edge(Pool,Gym) + b_edge(Ramen,Gym) = 1
eq_src_dest_(PoolRamen): b_edge(Pool,Office) + b_edge(Pool,Library)
+ b_edge(Pool,Class) + b_edge(Pool,Gym) + b_edge(Pool,Ramen) = 1
eq_dest_src_(RamenPool): b_edge(Office,Pool) + b_edge(Library,Pool)
+ b_edge(Class,Pool) + b_edge(Gym,Pool) + b_edge(Ramen,Pool) = 1
eq_src_dest_(RamenRamen): b_edge(Ramen,Office) + b_edge(Ramen,Library)
+ b_edge(Ramen,Class) + b_edge(Ramen,Gym) + b_edge(Ramen,Pool) = 1
eq_dest_src_(RamenRamen): b_edge(Office,Ramen) + b_edge(Library,Ramen)
+ b_edge(Class,Ramen) + b_edge(Gym,Ramen) + b_edge(Pool,Ramen) = 1
eq_u(Library,Class): u(Library) + 5 b_edge(Library,Class) - u(Class) <= 4
eq_u(Library,Gym): u(Library) + 5 b_edge(Library,Gym) - u(Gym) <= 4
eq_u(Library,Pool): u(Library) + 5 b_edge(Library,Pool) - u(Pool) <= 4
eq_u(Library,Ramen): u(Library) + 5 b_edge(Library,Ramen) - u(Ramen) <= 4
eq_u(Class,Library): - u(Library) + u(Class) + 5 b_edge(Class,Library)
<= 4
eq_u(Class,Gym): u(Class) + 5 b_edge(Class,Gym) - u(Gym) <= 4
eq_u(Class,Pool): u(Class) + 5 b_edge(Class,Pool) - u(Pool) <= 4
eq_u(Class,Ramen): u(Class) + 5 b_edge(Class,Ramen) - u(Ramen) <= 4
eq_u(Gym,Library): - u(Library) + u(Gym) + 5 b_edge(Gym,Library) <= 4
eq_u(Gym,Class): - u(Class) + u(Gym) + 5 b_edge(Gym,Class) <= 4
eq_u(Gym,Pool): u(Gym) + 5 b_edge(Gym,Pool) - u(Pool) <= 4
eq_u(Gym,Ramen): u(Gym) + 5 b_edge(Gym,Ramen) - u(Ramen) <= 4
eq_u(Pool,Library): - u(Library) + u(Pool) + 5 b_edge(Pool,Library) <= 4
eq_u(Pool,Class): - u(Class) + u(Pool) + 5 b_edge(Pool,Class) <= 4
eq_u(Pool,Gym): - u(Gym) + u(Pool) + 5 b_edge(Pool,Gym) <= 4
eq_u(Pool,Ramen): u(Pool) + 5 b_edge(Pool,Ramen) - u(Ramen) <= 4
eq_u(Ramen,Library): - u(Library) + u(Ramen) + 5 b_edge(Ramen,Library)
<= 4
eq_u(Ramen,Class): - u(Class) + u(Ramen) + 5 b_edge(Ramen,Class) <= 4
eq_u(Ramen,Gym): - u(Gym) + u(Ramen) + 5 b_edge(Ramen,Gym) <= 4
eq_u(Ramen,Pool): - u(Pool) + u(Ramen) + 5 b_edge(Ramen,Pool) <= 4
Bounds
1 <= u(Library) <= 5
1 <= u(Class) <= 5
1 <= u(Gym) <= 5
1 <= u(Pool) <= 5
1 <= u(Ramen) <= 5
Binaries
b_edge(Office,Library) b_edge(Office,Class) b_edge(Office,Gym)
b_edge(Office,Pool) b_edge(Office,Ramen) b_edge(Library,Office)
b_edge(Library,Class) b_edge(Library,Gym) b_edge(Library,Pool)
b_edge(Library,Ramen) b_edge(Class,Office) b_edge(Class,Library)
b_edge(Class,Gym) b_edge(Class,Pool) b_edge(Class,Ramen)
b_edge(Gym,Office) b_edge(Gym,Library) b_edge(Gym,Class) b_edge(Gym,Pool)
b_edge(Gym,Ramen) b_edge(Pool,Office) b_edge(Pool,Library)
b_edge(Pool,Class) b_edge(Pool,Gym) b_edge(Pool,Ramen)
b_edge(Ramen,Office) b_edge(Ramen,Library) b_edge(Ramen,Class)
b_edge(Ramen,Gym) b_edge(Ramen,Pool)
End
最適化実行¶
In [5]:
model.optimize()
Optimize a model with 32 rows, 35 columns and 120 nonzeros
Variable types: 5 continuous, 30 integer (30 binary)
Coefficient statistics:
Matrix range [1e+00, 5e+00]
Objective range [6e+01, 1e+03]
Bounds range [1e+00, 5e+00]
RHS range [1e+00, 4e+00]
Found heuristic solution: objective 2837.0000000
Presolve time: 0.00s
Presolved: 32 rows, 35 columns, 180 nonzeros
Variable types: 5 continuous, 30 integer (30 binary)
Root relaxation: objective 2.061517e+03, 21 iterations, 0.00 seconds
Nodes | Current Node | Objective Bounds | Work
Expl Unexpl | Obj Depth IntInf | Incumbent BestBd Gap | It/Node Time
0 0 2061.51724 0 9 2837.00000 2061.51724 27.3% - 0s
H 0 0 2495.0000000 2061.51724 17.4% - 0s
H 0 0 2387.0000000 2061.51724 13.6% - 0s
H 0 0 2207.0000000 2061.51724 6.59% - 0s
0 0 infeasible 0 2207.00000 2207.00000 0.00% - 0s
Cutting planes:
Gomory: 1
Implied bound: 2
Clique: 2
MIR: 6
Explored 1 nodes (31 simplex iterations) in 0.06 seconds
Thread count was 8 (of 8 available processors)
Solution count 4: 2207 2387 2495 2837
Optimal solution found (tolerance 1.00e-04)
Best objective 2.207000000000e+03, best bound 2.207000000000e+03, gap 0.0000%
最適化結果¶
In [6]:
optimum_var_dict = model.getAttr('x', delta_var_dict)
optimal_dict = {}
for src in dist_df.keys():
for dest in dist_df.keys():
if src == dest: continue
if optimum_var_dict[src, dest] != 0:
optimal_dict[(src, dest)] = 1
display(model.getAttr('x', cont_var_dict), optimal_dict)
{'Class': 5.0,
'Gym': 2.0,
'Library': 4.0,
'Pool': 2.9999999999999942,
'Ramen': 1.0}
{('Class', 'Office'): 1,
('Gym', 'Pool'): 1,
('Library', 'Class'): 1,
('Office', 'Ramen'): 1,
('Pool', 'Library'): 1,
('Ramen', 'Gym'): 1}
可視化¶
In [7]:
fig = gmaps.figure(center=(35.706219, 139.712076), zoom_level=16)
marker_drawing = gmaps.symbol_layer(locations=latlng_df, fill_color='green', stroke_color='green',
scale=5, hover_text=latlng_df.index.values,
info_box_content=latlng_df.index.values)
fig.add_layer(marker_drawing)
lines = []
for key, src_latlng in latlng_df.iterrows():
for key2, dest_latlng in latlng_df.iterrows():
if src_latlng['lat'] == dest_latlng['lat'] and src_latlng['lng'] == dest_latlng['lng']: continue
line = gmaps.Line(start=(src_latlng['lat'], src_latlng['lng']),
end=(dest_latlng['lat'], dest_latlng['lng']))
if line not in lines:
lines.append(line)
line_drawing = gmaps.drawing_layer()
line_drawing.features = lines
fig.add_layer(line_drawing)
#fig
embed_minimal_html('../_static/html/tsp_candidate.html', views=[fig])
In [8]:
%%HTML
<iframe src='../_static/html/tsp_candidate.html' width=700, height=500></iframe>
In [9]:
fig = gmaps.figure(center=(35.706219, 139.712076), zoom_level=16)
marker_drawing = gmaps.symbol_layer(locations=latlng_df, fill_color='green', stroke_color='green',
scale=5, hover_text=latlng_df.index.values, info_box_content=latlng_df.index.values)
fig.add_layer(marker_drawing)
lines = []
for key, value in optimal_dict.items():
lines.append(gmaps.Line(start=(latlng_df.loc[key[0], 'lat'], latlng_df.loc[key[0], 'lng']),
end=(latlng_df.loc[key[1], 'lat'], latlng_df.loc[key[1], 'lng'])))
line_drawing = gmaps.drawing_layer()
line_drawing.features = lines
fig.add_layer(line_drawing)
#fig
embed_minimal_html('../_static/html/tsp_optimum.html', views=[fig])
In [10]:
%%HTML
<iframe src='../_static/html/tsp_optimum.html' width=700, height=500></iframe>