【最短経路問題】ダイクストラ法の計算コスト!ポケモンGOのジムバトル最短討伐時間を題材に確認しました!

ごごちと申します。

先日、ポケモンGOの最適バトルチームを最短経路問題で考えてみました。

最短経路を計算するためにダイクストラ法を使ったのですが、
計算コストが気になったので実際に確認してみました。

ポケモンGOジムバトルの最短経路問題化

ポケモンGOのジムバトルでは、
防衛ポケモンが順番に並んでいて、
攻撃側は自由にポケモンを選び闘います。

攻撃ポケモンが防衛ポケモンと相性が悪い場合、
別の攻撃ポケモンに交代した方が早く撃破できます。
ただし、交代には3秒かかります。

このような状況をグラフで表現すると
下の図のようになります。


図の例では、
防衛ポケモンとして
ハピナス→ソーナンス
攻撃ポケモンとして、
ルカリオ、ダークライ
を挙げています。
グラフをPythonなどで扱う方法としては、
隣接行列と隣接リストがあります。
どちらを使っても問題ありませんが、
隣接リストを使った方が
計算コストが小さいとされています。

前回の計算では、
18タイプの候補ポケモンに対して、
6匹の防衛ポケモンの並びを考えましたので、
全体のグラフは下の図の様になります。
ノードの数は以下の様に計算されます。
始点+終点+ 18×6 = 110
110個のノードからなるグラフですので、
110×110の隣接行列になります。

図の防衛ポケモンブロックは
対角成分に交代なしの討伐時間
非対角成分に交代ありの討伐時間
が格納されています。
隣接リストに比べて、隣接行列は
直感的にイメージしやすいのがメリットです。

計算コストについて

全組み合わせ

18匹の攻撃ポケモンと
6匹の防衛ポケモンだけですが、
全ての討伐時間の組み合わせとして

18の6乗 = 34,012,224 通り

計算コストとしては
O(10^6)
と考えられます。

ダイクストラ法

ノードの数をm, 辺の数をnとすると、
ダイクストラ法の計算コストは
m + n × log(n)
で計算されます。
今回の場合、m = 110, n = 1,656なので、
計算コストは ~2,700であり、
O(10^3)
と考えられます。

つまり、ダイクストラ法を使うと、
全組み合わせの計算よりも1,000倍
少ないコストで計算できるはずです。

実行時間の比較

実際にPythonで計算した結果が上の図です。
全組み合わせを計算した場合、
12,900 ms(12秒)ほどで計算が終了しました。
隣接行列でダイクストラ法の計算をした場合、
10 msとほぼ一瞬で計算が終了しました。
さきほど見積もったように、
ダイクストラ法は全組み合わせよりも
1,000倍短い計算時間となることが確認できました。

さらに隣接リストを使うと、隣接行列の場合よりも
計算時間が2倍短くなりました。

アルゴリズムによる時間短縮の効果が実感できます。

実行コード

import sys

def dijkstra(graph, start):
    # ノード数
    num_nodes = len(graph)
    
    # 初期化
    distances = [sys.maxsize] * num_nodes  # 各ノードまでの最短コスト
    visited = [False] * num_nodes  # ノードの訪問状態
    distances[start] = 0  # スタートノードの距離は0
    parent = [-1] * num_nodes  # 最短経路木における各ノードの親ノード
    
    for _ in range(num_nodes):
        # 未訪問のノードの中で最短コストのノードを選択
        min_distance = sys.maxsize
        min_node = -1
        for i in range(num_nodes):
            if not visited[i] and distances[i] < min_distance:
                min_distance = distances[i]
                min_node = i
        
        # 選択したノードを訪問済みとする
        visited[min_node] = True
        
        # 選択したノードからの移動先のコストを更新
        for i in range(num_nodes):
            if (not visited[i]) and graph[min_node][i] != float('inf'):
                new_distance = distances[min_node] + graph[min_node][i]
                if new_distance < distances[i]:
                    distances[i] = new_distance
                    parent[i] = min_node
    
    # 最短経路上のノードを取得
    path = []
    node = num_nodes - 1  # ゴールノードから逆にたどる
    while node != -1:  # スタートノードに到達するまで親ノードをたどる
        path.append(node+1)
        node = parent[node]
    
    path.reverse()  # スタートからゴールまでの順序にする
    
    return distances, path

# 隣接行列を作成
graph = [[0, 30, 60, float('inf'), float('inf'), float('inf')],
         [float('inf'), 0, float('inf'), 40, 17+3, float('inf')],
         [float('inf'), float('inf'), 0, 40+3, 17, float('inf')],
         [float('inf'), float('inf'), float('inf'), 0, float('inf'), 0],
         [float('inf'), float('inf'), float('inf'), float('inf'), 0, 0],
         [float('inf'), float('inf'), float('inf'), float('inf'), float('inf'), 0]]

start_node = 0  # スタートノード
target_node = 5  # ゴールノード

distances, path = dijkstra(graph, start_node)

# スタートノードからゴールノードまでの最短コストを表示
print("最短コスト:", distances[target_node])

# スタートノードからゴールノードまでの最短経路を表示
print("最短経路:", "->".join(str(node) for node in path))

最初の図を再現するためのコードが上記のものです。
実行すると最短コストと
通ったノードが出力されます。

最適化計算は奥が深い

まだまだ最適化計算の素人ですが、
ポケモンGOを題材にして楽しく勉強しています。
本業でも回帰モデルの特徴量組合せを
最適化する機会があったりするので、
これからも最適化計算を
学んでいきたいと思います。

1 COMMENT

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA