Javaの迷路ソルバー

Javaの迷路ソルバー

1. 前書き

この記事では、Javaを使用して迷路をナビゲートするための可能な方法を探ります。

迷路は白黒の画像であると考えてください。黒いピクセルは壁を表し、白いピクセルはパスを表します。 2つの白いピクセルは特別で、1つは迷路への入り口、もう1つは出口です。

このような迷路を考えると、入り口から出口までのパスを見つけたいと思います。

2. 迷路のモデリング

迷路は2D整数配列と見なします。 配列内の数値の意味は、次の規則に従います。

  • 0→道路

  • 1→壁

  • 2→迷路エントリー

  • 3→迷路出口

  • 4→入口から出口までの経路のセル部分

We’ll model the maze as a graph。 入り口と出口は2つの特別なノードであり、その間のパスが決定されます。

典型的なグラフには、ノードとエッジの2つのプロパティがあります。 エッジはグラフの接続性を決定し、あるノードを別のノードにリンクします。

したがって、各ノードから4つの暗黙的なエッジを想定し、特定のノードをその左、右、上、下のノードにリンクします。

メソッドシグネチャを定義しましょう:

public List solve(Maze maze) {
}

メソッドへの入力は、上記で定義された命名規則の2D配列を含むmaze,です。

メソッドの応答はノードのリストであり、エントリノードから出口ノードへのパスを形成します。

3. 再帰的バックトラッカー(DFS)

3.1. アルゴリズム

かなり明白なアプローチの1つは、考えられるすべてのパスを探索することです。これにより、パスが存在する場合は最終的に検出されます。 しかし、そのようなアプローチには指数関数的な複雑さがあり、うまくスケールしません。

ただし、訪問したノードをバックトラックしてマークすることにより、上記のブルートフォースソリューションをカスタマイズして、妥当な時間内にパスを取得することができます。 このアルゴリズムは、Depth-first searchとも呼ばれます。

このアルゴリズムの概要は次のとおりです。

  1. 壁にいる場合、またはすでにアクセスしたノードにいる場合は、失敗を返します

  2. それ以外の場合、私たちが出口ノードである場合は、成功を返します

  3. それ以外の場合は、ノードをパスリストに追加し、4方向すべてに再帰的に移動します。 失敗が返された場合、パスからノードを削除し、失敗を返します。 出口が見つかった場合、パスリストには一意のパスが含まれます

このアルゴリズムを図1(a)に示す迷路に適用してみましょう。ここで、Sは開始点、Eは終了点です。

各ノードについて、右、下、左、上という順序で各方向を走査します。

1(b)では、パスを探索して壁にぶつかります。 次に、壁に隣接しないノードが見つかるまでバックトラックし、1(c)に示すように別のパスを探索します。

1(d)に示すように、再び壁にぶつかり、プロセスを繰り返して最終的に出口を見つけます。

imageimage imageimage

3.2. 実装

Javaの実装を見てみましょう。

まず、4つの方向を定義する必要があります。 これを座標で定義できます。 これらの座標を任意の座標に追加すると、隣接する座標のいずれかが返されます。

private static int[][] DIRECTIONS
  = { { 0, 1 }, { 1, 0 }, { 0, -1 }, { -1, 0 } };

また、2つの座標を追加するユーティリティメソッドも必要です。

private Coordinate getNextCoordinate(
  int row, int col, int i, int j) {
    return new Coordinate(row + i, col + j);
}

これで、メソッドシグネチャsolve.The logic here is simpleを定義できます。入口から出口までのパスがある場合はパスを返し、そうでない場合は空のリストを返します。

public List solve(Maze maze) {
    List path = new ArrayList<>();
    if (
      explore(
        maze,
        maze.getEntry().getX(),
        maze.getEntry().getY(),
        path
      )
      ) {
        return path;
    }
    return Collections.emptyList();
}

上で参照したexploreメソッドを定義しましょう。 パスがある場合は、引数pathに座標のリストを指定してtrueを返します。 このメソッドには3つのメインブロックがあります。

まず、無効なノード、つまり 迷路の外にあるノードまたは壁の一部であるノード。 その後、同じノードに何度もアクセスしないように、現在のノードにアクセス済みのマークを付けます。

最後に、出口が見つからない場合は、すべての方向に再帰的に移動します。

private boolean explore(
  Maze maze, int row, int col, List path) {
    if (
      !maze.isValidLocation(row, col)
      || maze.isWall(row, col)
      || maze.isExplored(row, col)
    ) {
        return false;
    }

    path.add(new Coordinate(row, col));
    maze.setVisited(row, col, true);

    if (maze.isExit(row, col)) {
        return true;
    }

    for (int[] direction : DIRECTIONS) {
        Coordinate coordinate = getNextCoordinate(
          row, col, direction[0], direction[1]);
        if (
          explore(
            maze,
            coordinate.getX(),
            coordinate.getY(),
            path
          )
        ) {
            return true;
        }
    }

    path.remove(path.size() - 1);
    return false;
}

このソリューションでは、迷​​路のサイズまでのスタックサイズを使用します。

4. バリアント–最短パス(BFS)

4.1. アルゴリズム

上記の再帰的アルゴリズムはパスを見つけますが、必ずしも最短パスであるとは限りません。 最短経路を見つけるために、Breadth-first searchとして知られる別のグラフ走査アプローチを使用できます。

DFSでは、1人の子供とそのすべての孫が最初に探索された後、別の子供に移りました。 Whereas in BFS, we’ll explore all the immediate children before moving on to the grandchildren.これにより、親ノードから特定の距離にあるすべてのノードが同時に探索されるようになります。

アルゴリズムの概要は次のとおりです。

  1. 開始ノードをキューに追加します

  2. キューは空ではありませんが、ノードをポップし、以下を実行します。

    1. 壁に到達するか、ノードが既に訪問されている場合は、次の反復にスキップします

    2. 終了ノードに到達した場合、現在のノードから開始ノードまでバックトラックして最短パスを見つけます

    3. そうでない場合は、キュー内の4つの方向にあるすべての直接の近隣を追加します

One important thing here is that the nodes must keep track of their parent, i.e. from where they were added to the queue.これは、出口ノードが検出されたときにパスを見つけるために重要です。

次のアニメーションは、このアルゴリズムを使用して迷路を探索するときのすべての手順を示しています。 次のレベルに移動する前に、同じ距離にあるすべてのノードが最初に探索されることがわかります。

image

4.2. 実装

Javaでこのアルゴリズムを実装しましょう。 前のセクションで定義したDIRECTIONS変数を再利用します。

最初に、特定のノードからそのルートまでバックトラックするユーティリティメソッドを定義します。 出口が見つかったら、これを使用してパスをトレースします。

private List backtrackPath(
  Coordinate cur) {
    List path = new ArrayList<>();
    Coordinate iter = cur;

    while (iter != null) {
        path.add(iter);
        iter = iter.parent;
    }

    return path;
}

コアメソッドsolve.を定義しましょう。DFS実装で使用される3つのブロックを再利用します。 ノードを検証し、訪問したノードをマークし、隣接ノードを横断します。

少し変更を加えます。 再帰的トラバーサルの代わりに、FIFOデータ構造を使用してネイバーを追跡し、それらを反復処理します。

public List solve(Maze maze) {
    LinkedList nextToVisit
      = new LinkedList<>();
    Coordinate start = maze.getEntry();
    nextToVisit.add(start);

    while (!nextToVisit.isEmpty()) {
        Coordinate cur = nextToVisit.remove();

        if (!maze.isValidLocation(cur.getX(), cur.getY())
          || maze.isExplored(cur.getX(), cur.getY())
        ) {
            continue;
        }

        if (maze.isWall(cur.getX(), cur.getY())) {
            maze.setVisited(cur.getX(), cur.getY(), true);
            continue;
        }

        if (maze.isExit(cur.getX(), cur.getY())) {
            return backtrackPath(cur);
        }

        for (int[] direction : DIRECTIONS) {
            Coordinate coordinate
              = new Coordinate(
                cur.getX() + direction[0],
                cur.getY() + direction[1],
                cur
              );
            nextToVisit.add(coordinate);
            maze.setVisited(cur.getX(), cur.getY(), true);
        }
    }
    return Collections.emptyList();
}

5. 結論

このチュートリアルでは、迷路を解決するための2つの主要なグラフアルゴリズムである深さ優先探索と幅優先探索について説明しました。 また、BFSが入口から出口までの最短経路をどのように与えるかについても触れました。

さらに読むには、A *やダイクストラアルゴリズムなど、迷路を解決する他の方法を調べてください。

いつものように、完全なコードはover on GitHubで見つけることができます。