{ }codelper
幅優先探索P0249

踏むたびに反転する扉

長方形の地図の上で、S のマスを出て E のマスまで行きたい。
地図の文字は . 空きマス、# 壁、S 出発、E 到着、A・B・C 色の扉、a・b・c スイッチである。
1 分ごとに上下左右に隣り合うマス一つへ移る。
地図の外、壁、閉じた扉には移れず、その場にとどまることもできない。
最初はすべての扉が閉じている。
スイッチのマスに入るたびに、そのスイッチと同じ色の扉がすべて一斉に反転する(a は A、b は B、c は C)。
閉じていた扉は開き、開いていた扉は閉じる。
スイッチのマスから出るときは何も起こらない。
同じスイッチにまた入るとまた反転する。
同じ色のスイッチが複数あることもあり、どれに入っても同じように反転する。
反転はスイッチのマスに入るときだけ起こるので、扉のマスに立っている間にその扉が閉じることはない。
S と E は空きマスのように通れ、S のマスにまた入ってもよい。
E に初めて着く最短の時間(分)を出力せよ。
着けなければ -1 を出力せよ。
一行目に R と C、続く R 行に C 文字ずつ地図が与えられる。

入力
1 ≤ R, C ≤ 30。
地図には S と E が一つずつあり、ほかの文字は .#ABCabc だけである。
出力
最短の時間、または -1
例 1 · stdin
2 5
SaaAE
...##
stdout
6
例 2 · stdin
1 5
SaaAE
stdout
6
main.py
入力(stdin)

実行すると、これらの行が順に input()・標準入力に渡されます(実行中に入力を求めません)。

$ 準備完了

コードを入力して解くにはパソコンで開いてください。モバイルでは問題と解答を見られます。

ほかの言語では?言語別の解答を見る · 8 言語の解答

同じマスでも開いている扉が違えば別の状況なので、状態を(マス、開いている扉の色のビットマスク 0~7)にして幅優先探索をします。
状態は多くても 30 × 30 × 8 = 7,200 個です。
スイッチを踏んだら mask ^ (1 << 色)(XOR)でそのビットだけ反転します。
キュー:Python collections.deque、JS・TS は配列に入れて読む位置 head を進める(shift() は毎回先頭を詰めるので遅い)、Java ArrayDeque、C# Queue<int>、C は固定配列と head・tail、C++ std::queue。
Craykon にはビット演算がないので、色ごとに 1・2・4 を足した数で持ち、数学.商(mask, 2) % 2 のように商と余りで読み、立っていれば引き、立っていなければ足して反転します。
キューはリストと head です。
演算子の優先順位も言語ごとに違います:Python は & が == より先なので m >> k & 1 == 0 が意図どおりになりますが、C・C++・JS は == が先なので m >> k & (1 == 0)、つまり常に 0 になり、Java・C# は int と bool を & できずコンパイルエラーになります。
括弧 ((m >> k) & 1) == 0 を書きましょう。
落とし穴は四つ:① 鍵の問題のように mask | bit で一度開けた扉を開けっぱなしにすると、もう一度踏んで閉じた扉を通ってしまいます。
② 訪問済みの印をマスごとに一つしか持たないと、扉の状態を変えるために同じマスへ戻る道(例)を見落とします。
③ 上の C の優先順位の間違いで、扉がいつも開いているように見えます。
④ スイッチを踏んだときに一つの地図を直接書き換えると(扉の文字を . に)、キューにある別の状態までその扉が開いた地図を見てしまいます。
扉の状態は地図ではなく状態の中に持たせます。

Pythonmain.py · 42 行
import sysfrom collections import deque  def main():    data = sys.stdin.read().split()    r, c = int(data[0]), int(data[1])    grid = data[2:2 + r]    for y in range(r):        for x in range(c):            if grid[y][x] == "S":                start = (y, x)    # 상태 = (칸, 열린 문 색의 비트마스크). 같은 칸이라도 열린 문이 다르면 다른 상태다    dist = [[[-1] * 8 for _ in range(c)] for _ in range(r)]    sy, sx = start    dist[sy][sx][0] = 0    queue = deque([(sy, sx, 0)])    while queue:        y, x, mask = queue.popleft()        d = dist[y][x][mask]        for dy, dx in ((-1, 0), (1, 0), (0, -1), (0, 1)):            ny, nx = y + dy, x + dx            if not (0 <= ny < r and 0 <= nx < c):                continue            ch = grid[ny][nx]            if ch == "#":                continue            if ch in "ABC" and not mask >> (ord(ch) - ord("A")) & 1:                continue  # 닫힌 문            nmask = mask            if ch in "abc":                nmask = mask ^ (1 << (ord(ch) - ord("a")))  # 들어설 때마다 뒤집는다            if ch == "E":                print(d + 1)                return            if dist[ny][nx][nmask] < 0:                dist[ny][nx][nmask] = d + 1                queue.append((ny, nx, nmask))    print(-1)  main()
JavaScriptmain.js · 40 行
const data = require("fs").readFileSync(0, "utf8").trim().split(/\s+/);const r = Number(data[0]);const c = Number(data[1]);const grid = data.slice(2, 2 + r);let start = 0;for (let y = 0; y < r; y++) for (let x = 0; x < c; x++) if (grid[y][x] === "S") start = y * c + x;// 상태 번호 = 칸 * 8 + 열린 문 색의 비트마스크const dist = new Array(r * c * 8).fill(-1);const queue = [start * 8];dist[start * 8] = 0;const DY = [-1, 1, 0, 0];const DX = [0, 0, -1, 1];let answer = -1;search: for (let head = 0; head < queue.length; head++) {  const s = queue[head];  const cell = s >> 3;  const mask = s & 7;  const y = Math.floor(cell / c);  const x = cell % c;  for (let k = 0; k < 4; k++) {    const ny = y + DY[k];    const nx = x + DX[k];    if (ny < 0 || ny >= r || nx < 0 || nx >= c) continue;    const ch = grid[ny][nx];    if (ch === "#") continue;    if (ch >= "A" && ch <= "C" && ((mask >> (ch.charCodeAt(0) - 65)) & 1) === 0) continue; // 닫힌 문    let nmask = mask;    if (ch >= "a" && ch <= "c") nmask ^= 1 << (ch.charCodeAt(0) - 97); // 들어설 때마다 뒤집는다    if (ch === "E") {      answer = dist[s] + 1;      break search;    }    const ns = (ny * c + nx) * 8 + nmask;    if (dist[ns] < 0) {      dist[ns] = dist[s] + 1;      queue.push(ns);    }  }}console.log(answer);
TypeScriptmain.ts · 40 行
const data: string[] = require("fs").readFileSync(0, "utf8").trim().split(/\s+/);const r = Number(data[0]);const c = Number(data[1]);const grid = data.slice(2, 2 + r);let start = 0;for (let y = 0; y < r; y++) for (let x = 0; x < c; x++) if (grid[y][x] === "S") start = y * c + x;// 상태 번호 = 칸 * 8 + 열린 문 색의 비트마스크const dist: number[] = new Array(r * c * 8).fill(-1);const queue: number[] = [start * 8];dist[start * 8] = 0;const DY = [-1, 1, 0, 0];const DX = [0, 0, -1, 1];let answer = -1;search: for (let head = 0; head < queue.length; head++) {  const s = queue[head];  const cell = s >> 3;  const mask = s & 7;  const y = Math.floor(cell / c);  const x = cell % c;  for (let k = 0; k < 4; k++) {    const ny = y + DY[k];    const nx = x + DX[k];    if (ny < 0 || ny >= r || nx < 0 || nx >= c) continue;    const ch = grid[ny][nx];    if (ch === "#") continue;    if (ch >= "A" && ch <= "C" && ((mask >> (ch.charCodeAt(0) - 65)) & 1) === 0) continue; // 닫힌 문    let nmask = mask;    if (ch >= "a" && ch <= "c") nmask ^= 1 << (ch.charCodeAt(0) - 97); // 들어설 때마다 뒤집는다    if (ch === "E") {      answer = dist[s] + 1;      break search;    }    const ns = (ny * c + nx) * 8 + nmask;    if (dist[ns] < 0) {      dist[ns] = dist[s] + 1;      queue.push(ns);    }  }}console.log(answer);
JavaMain.java · 48 行
import java.io.*;import java.util.*; public class Main {    public static void main(String[] args) throws IOException {        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));        StringTokenizer st = new StringTokenizer(br.readLine());        int r = Integer.parseInt(st.nextToken());        int c = Integer.parseInt(st.nextToken());        char[][] grid = new char[r][];        int start = 0;        for (int y = 0; y < r; y++) {            grid[y] = br.readLine().trim().toCharArray();            for (int x = 0; x < c; x++) if (grid[y][x] == 'S') start = y * c + x;        }        // 상태 번호 = 칸 * 8 + 열린 문 색의 비트마스크        int[] dist = new int[r * c * 8];        Arrays.fill(dist, -1);        ArrayDeque<Integer> queue = new ArrayDeque<>();        dist[start * 8] = 0;        queue.add(start * 8);        int[] dy = {-1, 1, 0, 0}, dx = {0, 0, -1, 1};        while (!queue.isEmpty()) {            int s = queue.poll();            int cell = s / 8, mask = s % 8;            int y = cell / c, x = cell % c;            for (int k = 0; k < 4; k++) {                int ny = y + dy[k], nx = x + dx[k];                if (ny < 0 || ny >= r || nx < 0 || nx >= c) continue;                char ch = grid[ny][nx];                if (ch == '#') continue;                if (ch >= 'A' && ch <= 'C' && ((mask >> (ch - 'A')) & 1) == 0) continue; // 닫힌 문                int nmask = mask;                if (ch >= 'a' && ch <= 'c') nmask ^= 1 << (ch - 'a'); // 들어설 때마다 뒤집는다                if (ch == 'E') {                    System.out.println(dist[s] + 1);                    return;                }                int ns = (ny * c + nx) * 8 + nmask;                if (dist[ns] < 0) {                    dist[ns] = dist[s] + 1;                    queue.add(ns);                }            }        }        System.out.println(-1);    }}
C#Main.cs · 53 行
using System;using System.Collections.Generic; class Program{    static void Main()    {        var first = Console.ReadLine().Split(' ', StringSplitOptions.RemoveEmptyEntries);        int r = int.Parse(first[0]), c = int.Parse(first[1]);        var grid = new string[r];        int start = 0;        for (int y = 0; y < r; y++)        {            grid[y] = Console.ReadLine().Trim();            for (int x = 0; x < c; x++) if (grid[y][x] == 'S') start = y * c + x;        }        // 상태 번호 = 칸 * 8 + 열린 문 색의 비트마스크        var dist = new int[r * c * 8];        Array.Fill(dist, -1);        var queue = new Queue<int>();        dist[start * 8] = 0;        queue.Enqueue(start * 8);        int[] dy = { -1, 1, 0, 0 }, dx = { 0, 0, -1, 1 };        while (queue.Count > 0)        {            int s = queue.Dequeue();            int cell = s / 8, mask = s % 8;            int y = cell / c, x = cell % c;            for (int k = 0; k < 4; k++)            {                int ny = y + dy[k], nx = x + dx[k];                if (ny < 0 || ny >= r || nx < 0 || nx >= c) continue;                char ch = grid[ny][nx];                if (ch == '#') continue;                if (ch >= 'A' && ch <= 'C' && ((mask >> (ch - 'A')) & 1) == 0) continue; // 닫힌 문                int nmask = mask;                if (ch >= 'a' && ch <= 'c') nmask ^= 1 << (ch - 'a'); // 들어설 때마다 뒤집는다                if (ch == 'E')                {                    Console.WriteLine(dist[s] + 1);                    return;                }                int ns = (ny * c + nx) * 8 + nmask;                if (dist[ns] < 0)                {                    dist[ns] = dist[s] + 1;                    queue.Enqueue(ns);                }            }        }        Console.WriteLine(-1);    }}
C++main.cpp · 48 行
#include <iostream>#include <queue>#include <string>#include <vector> int main() {    int r, c;    std::cin >> r >> c;    std::vector<std::string> grid(r);    int start = 0;    for (int y = 0; y < r; y++) {        std::cin >> grid[y];        for (int x = 0; x < c; x++)            if (grid[y][x] == 'S') start = y * c + x;    }    // 상태 번호 = 칸 * 8 + 열린 문 색의 비트마스크    std::vector<int> dist(r * c * 8, -1);    std::queue<int> q;    dist[start * 8] = 0;    q.push(start * 8);    const int dy[4] = {-1, 1, 0, 0}, dx[4] = {0, 0, -1, 1};    while (!q.empty()) {        int s = q.front();        q.pop();        int cell = s / 8, mask = s % 8;        int y = cell / c, x = cell % c;        for (int k = 0; k < 4; k++) {            int ny = y + dy[k], nx = x + dx[k];            if (ny < 0 || ny >= r || nx < 0 || nx >= c) continue;            char ch = grid[ny][nx];            if (ch == '#') continue;            if (ch >= 'A' && ch <= 'C' && ((mask >> (ch - 'A')) & 1) == 0) continue; // 닫힌 문            int nmask = mask;            if (ch >= 'a' && ch <= 'c') nmask ^= 1 << (ch - 'a'); // 들어설 때마다 뒤집는다            if (ch == 'E') {                std::cout << dist[s] + 1 << '\n';                return 0;            }            int ns = (ny * c + nx) * 8 + nmask;            if (dist[ns] < 0) {                dist[ns] = dist[s] + 1;                q.push(ns);            }        }    }    std::cout << -1 << '\n';    return 0;}
Cmain.c · 46 行
#include <stdio.h> static char grid[30][32];static int dist[30 * 30 * 8];static int queue[30 * 30 * 8]; /* 상태마다 많아야 한 번 들어간다 */ int main(void) {    int r, c, start = 0;    scanf("%d %d", &r, &c);    for (int y = 0; y < r; y++) {        scanf("%31s", grid[y]);        for (int x = 0; x < c; x++)            if (grid[y][x] == 'S') start = y * c + x;    }    /* 상태 번호 = 칸 * 8 + 열린 문 색의 비트마스크 */    for (int i = 0; i < r * c * 8; i++) dist[i] = -1;    int head = 0, tail = 0;    dist[start * 8] = 0;    queue[tail++] = start * 8;    const int dy[4] = {-1, 1, 0, 0}, dx[4] = {0, 0, -1, 1};    while (head < tail) {        int s = queue[head++];        int cell = s / 8, mask = s % 8;        int y = cell / c, x = cell % c;        for (int k = 0; k < 4; k++) {            int ny = y + dy[k], nx = x + dx[k];            if (ny < 0 || ny >= r || nx < 0 || nx >= c) continue;            char ch = grid[ny][nx];            if (ch == '#') continue;            if (ch >= 'A' && ch <= 'C' && ((mask >> (ch - 'A')) & 1) == 0) continue; /* 닫힌 문 */            int nmask = mask;            if (ch >= 'a' && ch <= 'c') nmask ^= 1 << (ch - 'a'); /* 들어설 때마다 뒤집는다 */            if (ch == 'E') {                printf("%d\n", dist[s] + 1);                return 0;            }            int ns = (ny * c + nx) * 8 + nmask;            if (dist[ns] < 0) {                dist[ns] = dist[s] + 1;                queue[tail++] = ns;            }        }    }    printf("-1\n");    return 0;}
Craykonmain.cray · 88 行
// 크레이콘에는 비트 연산이 없어서, 열린 문 색 세 개를 0~7의 수(1·2·4의 합)로 담고 몫과 나머지로 읽는다첫줄 = コンソール.数読み();r = 첫줄[0];c = 첫줄[1];grid = [];start = 0;繰り返す (y = 0; y < r; y++){    줄 = コンソール.行読み().空白除去();    grid.追加(줄);    繰り返す (x = 0; x < c; x++)    {        もし (줄[x] == "S")        {            start = y * c + x;        }    }}문색 = 辞書(); // 문 글자 → 그 색의 값(1, 2, 4)문색["A"] = 1;문색["B"] = 2;문색["C"] = 4;스위치색 = 辞書();스위치색["a"] = 1;스위치색["b"] = 2;스위치색["c"] = 4; // 상태 번호 = 칸 * 8 + 열린 문 색의 합dist = 新リスト(r * c * 8, -1);queue = [start * 8];dist[start * 8] = 0;DY = [-1, 1, 0, 0];DX = [0, 0, -1, 1];answer = -1;head = 0;間 (head < queue.個数 && answer < 0){    s = queue[head];    head++;    cell = 数学.商(s, 8);    mask = s % 8;    y = 数学.商(cell, c);    x = cell % c;    繰り返す (k = 0; k < 4; k++)    {        ny = y + DY[k];        nx = x + DX[k];        もし (ny < 0 || ny >= r || nx < 0 || nx >= c)        {            次へ;        }        ch = grid[ny][nx];        もし (ch == "#")        {            次へ;        }        もし (문색.含む(ch) && 数学.商(mask, 문색[ch]) % 2 == 0)        {            次へ; // 닫힌 문        }        nmask = mask;        もし (스위치색.含む(ch))        {            v = 스위치색[ch];            // 들어설 때마다 뒤집는다: 켜져 있으면 빼고, 꺼져 있으면 더한다            もし (数学.商(mask, v) % 2 == 1)            {                nmask = mask - v;            }            でなければ            {                nmask = mask + v;            }        }        もし (ch == "E")        {            answer = dist[s] + 1;            抜ける;        }        ns = (ny * c + nx) * 8 + nmask;        もし (dist[ns] < 0)        {            dist[ns] = dist[s] + 1;            queue.追加(ns);        }    }}コンソール.書く(answer);