3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

この記事について

前回は2-optでルートを改善しました。今回はさらに一歩進んだ3-optを解説します。

3-optは「3本の辺を入れ替える」改善手法で、2-optで改善できない局所最適を脱出できることがあります。ただし、組み合わせが増える分、計算コストも上がります。

配送最適化入門シリーズの記事一覧はこちら。

記事 内容
⓪ 配送最適化とは?基礎概念
① 近傍法の説明
② 2-optの説明
③(本記事) 3-optの説明
④ Lin-Kernighan法の説明
⑤ 焼きなまし法(SA)の説明
⑥ 遺伝的アルゴリズム(GA)の説明
⑦ VRPとは?
⑧ VRPをアルゴリズムで解く

3-optとは?

3-optは 「3本の辺を削除し、残ったセグメントを再接続する」 改善手法です。

2-optとの違い

2-optでは2本の辺を選んで入れ替えるため、再接続のパターンは1通りしかありません(間のセグメントを反転するだけ)。

3-optでは3本の辺を選ぶため、残った3つのセグメントの再接続パターンが複数存在します。

3本の辺 $(e_1, e_2, e_3)$ を削除すると、ルートは3つのセグメント $(A, B, C)$ に分かれます。
これらを再接続するパターンは(元の接続を除いて)7通りあります。

再接続パターンを図にすると以下のようになります。

three_opt_patterns2.png

(きれいに作ったつもりですが、わかりづらくなってたら申し訳ないです。)

パターン 説明
2-opt(A,B) $e_1, e_2$ の入れ替え(Bを反転)
2-opt(B,C) $e_2, e_3$ の入れ替え(Cを反転)
2-opt(A,C) $e_1, e_3$ の入れ替え(A,Bを反転)
3-opt(順) A→B→C(元と同じ向き)
3-opt(逆B) A→B反転→C
3-opt(逆C) A→B→C反転
3-opt(純) A→C→B(セグメントの順序を変える)

最後の「セグメントの順序を変える」パターンが2-optでは到達できないケースに対応しています。

Python実装

3-optの完全な実装は複雑なため、ここでは「or-optムーブ」(1都市または複数都市を別の位置に挿入する)と組み合わせたシンプルな版を示します。

import numpy as np
from itertools import combinations

def three_opt_segment(route, dist):
    """
    3-opt(セグメント順序変更を含む簡略版)
    route: 現在のルート
    dist: 距離行列
    """
    n = len(route)
    best = route[:]
    improved = True

    while improved:
        improved = False
        for i in range(n - 2):
            for j in range(i + 1, n - 1):
                for k in range(j + 1, n):
                    # 3本の辺を特定
                    a, b = best[i], best[(i + 1) % n]
                    c, d = best[j], best[(j + 1) % n]
                    e, f = best[k], best[(k + 1) % n]

                    d0 = dist[a][b] + dist[c][d] + dist[e][f]

                    # 全パターンを試す
                    seg_A = best[i + 1: j + 1]
                    seg_B = best[j + 1: k + 1]

                    # パターン1: セグメントA, B を順序変更(A→C再接続→B)
                    new_route = best[:i + 1] + seg_B + seg_A + best[k + 1:]
                    new_dist = (dist[a][seg_B[0]] + dist[seg_B[-1]][seg_A[0]]
                                + dist[seg_A[-1]][f])
                    if new_dist < d0 - 1e-10:
                        best = new_route
                        improved = True

                    # パターン2: Bを反転してセグメント順序変更
                    seg_B_rev = seg_B[::-1]
                    new_route = best[:i + 1] + seg_B_rev + seg_A + best[k + 1:]
                    new_dist = (dist[a][seg_B_rev[0]] + dist[seg_B_rev[-1]][seg_A[0]]
                                + dist[seg_A[-1]][f])
                    if new_dist < d0 - 1e-10:
                        best = new_route
                        improved = True

    return best

2-optと組み合わせる

実務では2-optで収束した後に3-optを適用するパターンが多いです。

def generate_cities(n=20, seed=42):
    np.random.seed(seed)
    return np.random.rand(n, 2) * 100

def calc_distance(cities):
    n = len(cities)
    dist = np.zeros((n, n))
    for i in range(n):
        for j in range(n):
            dist[i][j] = np.linalg.norm(cities[i] - cities[j])
    return dist

def total_distance(route, dist):
    n = len(route)
    return sum(dist[route[i]][route[(i + 1) % n]] for i in range(n))

def nearest_neighbor(dist, start=0):
    n = len(dist)
    unvisited = set(range(n))
    route = [start]
    unvisited.remove(start)
    while unvisited:
        current = route[-1]
        nearest = min(unvisited, key=lambda city: dist[current][city])
        route.append(nearest)
        unvisited.remove(nearest)
    return route

def two_opt(route, dist):
    n = len(route)
    best_route = route[:]
    improved = True
    while improved:
        improved = False
        for i in range(1, n - 1):
            for j in range(i + 1, n):
                a, b = best_route[i - 1], best_route[i]
                c, d = best_route[j], best_route[(j + 1) % n]
                if dist[a][c] + dist[b][d] < dist[a][b] + dist[c][d] - 1e-10:
                    best_route[i:j + 1] = best_route[i:j + 1][::-1]
                    improved = True
    return best_route

cities = generate_cities(n=20, seed=42)
dist   = calc_distance(cities)

# 近傍法 → 2-opt → 3-opt の順で改善
nn_route   = nearest_neighbor(dist, start=7)
opt2_route = two_opt(nn_route, dist)
opt3_route = three_opt_segment(opt2_route, dist)

print(f'近傍法:         {total_distance(nn_route, dist):.1f}')
print(f'2-opt後:        {total_distance(opt2_route, dist):.1f}')
print(f'3-opt後:        {total_distance(opt3_route, dist):.1f}')
近傍法:         497.1
2-opt後:        459.4
3-opt後:        386.6

route_nn3.png

route_2opt3.png

route_3opt3.png

計算量の比較

手法 1スキャンの計算量 特徴
2-opt $O(n^2)$ シンプル・高速
3-opt $O(n^3)$ 改善幅大きい・低速

3-optは2-optに比べて計算量が $n$ 倍増加します。都市数が100を超えてくると、3-optの完全実装は実用的でなくなってくることがあります。。。

(おまけ)実用的な工夫:Or-opt

3-optの代替として Or-opt がよく使われます。Or-optは「1〜3都市のサブシーケンスを別の位置に挿入する」操作で、3-optの一部のムーブを効率的に試すことができます。(雰囲気だけ)

def or_opt(route, dist, seg_len=1):
    """
    Or-opt:seg_len個の都市を別の位置に挿入
    seg_len: 移動するセグメントの長さ(1〜3)
    """
    n = len(route)
    best = route[:]
    improved = True

    while improved:
        improved = False
        for i in range(n):
            # セグメント [i, i+seg_len) を取り出す
            seg = [best[(i + k) % n] for k in range(seg_len)]
            seg_prev = best[(i - 1) % n]
            seg_next = best[(i + seg_len) % n]

            # セグメントを取り除いたコスト削減
            remove_gain = (dist[seg_prev][seg[0]] + dist[seg[-1]][seg_next]
                           - dist[seg_prev][seg_next])

            # 別の位置に挿入するコスト
            for j in range(n):
                if j in range(i, i + seg_len):
                    continue
                jn = (j + 1) % n
                insert_cost = (dist[best[j]][seg[0]] + dist[seg[-1]][best[jn]]
                               - dist[best[j]][best[jn]])
                if remove_gain - insert_cost > 1e-10:
                    # 挿入操作を実行
                    new_route = [c for c in best
                                 if c not in seg]
                    idx = new_route.index(best[j])
                    for k, city in enumerate(seg):
                        new_route.insert(idx + 1 + k, city)
                    best = new_route
                    improved = True
                    break

    return best

まとめ

今回は3-optを説明しました。

  • 3-opt:3本の辺を入れ替えてルートを改善する局所探索
  • 2-optで到達できない局所最適を脱出できるケースがある
  • 計算量は $O(n^3)$ で、都市数が増えると重くなる
  • 実用上は Or-opt(1〜3都市の移動)と組み合わせると効果的

次回はより高精度な局所探索であるLin-Kernighan法を説明します。

参考にした記事や本、論文等

  • S. Lin and B. Kernighan, "An Effective Heuristic Algorithm for the Traveling-Salesman Problem," Operations Research, vol. 21, no. 2, pp. 498-516, 1973.
  • G. Gutin and A. P. Punnen, eds., "The Traveling Salesman Problem and Its Variations," Springer, 2002.
3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?