C++
 Computer >> コンピューター >  >> プログラミング >> C++

C++で解く「ユニークパスIII」― DFSとバックトラッキングで全マスを1回だけ通る経路を数える

問題の概要

2次元のグリッドが与えられ、各マスは次の4種類のいずれかで表されます。

  • 1 … スタート地点(必ず1つだけ存在します)
  • 2 … ゴール地点(必ず1つだけ存在します)
  • 0 … 自由に移動できる空きマス
  • -1 … 通ることができない障害物

求めるのは、スタートからゴールまで、障害物以外のすべてのマスをちょうど1回ずつ通るような、上下左右4方向への移動による経路の総数です。

入力例

1000
0000
102-1

この場合の出力は 2 となります。条件を満たす経路は次の2通りです。

(0,0) → (0,1) → (0,2) → (0,3) → (1,3) → (1,2) → (1,1) → (1,0) → (2,0) → (2,1) → (2,2)
(0,0) → (1,0) → (2,0) → (2,1) → (1,1) → (0,1) → (0,2) → (0,3) → (1,3) → (1,2) → (2,2)

解法のアプローチ:DFS+バックトラッキング

この問題は、深さ優先探索(DFS)とバックトラッキングを組み合わせることで解けます。ポイントは「残りの空きマス数」を管理しながら探索を進め、ゴールに到達した時点ですべてのマスを訪問し終えたかどうかを判定することです。

dfs() 関数の処理内容

dfs() は、グリッド grid、現在位置 (i, j)、ゴール座標 (ex, ey)、残り空きマス数 empty を引数として受け取ります。

  • 現在位置がグリッドの範囲外、または grid[i][j] が -1(障害物・訪問済み)の場合は 0 を返します。
  • grid[i][j] が 2(ゴール)の場合は、empty が -1(=すべてのマスを訪問済み)であるときに限り true(1)を返します。
  • x := 0 として答えを初期化します。
  • empty を1減らし、grid[i][j] を -1 にして訪問済みマークを付けます。
  • 4方向それぞれについて隣接マス (nx, ny) を計算し、x += dfs(grid, nx, ny, ex, ey, empty) で再帰的に探索します。
  • バックトラックとして、empty を1増やし、grid[i][j] を 0 に戻して状態を復元します。
  • 最後に x を返します。

uniquePathsIII() の処理内容

  • empty := 0 とし、行数 n、列数 m を取得します。
  • 全マスを走査し、0 なら empty を増加、1 ならスタート座標 (sx, sy)、2 ならゴール座標 (ex, ey) を記録します。
  • dfs(grid, sx, sy, ex, ey, empty) の結果をそのまま返します。

なお、最悪ケースの計算量は O(4^(n×m)) となり、グリッドが小さい前提の問題です。訪問済みマークを一時的に書き込んでから元に戻すことで、追加のメモリをほぼ使わずに実装できる点も特徴です。

C++での実装例

#include <bits/stdc++.h>
using namespace std;
int dir[4][2] = {{1,0},{-1,0},{0,1},{0,-1}};
class Solution {
    public:
    int dfs(vector<vector<int> >& grid, int i, int j, int ex, int ey,
    int empty){
        if (i >= grid.size() || i < 0 || j >= grid[0].size() || j < 0
        || grid[i][j] == -1)
        return 0;
        if (grid[i][j] == 2) {
            return empty == -1;
        }
        int x = 0;
        empty--;
        grid[i][j] = -1;
        for (int k = 0; k < 4; k++) {
            int nx = i + dir[k][0];
            int ny = j + dir[k][1];
            x += dfs(grid, nx, ny, ex, ey, empty);
        }
        empty++;
        grid[i][j] = 0;
        return x;
    }
    int uniquePathsIII(vector<vector<int> >& grid){
        int empty = 0;
        int sx, sy, ex, ey;
        int n = grid.size();
        int m = grid[0].size();
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (grid[i][j] == 0)
                empty++;
                else if (grid[i][j] == 1) {
                    sx = i;
                    sy = j;
                }
                else if (grid[i][j] == 2) {
                    ex = i;
                    ey = j;
                }
            }
        }
        return dfs(grid, sx, sy, ex, ey, empty);
    }
};
main(){
    Solution ob;
    vector<vector<int>> v = {{1,0,0,0},{0,0,0,0},{0,0,2,-1}};
    cout << (ob.uniquePathsIII(v));
}

入力

{{1,0,0,0},{0,0,0,0},{0,0,2,-1}}

出力

2
  1. C++で解く「電球スイッチャーIII」― マップと優先度付きキューによる効率的な解法

    問題概要 部屋にn個の電球があり、1からnまでの番号が付けられて、左から右へ一列に並んでいます。最初はすべての電球が消えています。時刻k(kは0からn-1までの範囲)に、light[k]番目の電球を点灯させていきます。ある電球が青色に変わるのは、その電球が点灯しており、かつそれより左側にあるすべての電球も点灯している場合だけです。点灯しているすべての電球が青色になっている瞬間の数を求めるのが、この問題の目的です。 次の図のようなイメージです。 この例の出力は3となり、条件を満たすのは時刻1、2、4です。 解法のアプローチ この問題は、マップと最小ヒープ(優先度付きキュー)を組み合わせること

  2. C++で解く「House Robber III(二分木の強盗問題)」の解説

    問題の概要ある泥棒が、新たな盗みの場所を見つけました。このエリアへ入れる入り口は一つだけで、「root(根)」と呼ばれています。root以外のすべての家には、必ず親となる家が1つだけ存在します。下見を終えた賢い泥棒は、「この場所のすべての家は二分木を形成している」ことに気づきました。さらに、直接つながっている2つの家が同じ夜に泥棒に入ると、警察へ自動的に通報される仕組みになっています。そこで、警察に通報されることなく今夜盗める金額の最大値を求める必要があります。例として、次のような二分木を考えてみましょう。この場合、出力は 7 となります。解き方のアルゴリズムこの問題は、木構造に対する動的計画