grbl1.1+Arduino CNCシールドV3.5+bCNCを使用中。
BluetoothモジュールおよびbCNCのPendant機能でスマホからもワイヤレス操作可能。
その他、電子工作・プログラミング、機械学習などもやっています。
MacとUbuntuを使用。

CNCマシン全般について:
国内レーザー加工機と中国製レーザー加工機の比較
中国製レーザーダイオードについて
CNCミリングマシンとCNCルーターマシンいろいろ
その他:
利用例や付加機能など:
CNCルーター関係:



*CNCマシンの制作記録は2016/04/10〜の投稿に書いてあります。


ラベル LP の投稿を表示しています。 すべての投稿を表示
ラベル LP の投稿を表示しています。 すべての投稿を表示

2019年7月18日木曜日

TSP LP with scipy.optimize.linprog

今回はscipyを使ってTSP(巡回セールスマン問題)を解いてみました。前回までのPulpのような最適化ソルバー/モデラーであれば0-1整数計画法(混合整数計画法)として計算可能でしたが、scipyの線形計画法の関数であるlinprogは整数計画法まではカバーされていないため、離散値(整数値)ではなく連続値として計算しなければいけません。Pulpを使ったほうがコーディングも楽だし処理も早いと思いますが、今回は敢えてscipyで可能かどうかという試みです。アルゴリズム的には基本的に前回と同じですが、今回は連続値を扱うのとscipyのフォーマットで書き直すため微妙に異なる点があります。データは前回同様TSPLIBのアメリカ48都市を用いることにします。

目的関数とベースの制約:
最小化する式は以下のようになります。

min: Σ(dist(i, j) * x(i, j))

s.t: sum(x(0, 1) + x(0, 2) + ... + x(0, 47)) = 2
     sum(x(0, 1) + x(1, 2) + ... + x(1, 47)) = 2
     ・・・
     sum(x(0, 47) + x(1, 47) + ... + x(46, 47)) = 2

とりあえず各都市において最低2本の経路が接続されていなければならないという条件を加えておきます。dist(i, j)は二都市間(i, j)における距離、x(i, j)は経路として採用された場合は1、不採用なら0になりますが、scipyの場合はこの値が連続値になるので0.0〜1.0の小数になります。
上記をscipy.optimize.linprogのフォーマットで書き直すと以下のようになります。
e = [(i, j) for j in range(num) for i in range(num) if i < j]
dist = [abs(XY[i] - XY[j]) for j in range(num) for i in range(num) if i < j]

A_eq = [[1 if n in (i, j) else 0 for (i, j) in e] for n in range(num)]
b_eq = [2] * num
bounds = [(0, 1)] * len(dist)
    
res = linprog(c=dist, A_eq=A_eq, b_eq=b_eq, bounds=bounds, method="revised simplex")
linprogの引数c=distは1次元配列でなければならないので、eとdistは二都市間(i, j)のマトリクスになりますが1次元配列として用意します。48*47/2=1128個の値を含んだ配列になります。
e:(i, j)の二都市を参照するための配列
dist:二都市間の距離を格納した配列
A_eq:各都市における二経路接続の制約マトリクス(二次元配列:1128x48)
b_eq:制約ベクター
bounds:x(i, j)の値の範囲(今回は0.0〜1.0の連続値)
この制約で一度答えを出してみます。methodはより正確なrevised simplexを用いることにします(そうしないとWarningが出る)。まず以下の方法でx(i, j)の値がそれぞれどのくらいになっているか見てみます。
plt.plot(list(range(len(dist))), res.x )
そうすると、以下のように1128通りあるx(i, j)は0か0.5か1.0(縦軸)になっています。
横軸はx(i, j)をx[n]のインデックス番号に変換した値です(n=0〜1127)。実際これを地図に落とし込むと以下のようになります。

赤い線がx(i, j)=0.5、グレーの線がx(i, j)=1.0です。今回はx(i, j)の値が0.0〜1.0の連続値になっているため、この段階では0.5と1.0の二種類の経路が混在しています(もしバイナリ設定が可能なPulpなどのライブラリを使えば0か1の表現になるのでもう少し簡単になるのですが)。


部分巡回路除去:
次の段階として、この状態から部分巡回路を選択して、その部分巡回路に追加の制約を与えます。まず目につくのが、右上にある7都市からなる部分巡回路と下部右寄りにある五角形の部分巡回路です。この段階で選択する部分巡回路の条件は、部分巡回路の都市数が全体の半数未満(48都市あるので24都市未満)でx(i, j)=1.0で構成されているもの(実際は0.5の経路群を含めても可)。複数ある場合は都市数が少ない方をターゲットとします。よって今回は下部右寄りにある五角形{2, 15, 21, 33, 40}を選択します。

ターゲットの部分巡回路の都市群をst_flat={2, 15, 21, 33, 40}として、以下の制約を追加して再度解きます。ST15[0]というのはこの都市群における経路群[(2, 21), (2, 33), (15, 21), (15, 40), (33, 40)]であり、st_flatというのは経路群を都市群に変換したものです。
st_flat = set(sum(ST15[0], ()))
A_ub = [[-1 if len({i, j} & st_flat) == 1 else 0 for (i, j) in e]]
b_ub = [-2]
res = linprog(c=dist, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq=b_eq, bounds=bounds, method="revised simplex")
閉じている部分巡回路に対して部分巡回路外の都市と接続するように仕向けるための制約です。x(i, j)のiかjの一方が部分巡回路に属し、もう一方が属していない都市となるx(i, j)の組み合わせが2個以上という制約です。Pulpなどでの制約式としては以下のようになりますが(前回参照)、

sum([x(i, j) for (i, j) in e if len({i, j} & st_flat) == 1]) >= 2

scipy.optimize.linprogでは基本的に「<=」が使用されるため「>=」の場合はをマイナス反転するようです。よってA_ubにおいてx(i, j)が条件に従う場合は-1でそうでない場合は0という配列にしておきます(この部分が多少面倒)。またb_ubのほうにもマイナスをかけて「-2」にしておきます。

この結果、五角形の部分巡回路は以下のようにつながります。
次のターゲットは右上にある7都市の部分巡回路です。同様に制約を追加して解いていきます。


部分巡回路がない場合(接続数1):
部分巡回路除去の手順を繰り返すと徐々に部分巡回路はつながっていき、以下のような状態になります。
この状態では分離した部分巡回路はありません。このようなひとつながりでありつつ巡回路としては不完全な状態のときには、条件を変えて次のターゲットを選ぶことにします。
今回の場合であれば、左側にある辛うじて一本の経路でつながっている7都市からなる都市群{3, 9, 23, 25, 34, 41, 44}を選択します。この経路はx(i, j)=0.5である赤い経路の都市群{9, 23, 25, 41, 44}とx(i, j)=1.0である{3, 25, 34, 44}と{9, 23}で構成されています。{41}の1箇所で全体に対してつながっているので、この都市群に最低でも二本の経路でつながるような制約を追加します。
似たような箇所として右上にある{5, 16, 18, 26, 27, 29, 35, 36, 42}も{35}の1箇所で辛うじてつながっています。このような1箇所でしか接続されていない都市群を探し出して二経路接続の制約を追加するというのが今回の手段になります。もちろんこれらの都市群の数は全体の都市数の半分未満の数でなければならないという条件と、もし複数ある場合は合計都市数の少ない方を選択するということにしておきます。
条件としては、
・x(i, j)=0.5だけで構成される部分巡回路を探す
・x(i, j)=0.5の部分巡回路に2箇所以上接続しているx(i, j)=1.0の部分巡回路/部分経路を足し合わせる
・足し合わせた都市群は全体都市数の半数未満でなければならない
という感じです。

この部分はかなり面倒な手続きになりますが、そのような都市群を探し出す関数を用意することにしました(あとで登場)。

よって、st_flat={5, 16, 18, 26, 27, 29, 35, 36, 42}に制約を与えると以下のようになります。
左側は部分巡回路として分離しましたがx(i, j)=0.5の赤い経路は消えてくれました。
以後は、これまでと同様に
・分離した部分巡回路(全体の半分未満の都市数からなる)
・分離した部分巡回路がない場合、1箇所でつながっている都市群
という条件で制約を追加していきます。


部分巡回路がない場合(接続数3以上):
前述の手順を繰り返していると、以下のようなパターンもでてきます。
全体としてはつながっており、1箇所だけでつながっている部分もありません。次の条件としては、x(i, j)=0.5で構成される赤い部分巡回路で他の都市群に接続している部分が3箇所以上あるものを選び、他の都市群への接続数を2箇所以下に制約します。先ほどの方法同様、赤い部分巡回路に対して2箇所以上で接続しているx(i, j)=1.0の部分経路は赤い巡回路に足し合わせます。
具体的には、赤い巡回路{2, 13, 15, 22, 28, 33, 40}に着目します。x(i, j)=1.0である{33, 40}と{2, 15, 21}は、赤い巡回路{2, 13, 15, 22, 28, 33, 40}に対して2箇所で接続しているため、足し合わせて{2, 13, 15, 21, 22, 28, 33, 40}を今回のターゲットとします。
この都市群は{13}と{22}と{28}の3箇所において他の都市群とつながっているので、この接続数3を2以下に制約します。以下がこの部分のコード。
A_ub.append([1 if len({i, j} & st_flat) == 1 else 0 for (i, j) in e])
b_ub.append(2)
この制約で一旦解いてみると、以下のようになります。
分離した部分巡回路ができあがりましたが、赤い経路は消えてx(i, j)=1.0の部分巡回路だけになりました。
次に、最初の方法で右側の部分巡回路に二経路接続の制約を与えると、
部分巡回路がなくなってひとまとまりになります。これで最適解が得られました。


制約を与える手順:
今回は0.5の経路と1.0の経路(実際は0の経路もあるけれども表示されない)があるので、制約を与えるターゲットをどのように選ぶかが複雑でしたが、要約すれば以下のような感じになります。
・ターゲットとなる部分巡回路もしくは足し合わせた都市群の数は全体都市数の半数未満とする
・分離した部分巡回路あるいは都市群がある場合は、その都市群がターゲット
・分離した部分巡回路がない場合は、0.5の部分巡回路とそれに付随する1.0の部分経路を足し合わせた都市群をターゲットとする
・足し合わせた都市群が他の都市群と接続している箇所が、2未満の場合は2以上の制約をあたえる
・足し合わせた都市群が他の都市群と接続している箇所が、3以上の場合は2以下の制約をあたえる

この条件分けで繰り返しループ処理させるコードが以下になります。
def st_elim(ST10, ST05, ST15):
    st_flat = None
    if len(ST15) > 1:
        st_flat = set(sum(ST15[0], ()))
    else:
        if len(ST15[0]) == num:#  and is_loop(ST15[0]):
            st_flat = set(sum(ST15[0], ()))
            print('Optimal')
        else:
            if len(ST05) > 0:
                for st05 in ST05:
                    st05f = set(sum(st05, ()))
                    for st10 in ST10:
                        st10f = set(sum(st10, ()))
                        if len(st05f & st10f) >= 2 and len(st05f | st10f) < num / 2:
                            st05f |= st10f
                            st_flat = st05f

    print('st:',st_flat)
    return st_flat

while True:
    E10 = [e[i] for i, x in enumerate(res.x) if x > 0.99]
    E05 = [e[i] for i, x in enumerate(res.x) if x > 0.49 and x <= 0.51]
    E15 = E10 + E05
    
    ST10 = subtour_list(E10)
    ST05 = subtour_list(E05)
    ST15 = subtour_list(E15)
    
    st_flat = st_elim(ST10, ST05, ST15)
    
    if st_flat == set(range(num)):
        print('Optimal')
        draw_plot(False, E10, E05)
        break
         
    if st_flat is None:
        print('st_flat is None')
        break
    else:            
        cnt = 0
        for i, j in E10:
            if len({i, j} & st_flat) == 1:
                cnt += 1
            
        if cnt > 2:
            A_ub.append([1 if len({i, j} & st_flat) == 1 else 0 for (i, j) in e])
            b_ub.append(2)
            #print('cnt:', cnt)
        else:
            A_ub.append([-1 if len({i, j} & st_flat) == 1 else 0 for (i, j) in e])
            b_ub.append(-2)
        res = linprog(c=dist, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq=b_eq, bounds=bounds, method="revised simplex")
最初のst_elim()というのが、ターゲットとなる都市群を探し出す関数です。そして続くwhileループ内でターゲットとなる都市群に条件(他都市との接続数)に応じて制約を追加していきます。今回の場合1ループで約10秒、48都市を解決するのに約2分かかりました。
他のサンプルでは試していないので、まだ完璧ではないかもしれませんが(これから検証)、一応scipyを用いて試してみたという感じです。


以下が全体のコード:

2019年6月5日水曜日

TSP LP Lazy Subtour Elimination Constraints:巡回セールスマン問題  逐次組込制約による部分巡回路除去

前回Pulp(オープンソース最適化ソルバー/モデラー)を使ってTSP(巡回セールスマン問題)の厳密解をMTZ(Miller-Tucker-Zemlin)部分巡回路除去法で求めてみました。前々回に試したDP(動的計画法)よりはMTZのほうが高速でしたが、せいぜい数十ノード程度が限界でした。


Lazy Constraints(逐次組込制約):
そして今回もLPで厳密解を求めますが、Lazy Constraintsという制約式を使ってみようと思います。Lazy Constraintsの日本語訳が検索してもあまりでてこないので(ここくらい)、逐次組込制約と呼ぶことにしました。あるいは切除平面法と呼ばれているのかもしれません。
Lazy Constraintsの特長として、最初に基本的な制約だけを与えて一度結果を出し、その結果からその都度必要な制約を追加していき、何回かのループで答えを出します。予め全ての組み合わせの計算をしない分早く収束してくれるときがあります。必要なら計算するという方法が意外に効果的でこれまでの結果を圧倒してくれました。最適化ソルバーPulpのおかげかもしれませんが、これまでのアルゴリズムではせいぜい20ノードが限界でしたが、今回は100ノード以上でも短時間で厳密解を求めることができました。
実際、厳密解かどうかは不安なのでPyconcorde(TSP専用ソルバー)で求めた解と比較することにしました。


これまでの厳密解アルゴリズム比較:(Python 3.6、Jupyter Notebook使用
全探索法:20ノード未満で限界
DP(functools.lru_cache):20ノード:50.9s
DP(自前のmemoization):20ノード:57.7s
DP(ビットDP):20ノード:55.8s
LP(MTZ部分巡回路除去):20ノード:12.9s
LP(Lazy Constraints):20ノード:0.73s、100ノード:23.9s
Pyconcorde(TSPソルバー):20ノード:0.23s、100ノード:0.597s

20ノード以上になると極端に時間がかかっていましたが、今回のLazy Constraintsでは100ノードでも数十秒で計算可能でした。もちろんTSP専用ソルバーのPyconcordeなら100ノードでも1秒もかかりません。
ただ、Lazy Constraintsだと状況に応じてその都度制約を追加していくので、早く解決するときもあれば多少時間がかかるときもあり時間にばらつきがあります。今回200ノードまでは数分で厳密解を求めることができましたが、200ノードを越えるとRAMが足りないためなのか途端に処理が遅くなってしまいました。


Lazy Constraintsアルゴリズム:
アルゴリズムにおいては、前回のMTZと比較してもそれほど複雑なことをしているという感じでもありませんでした(ここを参考)。
基本はLPなので、
prob = LpProblem(name='TSP_LP', sense=LpMinimize)

# node i to node j: distance between i and j
dist = dict({(i, j): (abs(XY[i] - XY[j])) for i in range(num) for j in range(num) if i != j})

#  1: the tour includes the edge from i to j, 0: if not included
x = LpVariable.dicts('x', dist, 0, 1, LpBinary)

# the objective to be minimumized
prob += lpSum([dist[(i,j)] * x[(i,j)] for i, j in dist]) 
前回同様、各二点間(i, j)をディクショナリーのキーとして登録しておき、それぞれ値に距離を入れておきます。各ノードの座標は事前にランダム生成してあるので、その座標から距離を求めることができます。x(i, j)は0か1のバイナリで、最終的な巡回路にiからjへの経路が採用されれば1、不採用なら0になります。つまり採用された(i, j)の組だけが距離distとして合算されていきます。
そして最小化する式、

Σ(dist(i, j) * x(i, j))

を解いていきます。

ベースの制約として、これも前回同様iを基準としてiからjへの移動x(i, j)、あるいはその反対であるjからiへの移動における組み合わせではそれぞれ一通りずつなので、以下の制約式を加えておきます。
for i in path:
    prob += lpSum([x[(i, j)] for j in path if i != j]) == 1
    
for j in path:
    prob += lpSum([x[(i, j)] for i in path if i != j]) == 1
前回はこのあとMTZの部分巡回路除去の制約を加えていましたが、今回は一旦ここでこの問題を解いてみます。当然中途半端な答えがでてくるので、その答えを見ながら次に必要な制約式を加えていきます。ここがLazyと言われる部分なのでしょう。

この状態で解かせると、大体は以下のように二点間だけの巡回路がたくさんできてしまいます。「0→23→0」など単なる二点間の往復経路ばかり。
100ノードの場合。
最初の二つの制約は、(i, j)マトリクスで考えれば横軸上での組み合わせの中から一つ選び、縦軸上の組み合わせから一つ選ぶという二重制約です。
前回書いた以下のマトリクス(6x6の場合)、
------- x(0, 1) x(0, 2) x(0, 3) x(0, 4) x(0, 5)

x(1, 0) ------- x(1, 2) x(1, 3) x(1, 4) x(1, 5)

x(2, 0) x(2, 1) ------- x(2, 3) x(2, 4) x(2, 5)

x(3, 0) x(3, 1) x(3, 2) ------- x(3, 4) x(3, 5)

x(4, 0) x(4, 1) x(4, 2) x(4, 3) ------- x(4, 5)

x(5, 0) x(5, 1) x(5, 2) x(5, 3) x(5, 4) -------
仮にx(1, 2)を選ぶと、同じ横軸上のx(1, 0)、x(1, 3)、x(1, 4)、x(1, 5)は選べなくなり、同様に縦軸上のx(0, 2)、x(3, 2)、x(4, 2)、x(5, 2)も選べなくなるという制約。常に横軸上からひとつ、縦軸上から一つという条件を満たしていないといけません。

以下はシミューレータ(editor.p5js.orgで作成)。グリッド上の(i, j)のどれかを選ぶと次に選ぶことができない組み合わせができます。

グレー:選択可能な(i, j)
黒  :選択不可能な(i, j)
ピンク:選択した(i, j)


条件:
・選択した(i, j)と同じ横軸上の(i, j')は選択不可。
 例えば(1, 2)を選択したら(1, 0)〜(1, 9)は選択不可。
・選択した(i, j)と同じ縦軸上の(i', j)は選択不可。
 例えば(1, 2)を選択したら(0, 2)〜(9, 2)は選択不可。
・選択した(i, j)を反転した(j, i)は選択不可。
 例えば(1, 2)を選択したら(2, 1)は選択不可。
・二つ以上(かつ全ノード数未満)選択して部分巡回路が{i, j, k}となったとき(k, i)は選択不可。
 例えば(1, 2)、(2, 4)を選択したら(4, 1)は選択不可。また(1, 2)、(2, 4)、(4, 5)を選択したら(5, 1)は選択不可。


選べない経路(i, j)の組み合わせを考える:
この条件だけでは、二点間最短距離のペアばかりできてしまいます。いわゆるグラフ理論における最小マッチングのようなものでしょうか。
これではちょっと先行き不安なので、もう一つだけ簡単な制約式を自分なりに考えて加えておきました。
for i in path:
    for j in path:
        if i < j:
            prob += lpSum([x[(i, j)] + x[(j, i)]]) <= 1
これは、先ほどのマトリクス上で考えると、対角線上に線対称な組み合わせも選べないという制約です。たとえばx(1, 2)を選んだらその横軸と縦軸だけでなくx(2, 1)も選べないということです。これは先ほどの二点間のみの巡回路を制約しており、x(i, j)とx(j, i)は同時に存在しないので、x(i, j)+x(j, i)は1以下になるということです。

この制約を加えてまた一旦解かせてみると、
このように最低でも三点以上の巡回路になります。
少し改善されたように見えますが、実際効率が上がるのかは検証していないので不明。簡単な制約式なのでそれほど計算にも時間かからないのではと思って追加しただけです。かえって時間かかるようであれば、この制約がなくても解くことはできます。
ちなみに、三角形の経路を制約する場合、

lpSum(x[i, j] + x[j, k] + x[k, i]) <= 2

をさらに追加すると最低でも四角形の経路になりますが、3変数もあるのでかえって演算に時間がかかってしまいそうです。今回は2変数だけにして二点間往復路だけを制約してみました。

個人的に興味深いと思ったことは、道順と距離を求めることが目的なのにもかかわらず、それとは別に二点間のマトリクス上でのありえない組み合わせを探しているという部分です。TSPをそのような組み合わせパズルとして見たことはなかったので新鮮な視点が得られた感じがしました。これは一種の抽象的な思考方法なのかもしれませんが、こうやって別の角度から新たなルールを導き出しつつ解答に近くことができるのかもしれません。

ここまでが前準備で、そして以下からがLazy ConstraintsによるSubtour Elimination(部分巡回路除去)です。


Lazy Constraints Subtour Elimination:
制約式としては以下の部分だけです。
   for st in ST:
        nots = [j for j in path if j not in st]
        prob += lpSum(x[(i, j)] for i in st[:-1] for j in nots if (i, j) in x) >= 1
        prob += lpSum(x[(j, i)] for i in st[:-1] for j in nots if (i, j) in x) >= 1
先ほどの100ノードの場合であれば、部分巡回路は19個できてしまいました。この19個の部分巡回路のうち、全体のノード数の半分未満の部分巡回路に対して制約式を与えていきます。全体のノード数の半分以上の大きな部分巡回路を母体として残しておいて、全体のノード数の半分未満の小さな部分巡回路だけに変化(制約)を与えるということです。
今回は、全体のノード数の半分未満の小さな部分巡回路全てに制約を与えますが、代表的なひとつの部分巡回路だけに制約を与えていっても解けるようです。

制約の原理として、例えば「0→23→64→0」という三角形の部分巡回路がありますが、この部分巡回路に必要なのは最低でも一個以上のノードが他の部分巡回路のノードへつながらないといけない(そうしないと現状のまま閉じてしまうので)ということです。三角形内の辺x(0, 23)に対しては、i=0をそのままにしておいてj=23を他の巡回路へつないでもらうために、23が所属している部分巡回路(0, 23, 64)以外のノードをつなげたときに1になるような条件にします。notsという変数は、j=23であれば23が所属している部分巡回路(0, 23, 64)以外の他のノード全部ということです。
つまり、一度解いて出てきた結果x(0, 23)に対して、

x(0はそのまま, 23は{0, 23, 64}以外の全てのノード)

へつなげてみたらどうかという制約になります。
同様に、iとjを反転した場合の制約も加えておきます。
この制約で一発で解決するわけではないのですが、繰り返し解いてはその結果から他のノードへつながるよう(部分巡回路として閉じてしまわないように)仕向けて、最終的に一つの大きな部分巡回路ができあがれば、それが答えとなるわけです。

ちなみにmatplotlibでその経過をアニメーションにしてみました。





100ノードの場合です。この場合だと6回のループで最適解が求まりました。普通のノートパソコンで23.9sかかりました。
動画が自動ループしているとどこが始まりかわかりにくいですが、最初は細かい三角形や四角形の部分巡回路ばかりで、少しずつ繋がってはまた分解し全体としては徐々に大きい経路へと成長していきます。
一気に解決するような強力なアルゴリズムではなく、必要に応じて徐々に解決していくという部分が面白いと思います。結果的には無駄なことをあまりしていないので早く解決できるのと、その都度計算も重くならないのか100ノードでも現実的な時間内で解決できます。Lazyなのにすごい。

繰り返しループ処理させる部分については、以下のような感じです。

ED = []
while True:
    # Find edges as x(i, j) = 1
    E = [(i, j) for (i, j) in x if x[(i, j)].varValue == 1]
    ED.append(E)
    # Connect the edges to form subtours
    ST = []
    for n in path:
        if n not in sum(ST, []):
            subtour = [n]
            for _ in path:
                for (i, j) in E:
                    if subtour[-1] == i and j not in subtour:
                        subtour.append(j)
                    elif j == subtour[-1] and i not in subtour:
                        subtour.append(i)
            subtour.append(subtour[0])
            ST.append(subtour)

    print('Subtours:', len(ST))
    print(ST)
    if len(ST) == 1:
        print('\nOPT TOUR:', ST[0])
        print('SOLVED')
        break
        
    # Lazy constraints: for the minor subtours(the number of nodes in the subtour is less than half of the whole nodes)
    # Either of (i, j) in the subtour has to connect to a node in the other subtour,
    # hence 'i' stays in the subtour and 'j' has to be changed to one in the other subtour, and vice versa for (j, i)
    for st in ST:
        if len(st) < num / 2: # case of being less than half of the whole node
            nots = [j for j in path if j not in st]
            prob += lpSum(x[(i, j)] for i in st[:-1] for j in nots if (i, j) in x) >= 1
            prob += lpSum(x[(j, i)] for i in st[:-1] for j in nots if (i, j) in x) >= 1
    prob.solve()

T = ST[0]

最初のEDという変数はアニメーション用の配列なので無視して構いません。

whileですべての部分巡回路がひとつながりになるまでループします。
・最初の最適化された結果からx(i, j)=1の辺を探します。
・そして、それらのばらばらの辺(i, j)をつなぎ合わせてすべての部分巡回路(subtour=[0, 23, 64, 0]など)を配列でつくります。
・さらに各部分巡回路を全体用の配列STに入れていきます。len(ST)で何個の部分巡回路があるのかわかります。最終的に1になれば一つの大きな部分巡回路=正解の巡回路というわけです。
・まだ複数の部分巡回路があるうちは、全ノード数の半分未満の小さな部分巡回路stに対してその内容にあわせてLasy Constraintsを与えていきます。各部分巡回路内のx(i, j)に対してiは現状と同じノードでjだけ現在の所属する部分巡回路以外のノードを入れてみるということを繰り返していきます。
・同様に反転したx(j, i)についても制約を与えていきます。
・1ループごとにprob.solve()で暫定的な答えを出してみます。
・全体の部分巡回路の数が一つになるまで上記の繰り返し。
最終的には最適解がでてきます。1ループごとに部分巡回路の状況を出力させています。それをもとにアニメーションをつくりました。

今回は冒頭でも書いたように、本当に最適化されているのかどうかをTSP専用ソルバーであるPyconcordeでも同じ条件で解かせてその結果を比較するということにしました。一応結果は一致したのでアルゴリズム的には機能しているようです。TSP専用ソルバーと比べればまだまだですが、これまでの方法よりも圧倒的に早くかつ解決できるノード数も上回ったので、とりあえずひと段落ついたところです。

次へ:TSP LP: US48 cities / 巡回セールスマン問題 線形計画法
関連:Traveling Salesman Problem:巡回セールスマン問題について(まとめ)


人気の投稿