マガジン

最短経路はどう見つけるか:月夜の配達のヒントを手で計算する

遊びをつくる

執筆にClaudeを使用しています。

月夜の街を結ぶ配達経路と、手前に置かれた自転車と三つの小包。
記事のテーマをもとに制作したAI生成イラスト。

「月夜の配達」で、ヒントが表示している「残り最短距離」の数字と、自分が頭の中で数えた区間数が食い違う場面を考えてみます。数字を信じて進めば合っているはずなのに、なぜその数字になるのかが分かりません。この記事では、その数字を自分の手で作れるようになることを目指します。 使うのは幅優先探索(BFS)という考え方です。出発点から「1区間で行ける場所」「2区間で行ける場所」と、同心円を広げるように順番に調べていきます。燃料が1区間で1ずつ減る世界では、この方法で最短が求まります。 先に断っておくと、月夜の配達の内部実装は確認済みの情報にありません。そのため、以下で扱う地図は記事用の小さな架空の地図で、作品の面とは関係がありません。作品から言えるのは、公開されている仕様から読み取れる範囲の「設計から読み取れること」までです。ここで手順を身につけたら、最後に作品を開いて自分の目で突き合わせてください。

この記事の目次
  1. 「最短」で数えているのは何か
  2. 手計算用の小さな架空の地図
  3. 幅優先探索を一歩ずつ進める
  4. ゴールから数えると、ヒントが作れる
  5. 小さなコードで確かめる
  6. 起こりやすい誤りと、状態が増える場合
  7. 次の行動

「最短」で数えているのは何か

月夜の配達の確認済みの仕様は、小包が3つ、バッグが2枠、燃料は有限で、1区間進むと燃料が1減る、というものです。夜は3つあり、残り最短距離を示すヒント、次の一歩を照らすヒント、一歩戻る操作があります。

ここで重要なのは「1区間で燃料1」という点です。どの区間も同じ重さなので、最短とは「通る区間の数がいちばん少ない道」を意味します。道のりの長さや曲がり角の数は関係ありません。区間ごとに重さが違うと、BFSだけでは足りず、重みを足し合わせる別の方法(ダイクストラ法など)が必要になります。全部が同じ重さだからこそ、BFSが素直に使えます。

整理すると、次の2つが出発点です。

  • 数える単位は「区間」で、すべて同じ重さです。
  • 最短とは、区間数が最小の道のことです。

「最短」を考えるときは、まず何を1と数えるのかを確かめます。この確認が、次に作る地図の前提になります。

手計算用の小さな架空の地図

練習用に、8つの地点A〜Hを使います。つながりは次の10本の道で、どれも行き来できます。

  • A–B、A–C
  • B–D、C–D、C–E
  • D–F、E–F、E–G
  • F–H、G–H

出発点はA、目的地はHです。紙に8つの丸を書いて線を引くと、AからDとEのあたりで道が合流し、FとGを経てHに集まる形になります。

見ただけで最短が分かるでしょうか。AからHまで行く道は複数あって、どれも似た長さに見えます。ここで「たぶんこれ」と目で選ぶのではなく、手順で確実に求めるのが今回の練習です。

幅優先探索を一歩ずつ進める

手順は3つだけです。

  1. 出発点に距離0を書き、これを「待ち行列」の先頭に入れます。待ち行列は、順番待ちの列のことです。
  2. 列の先頭を取り出し、そこから行ける隣の地点のうち、まだ距離が書かれていない地点に「取り出した地点の距離+1」を書いて、列の後ろに並べます。
  3. 列が空になるまで2を繰り返します。

地図で実際に進めてみます。

  • 距離0、新しく距離が付く地点:A、由来:出発点です。
  • 距離1、新しく距離が付く地点:B、C、由来:Aの隣です。
  • 距離2、新しく距離が付く地点:D、E、由来:BからD、CからDとEです。
  • 距離3、新しく距離が付く地点:F、G、由来:DからF、EからFとGです。
  • 距離4、新しく距離が付く地点:H、由来:FからH、GからHです。

途中の細部も確かめておきます。距離2の層でBからDに進むとき、CからもDに届きますが、Dにはすでに距離が書かれているので、書き直しません。同じ地点を二度扱わないのが、この方法の肝です。

結論として、AからHの最短は4区間です。実際に4区間の道を探すと、A–B–D–F–H、A–C–D–F–H、A–C–E–F–H、A–C–E–G–Hの4本があり、同じ4区間になります。5区間以上かかる道はありますが、より短い道は存在しません。Hに隣り合うのはFとGだけで、どちらも距離3なので、Hは4より小さくなりようがないからです。

この「層ごとに広げる」という性質のおかげで、最初にゴールに届いた時点でそれが最短だと言い切れます。燃料が5あれば届き、3しかなければ、この地図ではどう進んでも届かないことも、手計算の時点で分かります。

ゴールから数えると、ヒントが作れる

ここからが「ヒント」の話です。上の計算は出発点から数えましたが、ゴールから逆向きに数える方法もあります。道は行き来できるので、結果は同じ形になります。ゴールHを距離0として、同じ手順で進めます。

  • H:0
  • F、G:1
  • D、E:2(DはF、EはFとGの隣です)
  • B、C:3(BはD、CはDとEの隣です)
  • A:4

こうして全地点に「ゴールまでの残り区間数」が付きます。この表があれば、2つのことが簡単に分かります。

  • 残り最短距離は、今いる地点の数字を読むだけです。Aにいれば4です。
  • 次の一歩は、隣の地点のうち数字が1小さいところを選べば決まります。Aの隣はBとCで、どちらも3なので、どちらに進んでも最短のままです。

月夜の配達の確認済みの仕様に、残り最短距離を示すヒントと、次の一歩を照らすヒントがあります。この2つは、上のような「ゴールまでの残り区間数の表」から自然に導けるものです。ただし、作品が実際にそう実装されているかは、確認済みの情報にはありません。ここでは「設計から読み取れること」として、2つのヒントが同じ種類の情報(残り区間数)から作れる、というところまでにとどめます。

作品で次の点を確かめると、仕組みの予想が当たっているか分かります。

  • 寄り道をして一歩戻ったとき、残り最短距離の数字は、戻る前の地点の数字に戻るでしょうか。
  • 次の一歩の候補が2つ以上あるとき、照らされるのは1つでしょうか、複数でしょうか。
  • 道を外れて燃料が減ったとき、残り最短距離の数字と残り燃料の関係はどう見えるでしょうか。

記録は、ヒントを使う前に自分で数えた区間数と、表示された数字を、夜ごとに紙に並べるのが確実です。各夜の最少区間数の記録は、プレイ中だけ残る仕様で、画面を閉じると消えます。比べるなら、その場でメモを取ってください。

小さなコードで確かめる

手計算の結果は、コードでも再現できます。次のHTMLはそのまま保存してブラウザで開けば動き、結果はコンソールに出ます。

html
<script>
const edges = "A-B A-C B-D C-D C-E D-F E-F E-G F-H G-H".split(" ");
const g = {};
for (const e of edges) {
  const [a, b] = e.split("-");
  (g[a] ??= []).push(b);
  (g[b] ??= []).push(a);
}
function distancesFrom(goal) {
  const dist = { [goal]: 0 };
  const queue = [goal];
  while (queue.length) {
    const now = queue.shift();
    for (const next of g[now]) {
      if (next in dist) continue;
      dist[next] = dist[now] + 1;
      queue.push(next);
    }
  }
  return dist;
}
const d = distancesFrom("H");
console.log(d);
const here = "A";
console.log(g[here].filter(n => d[n] === d[here] - 1));
</script>

コンソールに出る1つ目は、各地点の残り距離です。手計算の表と同じ値になっているはずです。2つ目は、Aから見て残りが1小さい隣の地点で、先ほどの手計算ならBとCの両方になります。

値が食い違ったときは、辺の書き間違いを疑ってください。edges の文字列を1本ずつ地図と照らすと、たいてい原因が見つかります。

自分の地図に置き換えるには、edges を書き換えるだけです。ゲームの面を作る人は、道の本数が増えても同じコードで残り距離が出せます。

起こりやすい誤りと、状態が増える場合

手計算でつまずきやすい点を挙げます。

  • 距離が書かれた地点を、あとから別の経路で書き直してしまいます。書き直さないのが決まりです。
  • 層の順番を飛ばします。距離2の地点をすべて処理する前に距離3へ進むと、最短が崩れます。
  • 行き来できない道(一方通行)を、両方向に書いてしまいます。道の向きは地図に合わせて決めます。

もう1つ、ゲームならではの落とし穴があります。小包を拾ったり届けたりする規則がある場合、「どこにいるか」だけでは状況を表せないことがあります。たとえばバッグに何が入っているか、どの小包を配達済みかが先の展開を左右するなら、探索の対象は「地点」ではなく「地点とバッグの中身の組」になります。この組を一つの状態として、同じ手順で層を広げていきます。

月夜の配達のバッグは2枠、小包は3つと、持てる数より運ぶ数のほうが多い仕様です。そのため、最短を考えるときに状態を増やす必要があるかもしれません。ただし、小包の受け取りと配達の細かな規則は、確認済みの情報にありません。この部分は作品を開いて、自分で試して確かめてください。

次の行動

まず、この記事の架空の地図で、距離の表を紙に書いて自力で再現してください。次に、月夜の配達を開き、ヒントを使う前に自分で区間数を数えて、表示された残り最短距離と見比べてみてください。ゲームを作る人は、自作の面の道を edges に書き写し、distancesFrom で残り距離の表を出してみると、ヒント機能の土台になります。