0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

再帰的バックトラッキングによる迷路生成

0
Last updated at Posted at 2025-05-30

はじめに

自分は小学生の頃、自由帳に書くものといえば、キン肉マンか迷路。これが2大コンテンツでした。ある日「惡魔の迷図」(悪魔の迷図)(DEVIL'S MAZES)で遊んで(買ってもらったのか、ねだったのか、兄か親かも、おぼえてないけど)これは本当に楽しかった。

悪魔の迷図とは巨大なA1サイズの巨大迷路で、解くと絵が出るやつです。
ほどなく、書店で見かけることはなくなり(自分じゃないけど)、待望する気持ちだけが子供心に残りました。
何種類かやったけど、いま調べた感じで最低でも6種類(2520/3551/4335/8311/8693/4438)、推測では25種類あるみたいですね。

image.png
https://www.kosho.or.jp/products/detail.php?product_id=282227072
https://note.com/shiroyukimin/n/n69334f087d14

やっぱり、自分は迷路好き、ということの再確認。

迷路プログラム

題材として、定番でしょうか。大別して、穴掘り法と、壁生成法があった気がします。

経緯のメモ

上のサイトから、以下のローグライクダンジョン生成サイトを知り、

以下の迷路アルゴリズム一覧サイトを知り、

以下の再帰的バックトラッキングアルゴリズムをたどりました。
様々な迷路生成アルゴリズムが紹介されている中で、一番美しく感じました。

そして、AIに変換してもらったものがこちらです。
テキスト出力版をグラフィカルにして、さらに、アニメーションも。
AIは、こちらの心を見透かして、先回りしてくれました。

再帰的バックトラッキングによる迷路生成

image.png

int cols, rows;
int w = 40;
Cell[][] grid;
ArrayList<Cell> stack = new ArrayList<Cell>();

void setup() {
  size(800, 800);
  frameRate(60);
  cols = floor(width / w);
  rows = floor(height / w);
  grid = new Cell[cols][rows];

  // グリッド初期化
  for (int j = 0; j < rows; j++) {
    for (int i = 0; i < cols; i++) {
      grid[i][j] = new Cell(i, j);
    }
  }

  grid[0][0].visited = true;
  stack.add(grid[0][0]);
}

void draw() {
  background(255);
  
  // グリッド描画
  for (int j = 0; j < rows; j++) {
    for (int i = 0; i < cols; i++) {
      grid[i][j].show();
    }
  }

  // 探索処理
  if (!stack.isEmpty()) {
    Cell current = stack.get(stack.size() - 1);
    Cell next = current.checkNeighbors();
    if (next != null) {
      next.visited = true;
      stack.add(next);
      removeWalls(current, next);
    } else {
      stack.remove(stack.size() - 1);
    }
  }
}

// 壁を削除
void removeWalls(Cell a, Cell b) {
  int x = a.i - b.i;
  int y = a.j - b.j;
  if (x == 1) {
    a.walls[3] = false;
    b.walls[1] = false;
  } else if (x == -1) {
    a.walls[1] = false;
    b.walls[3] = false;
  } else if (y == 1) {
    a.walls[0] = false;
    b.walls[2] = false;
  } else if (y == -1) {
    a.walls[2] = false;
    b.walls[0] = false;
  }
}

// セルクラス
class Cell {
  int i, j;
  boolean[] walls = { true, true, true, true }; // top, right, bottom, left
  boolean visited = false;

  Cell(int i, int j) {
    this.i = i;
    this.j = j;
  }

  void show() {
    int x = i * w;
    int y = j * w;
    stroke(0);
    if (walls[0]) line(x, y, x + w, y);
    if (walls[1]) line(x + w, y, x + w, y + w);
    if (walls[2]) line(x + w, y + w, x, y + w);
    if (walls[3]) line(x, y + w, x, y);
    if (visited) {
      noStroke();
      fill(200, 0, 255, 100);
      rect(x, y, w, w);
    }
  }

  Cell checkNeighbors() {
    ArrayList<Cell> neighbors = new ArrayList<Cell>();
    if (i > 0) {
      Cell left = grid[i - 1][j];
      if (!left.visited) neighbors.add(left);
    }
    if (i < cols - 1) {
      Cell right = grid[i + 1][j];
      if (!right.visited) neighbors.add(right);
    }
    if (j > 0) {
      Cell top = grid[i][j - 1];
      if (!top.visited) neighbors.add(top);
    }
    if (j < rows - 1) {
      Cell bottom = grid[i][j + 1];
      if (!bottom.visited) neighbors.add(bottom);
    }

    if (neighbors.size() > 0) {
      int r = int(random(neighbors.size()));
      return neighbors.get(r);
    } else {
      return null;
    }
  }
}

追記 2025/11/02

メモ(うまくいってない)
1,ハミルトン路問題
NP問題になり、正攻法では解決は困難。

2,パリティの説明
市松模様に塗った場合、数が同じでなければならない。

3,constraint-based maze
要文献調査

image.png

int cols, rows;
int w = 40;
Cell[][] grid;
ArrayList<Cell> stack = new ArrayList<Cell>();

boolean[][] mask;     // 黒Zだけtrue(通ってよい)
Cell[] waypoints = new Cell[4];

void setup() {
  size(800, 800);
  frameRate(60);

  cols = width / w;
  rows = height / w;

  // グリッド初期化
  grid = new Cell[cols][rows];
  for (int j = 0; j < rows; j++) {
    for (int i = 0; i < cols; i++) {
      grid[i][j] = new Cell(i, j);
    }
  }

  // 1. マスク読み込み(黒だけ通る)
  mask = loadMask("z.png", cols, rows);

  // 2. 中継地点をマスク上にスナップしておく
  waypoints[0] = snapToMask(0, 0);                 // (0,0)
  waypoints[1] = snapToMask(cols-5, 3);            // (39,0)
  waypoints[2] = snapToMask(4, rows-4);            // (0,39)
  waypoints[3] = snapToMask(cols-1, rows-1);       // (39,39)

  // 3. (0,0) → (39,0) を「DFS迷路」で掘る
  carveSegmentDFS(waypoints[0], waypoints[1]);

  // 4. (39,0) → (0,39)
  carveSegmentDFS(waypoints[1], waypoints[2]);

  // 5. (0,39) → (39,39)
  carveSegmentDFS(waypoints[2], waypoints[3]);

  // 6. 残りを埋めるために最後の地点から通常DFSを続ける
  waypoints[3].visited = true;
  stack.add(waypoints[3]);
}

void draw() {
  background(255);

  // 描画
  for (int j = 0; j < rows; j++) {
    for (int i = 0; i < cols; i++) {
      grid[i][j].show();
    }
  }

  // 残っているマスク領域を普通のDFSで埋める
  if (!stack.isEmpty()) {
    Cell current = stack.get(stack.size()-1);
    Cell next = current.checkNeighborsMask();  // マスク内だけ見る
    if (next != null) {
      next.visited = true;
      removeWalls(current, next);
      stack.add(next);
    } else {
      stack.remove(stack.size()-1);
    }
  } else {
    // まだ未訪問のマスクがあれば、そこからまたDFSを始める
    Cell c = findUnvisitedMaskCell();
    if (c != null) {
      c.visited = true;
      stack.add(c);
    }
  }
}

/* =========================================================
   画像 → マスク(黒だけtrue)
   ========================================================= */
boolean[][] loadMask(String fname, int cols, int rows) {
  PImage img = loadImage(fname);
  img.resize(cols, rows);
  img.loadPixels();

  boolean[][] m = new boolean[cols][rows];
  for (int y = 0; y < rows; y++) {
    for (int x = 0; x < cols; x++) {
      int c = img.pixels[y*cols + x];
      if (brightness(c) < 128) {
        m[x][y] = true;   // 黒なら通れる
      } else {
        m[x][y] = false;
      }
    }
  }
  return m;
}

/* =========================================================
   指定した(x,y)が白だったとき、近くの黒に寄せる
   ========================================================= */
Cell snapToMask(int x, int y) {
  x = constrain(x, 0, cols-1);
  y = constrain(y, 0, rows-1);

  if (mask[x][y]) return grid[x][y];

  int maxR = max(cols, rows);
  for (int r = 1; r < maxR; r++) {
    for (int dy = -r; dy <= r; dy++) {
      for (int dx = -r; dx <= r; dx++) {
        int nx = x + dx;
        int ny = y + dy;
        if (nx < 0 || nx >= cols || ny < 0 || ny >= rows) continue;
        if (mask[nx][ny]) {
          return grid[nx][ny];
        }
      }
    }
  }
  // なければとりあえず元のセル
  return grid[x][y];
}

/* =========================================================
   「再帰的バックトラッキング風」で start→goal を掘る
   途中で goal に着いたら止める
   ========================================================= */
void carveSegmentDFS(Cell start, Cell goal) {
  ArrayList<Cell> localStack = new ArrayList<Cell>();
  if (!start.visited) start.visited = true;
  localStack.add(start);

  while (!localStack.isEmpty()) {
    Cell cur = localStack.get(localStack.size()-1);

    // ここが「途中で止める」ポイント
    if (cur == goal) {
      break;
    }

    // マスク内だけから未訪問を探す(順序はランダム)
    ArrayList<Cell> neighbors = cur.unvisitedMaskNeighbors();
    if (neighbors.size() > 0) {
      // ランダムに選んでDFSらしくする
      int r = int(random(neighbors.size()));
      Cell next = neighbors.get(r);
      next.visited = true;
      removeWalls(cur, next);
      localStack.add(next);
    } else {
      // 行き止まりなので戻る
      localStack.remove(localStack.size()-1);
    }
  }
}

/* =========================================================
   残ってるマスクのセルを探す
   ========================================================= */
Cell findUnvisitedMaskCell() {
  for (int y = 0; y < rows; y++) {
    for (int x = 0; x < cols; x++) {
      if (mask[x][y] && !grid[x][y].visited) {
        return grid[x][y];
      }
    }
  }
  return null;
}

/* =========================================================
   壁を削除(あなたの元と同じ)
   ========================================================= */
void removeWalls(Cell a, Cell b) {
  int x = a.i - b.i;
  int y = a.j - b.j;
  if (x == 1) {
    a.walls[3] = false;
    b.walls[1] = false;
  } else if (x == -1) {
    a.walls[1] = false;
    b.walls[3] = false;
  } else if (y == 1) {
    a.walls[0] = false;
    b.walls[2] = false;
  } else if (y == -1) {
    a.walls[2] = false;
    b.walls[0] = false;
  }
}

/* =========================================================
   セルクラス
   ========================================================= */
class Cell {
  int i, j;
  boolean[] walls = { true, true, true, true };
  boolean visited = false;

  Cell(int i, int j) {
    this.i = i;
    this.j = j;
  }

  void show() {
    int x = i * w;
    int y = j * w;

    if (!mask[i][j]) {
      // 通れないところ(背景)
      noStroke();
      fill(255);
      rect(x, y, w, w);
      stroke(220);
      noFill();
      rect(x, y, w, w);
      return;
    }

    stroke(0);
    if (walls[0]) line(x, y, x + w, y);
    if (walls[1]) line(x + w, y, x + w, y + w);
    if (walls[2]) line(x + w, y + w, x, y + w);
    if (walls[3]) line(x, y + w, x, y);

    if (visited) {
      noStroke();
      fill(220, 190, 255, 110);
      rect(x, y, w, w);
    }

    // 見やすくするなら中継点を色付けしてもいい
    if (this == waypoints[0]) {
      noStroke(); fill(0,200,0,200); rect(x+1, y+1, w-9, w-9);
    }
    if (this == waypoints[1]) {
      noStroke(); fill(200,200,0,200); rect(x+1, y+1, w-9, w-9);
    }
    if (this == waypoints[2]) {
      noStroke(); fill(0,200,200,200); rect(x+1, y+1, w-9, w-9);
    }
    if (this == waypoints[3]) {
      noStroke(); fill(200,0,0,200); rect(x+1, y+1, w-9, w-9);
    }
  }

  // draw() で使う「マスク内だけ」のneighbors
  Cell checkNeighborsMask() {
    ArrayList<Cell> ns = unvisitedMaskNeighbors();
    if (ns.size() > 0) {
      int r = int(random(ns.size()));
      return ns.get(r);
    }
    return null;
  }

  // マスク内かつ未訪問の隣を全部返す
  ArrayList<Cell> unvisitedMaskNeighbors() {
    ArrayList<Cell> ns = new ArrayList<Cell>();
    if (i > 0) {
      if (mask[i-1][j] && !grid[i-1][j].visited) ns.add(grid[i-1][j]);
    }
    if (i < cols-1) {
      if (mask[i+1][j] && !grid[i+1][j].visited) ns.add(grid[i+1][j]);
    }
    if (j > 0) {
      if (mask[i][j-1] && !grid[i][j-1].visited) ns.add(grid[i][j-1]);
    }
    if (j < rows-1) {
      if (mask[i][j+1] && !grid[i][j+1].visited) ns.add(grid[i][j+1]);
    }
    return ns;
  }
}
0
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?