ごごちと申します。
先日、ポケモン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を題材にして楽しく勉強しています。
本業でも回帰モデルの特徴量組合せを
最適化する機会があったりするので、
これからも最適化計算を
学んでいきたいと思います。
ポケモンGOで楽しくなりそうですね^ ^