最短経路を探すという場合、いくつか方法が考えられます。 もし、スタートからゴールに至るすべての経路がリストアップできるなら、 それぞれで経路長を算出し、最小値をとる経路を選べば良いわけです。 しかし、この方法では、すべての経路をリストアップする、という 過程が非常に面倒なものとなります(京都市のような町なら...)。 そこで、ダイクストラ法と呼ばれる方法を使うことにします。
最短経路を求めるために、まず、各交差点からゴールまでの 最短距離を求めます。その次にそれをもとに最短経路を求めます。
最短経路を求める前に、ゴールまでの最短距離がわかると、なぜ 最短経路が求まるかを説明しておきましょう。
ある交差点[A]から、ゴール[G]までの最短経路を求めたいとします。 交差点[A]から道路がつながっている交差点として [B] [C] [D]が あり、
[B(8.3)]- .... 1.2 / 1.0 [D(10.5)]----[A(9.3)] .... [G(0.0)] \ 1.6 [C(7.9)]- .... |
という位置関係だったとします。ここで、[A(9.3)]の 9.3 は
ゴールまでの最短距離、結んだ線は
道路でその近傍の数値は道路の距離とします。
さて、[A]から[G]に行くにはどの交差点に行けばいいでしょうか。
遠くなる(9.3→10.5) [D]に行かないのは確かとして、[B],[C]の
選択です。一見[C]に行くと 8.3>7.9 で近そうですが、実際には
[B]を通ると 8.3+1.0=9.3, [C]を通ると 7.9+1.6=9.5 で [C]に
行くとすこし遠回りです。この方法を一般的な表現で言うと
「隣接する交差点からゴールまでの最短経路と、その交差点までの
距離の和が、最小となる交差点にすすめばよい」ということになります(※)。
このように各交差点のゴールまでの最短経路を求めることができると、 最短経路の探索ができます。そこで、次に、各交差点からゴールまでの 最短経路を求める方法を考えることにします。
※ 一見すると「隣接する交差点からゴールまでの最短経路と、その交差点までの 距離の和が、『今いる交差点のゴールまでの最短経路』と等しい」としても いいように思えます。しかし、コンピュータで小数変数を使う場合に、 ごく微少な誤差のために、直接代入した場合を除き『等しい』は成立しない 可能性が生じます。特に演算結果を比較する場合などは要注意です。 そのため『最小』という表現、プログラムにしたほうが安全です。 どうしても、『等しい』ことを 判定したい場合の手段としては、差の絶対値が十分小さい、などの評価を プログラムで行うことになります。
まず、第一段階として、各交差点からのゴールまでの最短距離を求めましょう。 そのために、本演習で推している方法は、このような場合に一般的に 用いられる Dijkstra(ダイクストラ)法です。
ダイクストラ法では、交差点を2つのグループに分けます。一つは 基準交差点(ここではゴール)までの最短距離が確定している交差点群、 もう一つは未確定の ものです。最初はすべての交差点が未確定ですが、1つずつ確定していき、 最後にすべての交差点を確定します。確定した交差点集合をV、未確定の 交差点集合をUをします。
それぞれの交差点には、基準交差点までの、現在のところわかっている 最短経路の距離を表す変数 distance を持たせます。この distanceは Vに含まれる 交差点では真の最短距離になりますが(つまり、全部Vに入った段階で 全部真の最短距離)、Uに含まれる交差点ではそうとは限りません。
さて、ある時点でこのように交差点がU・Vに分かれているときに、
Uから交差点をVに移していきます(Vは確定しているので、
もうなにもしない)。まず、Uに含まれるそれぞれの交差点の distance を
計算します。distance は
途中でVに属する、最短距離確定済の交差点のみを通る最短経路での
距離とします。このことは、Vに隣接する交差点のみが distance を
計算できる、ということです。またV内の交差点の複数とつながって
いる場合は、当然最も経路が短くなる交差点経由で計算します。
Vに隣接していない交差点は対象外とします。
この distance の定まった交差点の中から、distance が最小である
交差点を選びます(等しいのが二つ以上ある場合は適当にどれかひとつ)。
この交差点は以下に述べる理由で、Vに移してしまって構いません。
この手順を繰り返すことで、一つずつ交差点をUからVに移していくと、
最後にすべての交差点で最短距離が確定します。
さて、distance が最低の交差点をなぜ、Vに移しても構わないか、 ということは、よく考えてみると、実は単純な話です。 distance が最低の交差点の distance を d1, それ以外に適当に 選んできたU内の交差点のそれを d2 とします。この選び方では、 必ず d2>=d1 です(等しいものがあった場合を考え、=も含めておきます)。 今回は交差点間の距離を用いたので、その間の距離 l は 0より大です。 となると、適当に選んだ交差点を通ってdistance最低の交差点に 至る経路の長さは d2+l > d1 です。 これは常に成り立つので、d1 は 最初に選んだ交差点の「真に基準交差点までの 最短経路の距離」になっています(それ以外の道を通っても遠くなるだけ)。 なので、Vに移して構いません。 このことは、先ほど対象外とした、隣接しない交差点の場合も当てはまります。 最短と分かっている交差点群にも隣接しないような交差点 (=かならずどこかのV隣接交差点 は通過する)を経由したら、ますます遠くなりますので。
また、適当に選んだ交差点の distance が最短ではない可能性の 有無ですが、これは有り得ます。ある交差点の、 Vにつながる道路が実はぐにゃぐにゃの山道、 ところがすぐとなりの交差点からVにつながる道路はトンネルで一直線、 だったら迷わずトンネルを通りますよね(紅葉を見たい、とかでなければ)。 このように、Vに隣接していても、別の交差点を経由したほうが近い、 という場合は有り得ます。
さて、これをプログラムにしていきましょう。ここでポイントは、 U/Vの区別をする部分と、Uの中で、distance が定まらない交差点を 除外する方法、distance の算定方法です。
U/Vを区別するには、俗に「フラグ」と呼ばれる変数を用意します。 フラグというのは、ある状態が満たされたかどうかを記憶する ための変数を指す言葉で、大抵は0と0以外(1など)が代入されます。 ゲームなどでも「フラグを立てる」などといいますが、ゲームのプログラムを つくる側の用語がそのまま使われているものと言えます。 つまり、最初は各交差点のフラグをクリアしておき(Uを意味する)、 確定するときにフラグをセットする(Vを意味する)ことで区別する わけです。これは if 文で簡単にわかります。
つぎに distance が定まらない交差点の扱いです。これもフラグを別に
作っても良いのですが、今回は
distance に有り得ないような大きな値を代入することにします。
というのも、distance を決定した後で「distanceが最小のものを選ぶ」
ため、distanceを大きな値にしておけば選ばれる心配がない=除外したのと
等価になります。
しかも、このdistanceは
という特徴があります。よって、最初に大きな値にしておいて、
交差点を一個確定する毎に隣接する交差点のdistanceを更新する
だけで、上に述べた「Vを経由した最短経路の長さ」が算出される
ことになります。ここのところを良く考えてみてください。
ただし、例外的に、基準交差点の距離だけ、0にします。物理的には
当たり前ですが、プログラムとしても、これによって、最初にこの交差点が
確定され、それに隣接する交差点のdistanceが計算され...と
プログラムがスタートします。
(このような繰り返しアルゴリズムの場合、たいてい最初の1回目は
例外的なスタート処理が必要です。如何に定常状態に載せるかも
プログラムをつくる上で重要なテクニックです。ループ終了も同様。)
この案をもとにより詳細にプログラムを考えていくと、
となります。
さて、実際にプログラムはどう進むかというと、1週目は当然、 唯一0を設定したゴールの交差点が確定し、それに隣接する交差点の distance が(大きな数から最短距離に)更新されます。そのなかで、最も 数値の小さいものは、上の理論によって2週目に確定します。 以下、その操作が続きます。
以上がダイクストラ法なのですが、{はっきり|なんとなく}わかりますか?
理解した上でさらに元気のある人は、是非高速化に挑戦してみてください。 いまの方法では、distance に大きな数値を設定する、という手法で 「有効な交差点」と「対象外の交差点」を区別しました。ということは、 交差点数が莫大になったとき、莫大な「対象外の交差点」も調べることに なります。これを何とかして調べないですむようにしたら、速くなることは 期待できます。ただ、フラグを追加するくらいの一筋縄ではいきませんよ、 たぶん。
より直感的なのですが、時間のかかる方法もお話ししておきましょう。 その方法は、用意する変数はdistanceにあたる数値を格納しておくためだけの ものです。フラグは終了を判定するためだけに、用意します。
というものです。各交差点は常に隣接交差点の距離をみて、最短距離を 決定します。どこか一カ所でも距離が書きかわるとどこかに影響が でることが考えられるため、全交差点を洗い直す必要があります。 更新がなくなったということは、すべての交差点が最短距離を知ったことに なります。
前回の最後にやり残した方法です。いっそのこと、各交差点の ゴールまでの最短距離を途中の経路を無視して、直線距離で結ぶと どうなるでしょう。プログラムは非常に簡単になりますが、 京都のような碁盤の目の町ならともかく、複雑な地図上では、 はまってしまいます。人間なら、もと来た道をもどったりは しないのですが、単に距離だけをみて動くようなプログラムをつくると、 行ったり来たりすることになります。
さて、以上のように、各交差点からゴールまでの最短経路を通ったときの 距離は求まりました。一番最初に説明した方法を用いることで、 最短経路は求まることでしょう。
ただ、気づいた人も多いと思うのですが、最短経路を出すには、 もう少しスマートかもしれない方法があります。最短距離を求める 段階で、「どの交差点に向かえば最短経路か」はわかっています (最短距離の更新をする段階で)。変数を1種類増やして、 どの交差点にいけばいいかを残しておけば、距離をいじる必要も なくなります。変数を一つ増やすか、すこし複雑な方法にするか、 それはプログラマの考え方とも言えます。