今回はTSPLIBにある「att48.tsp」というアメリカ48都市のデータを用いて前回の方法(LP:Lazy constraints)を試してみました。前回同様Pulpを使ってLPで解くことにします。TSPLIBには厳密解の巡回路データ「att48.opt.tour」も用意されているので結果を照らし合わせることができます。前回、線形計画法によるTSPの解法がある程度理解できたのでアルゴリズムを整理し直してみました。
事前準備:
まず、ダウンロードした「att48.tsp」をPandasで読み込みます。
厳密解データ(解答)も読み込んでおきます。厳密解は巡回順の整数値が並んでいるだけです。
実際このような巡回路となれば正解というわけです。48都市あるので各都市のインデックス番号を0〜47としておきます。TSPLIBでは1〜48の番号になっていますが、とりあえず今回は0〜47で計算することにします。
LPアルゴリズム:
48都市あるので、2都市間経路の組み合わせは48*47/2=1128となります。この場合都市0から都市1への移動となる(0, 1)という組み合わせに対して、その反転した組み合わせ(1, 0)は同じものとみなします(順列ではなく組み合わせのほうが数が減るので)。
ただし、最低限の制約として、
(0, 1), (0, 2), (0, 3)...(0, 46), (0, 47) から二つ採用(都市0において)
(0, 1), (1, 2), (1, 3)...(1, 46), (1, 47) から二つ採用(都市1において)
(0, 2), (1, 2), (2, 3)...(2, 46), (2, 47) から二つ採用(都市2において)
(0, 3), (1, 3), (2, 3)...(3, 46), (3, 47) から二つ採用(都市3において)
...
(0, 46), (1, 46), (2, 46)...(45, 46), (46, 47) から二つ採用(都市46において)
(0, 47), (1, 47), (2, 47)...(45, 47), (46, 47) から二つ採用(都市47において)
一旦この制約でsolve()し、varValue=1となる経路(採用された経路)を表示させます。
そうするとこのような複数の部分巡回路が出来上がります。この段階では各辺がバラバラなので部分巡回路ごとにグループ分けする関数を用意して仕分けします。
Lasy constraints/部分巡回路除去:
次にこれら部分巡回路のひとつをターゲットとしLazy constraintsを追加し部分巡回路除去を行います。今回の場合は辺数の一番少ない[(6, 17), (6, 27), (17, 35), (27, 35)]に制約を与えることにしました(前回は複数の部分巡回路に制約を与えていましたが)。
部分巡回路は完結した閉路なので、最低でも二本の経路を外につなげるように仕向けます。それが以下の制約になります。
この条件を与えたあと一旦solve()して結果を見ます。部分巡回路に多少変化が見られるので、何度か繰り返すと部分巡回路がまとまって一つの大きな巡回路になります。while文で最終的にひとつの巡回路になるまで繰り返すコードが以下です。
上記の状態だとまだバラバラの辺の集合なので、以下の処理で最終的な都市順路に並び替えます。
という順路リストが得られます。逆順になっている場合もあるので必要に応じて反転させます。TSPLIBにあるデータは都市番号1から始まっているので、それぞれに+1すれば同じ値になります。
アニメーション:
また変化をアニメーション表示するには以下(前半の設定は省略)。
事前準備:
まず、ダウンロードした「att48.tsp」をPandasで読み込みます。
data = pd.read_csv('att48.tsp', header=5) df = data['NODE_COORD_SECTION'].str.split(' ', expand=True) X = df[1].values[:-1].astype('int32') Y = df[2].values[:-1].astype('int32') XY = X + Y * 1j num = len(X)データは6行目から各都市の「番号 X座標 Y座標」となっており、主にX座標とY座標だけ抜き出します。最後の行には「EOF(End Of File)」があるので、その部分を含めないようにします。
厳密解データ(解答)も読み込んでおきます。厳密解は巡回順の整数値が並んでいるだけです。
T_opt = pd.read_table('att48.opt.tour', header=4) T_opt = list(T_opt.values[:-2, 0].astype('int32') - 1) + [0] draw_plot(T_opt)表示用draw_plot()関数を用意しておき厳密解を表示させます。
LPアルゴリズム:
48都市あるので、2都市間経路の組み合わせは48*47/2=1128となります。この場合都市0から都市1への移動となる(0, 1)という組み合わせに対して、その反転した組み合わせ(1, 0)は同じものとみなします(順列ではなく組み合わせのほうが数が減るので)。
prob = LpProblem(name='TSP_LP', sense=LpMinimize) dist = {(i, j): abs(XY[i] - XY[j]) for i in range(num) for j in range(num) if i < j} x = LpVariable.dicts('x', dist, 0, 1, LpBinary) prob += lpSum([dist[(i, j)] * x[(i, j)] for i, j in dist]) for n in range(num): prob += lpSum([x[(i, j)] for i, j in dist if n in (i, j)]) == 2 result = prob.solve() print(LpStatus[result]) ED = [(i, j) for (i, j) in x if x[(i, j)].varValue == 1] draw_plot(False, ED)前回同様Pulpを使って最小化する式を用意します。ここで注意すべきことは、i<jという大小関係になっていることと、変数xがバイナリLpBinaryになっているということです。変数x[(i, j)]はその組み合わせが経路として採用されれば1で不採用なら0となります。dist[(i, j)]*x[(i, j)]を積算しその値が最小値になるようにLPで解いていきます。
ただし、最低限の制約として、
for n in range(num): prob += lpSum([x[(i, j)] for i, j in dist if n in (i, j)]) == 2があります。この制約の部分を前回よりも簡潔に書き直してみました。巡回路になるためには各都市において必ず二つの経路が接続されていなければならないので、都市1であれば、(i, 1)もしくは(1, j)が二つ必要ということになります。以下のような感じです。
(0, 1), (0, 2), (0, 3)...(0, 46), (0, 47) から二つ採用(都市0において)
(0, 1), (1, 2), (1, 3)...(1, 46), (1, 47) から二つ採用(都市1において)
(0, 2), (1, 2), (2, 3)...(2, 46), (2, 47) から二つ採用(都市2において)
(0, 3), (1, 3), (2, 3)...(3, 46), (3, 47) から二つ採用(都市3において)
...
(0, 46), (1, 46), (2, 46)...(45, 46), (46, 47) から二つ採用(都市46において)
(0, 47), (1, 47), (2, 47)...(45, 47), (46, 47) から二つ採用(都市47において)
一旦この制約でsolve()し、varValue=1となる経路(採用された経路)を表示させます。
def subtour_list(ED): E = [] for (i, j) in ED: E_flat = sum(E, []) if (i, j) not in E_flat: S = [(i, j)] while True: SC = S.copy() for (ii, jj) in ED: if (ii, jj) not in S and (ii, jj) not in E_flat: if {ii, jj} & set(sum(S, ())): S.append((ii, jj)) if SC == S: E.append(S) break EL = [len(e) for e in E] AS = np.argsort(EL) ST = [E[a] for a in AS] return ST # output subtours ST = subtour_list(ED) for st in ST: print(len(st), st)部分巡回路ごとにグループ分けすると、
4 [(6, 17), (6, 27), (17, 35), (27, 35)] 5 [(2, 21), (2, 33), (15, 21), (15, 40), (33, 40)] 7 [(5, 29), (5, 36), (18, 36), (29, 42), (16, 42), (18, 26), (16, 26)] 13 [(1, 28), (1, 41), (4, 28), (4, 47), (25, 41), (38, 47), (3, 25), (3, 34), (31, 38), (34, 44), (9, 44), (23, 31), (9, 23)] 19 [(0, 7), (0, 8), (7, 37), (8, 39), (14, 39), (30, 37), (30, 43), (43, 45), (11, 14), (32, 45), (10, 11), (10, 22), (13, 22), (13, 24), (19, 32), (19, 46), (20, 46), (12, 20), (12, 24)]になります。一応、辺数の少ない順に並べ替えています。先頭の値は辺数。
Lasy constraints/部分巡回路除去:
次にこれら部分巡回路のひとつをターゲットとしLazy constraintsを追加し部分巡回路除去を行います。今回の場合は辺数の一番少ない[(6, 17), (6, 27), (17, 35), (27, 35)]に制約を与えることにしました(前回は複数の部分巡回路に制約を与えていましたが)。
部分巡回路は完結した閉路なので、最低でも二本の経路を外につなげるように仕向けます。それが以下の制約になります。
st_flat = set(sum(ST[0], ())) prob += lpSum([x[(i, j)] for (i, j) in dist if len({i, j} & st_flat) == 1]) >= 2 prob.solve()(i, j)の組み合わせのうち、iかjのどちらか一方がその部分巡回路に含まれて、他方が含まれていない組み合わせが二つ以上必要という条件にします。「if len({i, j} & st_flat) == 1」の部分が、(i, j)のどちらか一方が部分巡回路に含まれているかどうかという条件です。こうすることで、部分巡回路内にある最低でも二つの都市が他の部分巡回路の都市へつながろうとします。
この条件を与えたあと一旦solve()して結果を見ます。部分巡回路に多少変化が見られるので、何度か繰り返すと部分巡回路がまとまって一つの大きな巡回路になります。while文で最終的にひとつの巡回路になるまで繰り返すコードが以下です。
while len(ST) > 1: st_flat = set(sum(ST[0], ())) prob += lpSum([x[(i, j)] for (i, j) in dist if len({i, j} & st_flat) == 1]) >= 2 prob.solve() ED = [(i, j) for (i, j) in x if x[(i, j)].varValue == 1] ST = subtour_list(ED) print('ST[0]:', ST[0])今回は15ループ(3秒ほど)で48都市を最適化できました。
T = list(ED[0]) while len(T) < num: for (i, j) in ED: if i == T[-1] and j not in T: T.append(j) elif i not in T and j == T[-1]: T.append(i) T += [T[0]] print('Optimal Tour:', T) draw_plot(T)この結果、
という順路リストが得られます。逆順になっている場合もあるので必要に応じて反転させます。TSPLIBにあるデータは都市番号1から始まっているので、それぞれに+1すれば同じ値になります。
アニメーション:
また変化をアニメーション表示するには以下(前半の設定は省略)。
from matplotlib import animation from IPython.display import HTML fig = plt.figure() ims = [] while True: # lazy constraints st_flat = set(sum(ST[0], ())) prob += lpSum([x[(i, j)] for (i, j) in dist if len({i, j} & st_flat) == 1]) >= 2 prob.solve() img = draw_plot(False, ED, ST[0], ANIM=True) ims.append(img) ED = [(i, j) for (i, j) in x if x[(i, j)].varValue == 1] ST = subtour_list(ED) if len(ST) == 1: img = draw_plot(False, ED, ST[0], ANIM=True) ims.append(img) break ani = animation.ArtistAnimation(fig, ims, interval=800) #ani.save("tsp_48_1.gif", writer = "imagemagick") # save as animation gif plt.close() HTML(ani.to_jshtml())
以下が作成したアニメーション。赤い部分は制約を与える部分巡回路。
以下が全体のコード。
上記Gist内のコードには(i, j)の組み合わせが倍の2256通りあるパターンについても試しています。組み合わせが多いのでその分時間がかかりますが、同じアルゴリズムで解くことができます。