地上の洞窟

どこにも行かず、液晶と「にらめっこ」し続ける人の物語。

経路探索とA*アルゴリズム

経路探索イメージ
スタートからゴールまでの最短経路を調べる

ある地点からある地点まで移動する際のルートを調べたい。
そのルートが最短であってほしい。
ゲームの自動行動や敵AIなどを作るのに使ったりする「経路探索」の考え方と「A*アルゴリズム」について解説する。

経路探索の基本

そもそも経路探索は何をするのか?

ざっくり

スタート地点から移動可能な場所を調べる。
調べた移動可能な場所から、また移動可能な場所を調べることを繰り返す。

ゴールへたどり着いたら探索完了である。
経路を作るには、ゴールへたどり着く一つ前の場所、そこからまた一つ前の場所と、逆を辿る。

こまかく

経路探索イメージ(ノード展開)
ノードの展開

調べた移動可能な場所は「ノード」と呼ばれる。
このように「調べる」ことを「ノードを展開する」とも言ったりする。
一か所を調べて得られるノードは大抵の場合一つではない。
そのため、展開したノードは何らかのデータ構造によって管理される。
配列(可変長)、キュー・スタック、二分ヒープなどだ。

ノードには以下の情報を持たせる。

  • 自ノードから他ノードを展開するための情報(X, Y座標など)
  • ゴールから逆を辿り経路を作成するための「自ノードの展開元のノードのポインタ」

他にも移動時の向きや行動内容などを含ませることがある。

一度探索した場所を再度探索することは原則行わない。
同じルートをぐるぐる調べ続けることになるためだ。

最短経路を知るには

ダイクストラ法
ダイクストラ

最短経路を知るには、展開したノードをスタートから近いものから優先して調べることが重要。
それがダイクストラである。
近いというのは移動コストのことであり、このようなグリッド構造では「歩数」ともいえる。

このコストには加算ができる。

  • 険しい山道なので移動が遅いので+1
  • 毒沼はできれば通りたくないので+100

というのが一例で、状況を考慮に入れた最短経路探索というのも出来る。

移動コストは重みとも呼ばれる。
グリッド構造で各ノード間の移動コストが均一であり、重みを考慮しなくていいという場合、先に見つけたものから優先*1して調べる「幅優先探索」でも同様に最短経路が求められる。

A*アルゴリズム

A*イメージ
ダイクストラ法+距離の予想

ダイクストラ法の移動コスト
「自ノードからゴールまでの予想距離」を加えたものが
「A*(エースター)アルゴリズムである。

評価のよい(値の低い)ノードから優先して探索が進められる。
ゴールとは反対方向も均一に調べるダイクストラ法に比べ、A*は場合によってはゴールに一直線。

評価に使う予想距離の計算式は、移動方法によっても変わる。

  • 上下左右 → |x1-x2|+|y1-y2|*2
  • 斜め移動も可 → \sqrt{x^2+y^2}*3 または \max(|x1-x2|, |y1- y2|)*4

この予想距離の計算に用いられる関数は「ヒューリスティック関数」と呼ばれる。

ノードを管理するデータ構造

youtu.be

ダイクストラ法やA*では、評価の良いノードから優先して探索していく。
そのため、複数ある探索予定のノードから最良のノードを探して取り出すことが繰り返される。

ノード管理をごく普通の配列で行えば、最良ノードを知るのには線形探索、取り出しでは大移動と効率が悪い。
そのため、最小値を一手で知れて、要素の追加・削除も高速な「ヒープ」がよく用いられる。

ヒープにはそのままノードを放り込むこともあれば、ヒープ+配列で管理することもある。
各ノードの評価値がばらける場合は前者、良くて数通りなら後者の方が効率が良いだろう。

なお

という組み合わせであれば、評価値は常に2通りとなる。
2つの配列 low・high を入れ替えながら管理すれば効率もいいし実装も簡単。

A*の手順(概略)

  • nodes → ノードを管理するデータ構造。
  • current_node → 現在のノード。展開を行う基準地点
  • goal_node → 基本は空。ゴールにたどり着いたノードを代入

nodesが空になるまでループ↓

  1. current_node = nodesから最良のノードを取り出し
  2. current_nodeをもとにノードを展開する。
    • 移動可能であること、既に展開されたことのあるノードでないことを確認。
    • ゴールに到達したノードであれば、goal_nodeに代入。
    • ノードには座標、スタートからの重みの累計(合計歩数)、親ノードなどの情報を持たせる。
    • 予想距離を計算。親の重み+親から自ノードへの重みを足したものが評価となる。
    • 評価の値を添えてnodesにノードを追加。
  3. goal_nodeが存在すればループ中断。
↑ループここまで

goal_nodeが空でなければルートを作成する。

ワンポイント
  • 調べるノードが多すぎて処理が重い場合、ノードの作成数をもとにループを打ち切らせる。
    最良ノードを記録しながら探索すれば、ある程度の精度での最短経路は見つけてくれる。
  • グリッド構造では移動時の向きを考慮すれば、ノードの展開時、調べるマスを一か所減らせる。

*1:ノードをキューで管理する

*2:マンハッタン距離・都市ブロック距離

*3:ユークリッド距離

*4:チェビシェフ距離