← 題庫 / Archive
2026-04-27 Daily Medium ArrayDepth-First SearchBreadth-First SearchUnion-FindMatrix

1391. Check if There is a Valid Path in a Grid

題目 / Problem

中文: 給定一個 m x n 的網格 grid,每個格子代表一條街道。街道類型編號 1-6: - 1:連接「左」與「右」(水平直線) - 2:連接「上」與「下」(垂直直線) - 3:連接「左」與「下」(左下彎) - 4:連接「右」與「下」(右下彎) - 5:連接「左」與「上」(左上彎) - 6:連接「右」與「上」(右上彎)

從左上角 (0, 0) 出發,只能沿著街道行走(不可變更任何街道)。請判斷是否存在一條從 (0, 0) 到右下角 (m-1, n-1) 的合法路徑。

English: You are given an m x n grid where each cell holds one of six street types (numbered 1–6) defined by which two of its four sides are open (left/right/up/down). Starting at (0, 0), you may only walk along streets — moving from cell A to neighbor B requires that A has an opening toward B and B has an opening back toward A. Return true if you can reach (m-1, n-1), otherwise false.

Constraints / 約束: - m == grid.length, n == grid[i].length - 1 <= m, n <= 300 - 1 <= grid[i][j] <= 6

Worked Example: grid = [[2,4,3],[6,5,2]]true. Path: (0,0) → (1,0) → (1,1) → (0,1) → (0,2) → (1,2).

名詞解釋 / Glossary

  • Grid / 網格:A 2D array indexed by (row, col). (0,0) is top-left, (m-1, n-1) is bottom-right. 二維陣列,以 (列, 行) 索引。
  • BFS (Breadth-First Search) / 廣度優先搜尋:A graph traversal that explores all neighbors of a node before going deeper, using a FIFO queue. Good for "is target reachable" questions. 用 FIFO 佇列逐層擴展,適合「是否可達」類問題。
  • DFS (Depth-First Search) / 深度優先搜尋:Traverses as far as possible along one branch before backtracking, typically using recursion or a stack. 一條路走到底,再回頭嘗試其他路徑。
  • Direction vector / 方向向量:A (dr, dc) pair representing a one-step move. Up = (-1, 0), Down = (1, 0), Left = (0, -1), Right = (0, 1). 表示一步位移的整數對。
  • Mutual connection / 雙向連通:Cell A can move to B only if both A's street opens toward B and B's street opens back toward A. Streets are not directional, but openings only exist on two sides per cell. 兩格須同時開口對應才算相連。
  • Visited array / 訪問陣列:Marks cells already enqueued to avoid revisiting and infinite loops. 標記已加入過佇列的格子,避免重複處理。
  • Queue / 佇列:FIFO data structure (push to back, pop from front). In C we simulate it with an array + two indices; in C++ we use std::queue. 先進先出資料結構。
  • malloc / calloc / free:C heap allocation. malloc gives uninitialized memory; calloc zero-initializes; free returns memory to the system. 必須 free 否則造成記憶體洩漏。
  • std::pair / structured bindings:C++17 lets you write auto [r, c] = q.front(); to destructure a pair into two named variables. 把 pair 拆成兩個變數的語法糖。

思路

每格街道只有兩個開口,因此從 (0,0) 出發時每步至多有兩種選擇。最直接的解法就是把網格看成圖:每格是節點,若兩相鄰格的開口互相對應,就連一條邊;接著從 (0,0) 做 BFS 或 DFS,看終點是否被訪問到。關鍵在於正確地判斷「兩格是否真的相連」——光看本格有沒有朝那個方向的開口不夠,還必須驗證鄰居那一格也有朝反方向的開口(街道不會單向接通)。為了寫得乾淨,我先把六種街道各自擁有的兩個方向預先存成查表,例如類型 4 是 {右, 下}、類型 6 是 {右, 上}。BFS 時對當前格的兩個開口逐一嘗試:算出鄰居座標,若沒越界、沒訪問過、且鄰居的方向表中含有目前移動方向的相反向量,就把鄰居入隊。整個過程每格至多入隊一次,所以複雜度為 O(m·n)。一個小但容易錯的細節:當 m == n == 1 時起點就是終點,迴圈第一次取出 (0,0) 就會回傳 true,所以不必特殊處理。

The cleanest framing is to treat the grid as a graph: each cell is a node, and an edge exists between two adjacent cells iff their street openings line up on both sides. Then the question reduces to "is (m-1, n-1) reachable from (0,0)?", which BFS answers in O(m·n). The trap to avoid is unidirectional reasoning — having an opening from cell A toward B does not mean you can walk from A to B; B must also have an opening pointing back. To make this check uniform, I precompute, for each of the six street types, the two (dr, dc) direction vectors it exposes. BFS pops a cell, walks its two openings, and for each candidate neighbor checks that the opposite direction (-dr, -dc) appears in the neighbor's opening list. A visited array prevents reprocessing. The single-cell case m == n == 1 falls out for free: the very first dequeue is already the target, so we return true without walking any edges.

逐步走查 / Walkthrough

Input: grid = [[2,4,3],[6,5,2]], so m = 2, n = 3.

Direction table (recall: U=(-1,0), D=(1,0), L=(0,-1), R=(0,1)): | Type | Openings | |---|---| | 1 | L, R | | 2 | U, D | | 3 | L, D | | 4 | R, D | | 5 | L, U | | 6 | R, U |

BFS trace (queue shown after each step, * = current cell):

Step Pop Type Try direction Neighbor Neighbor type Opposite in nbr table? Action
1 (0,0)* 2 U (-1,0) out of bounds skip
1 (0,0)* 2 D (1,0) (1,0) 6 (R,U) U? yes enqueue (1,0)
2 (1,0)* 6 R (0,1) (1,1) 5 (L,U) L? yes enqueue (1,1)
2 (1,0)* 6 U (-1,0) (0,0) 2 already visited skip
3 (1,1)* 5 L (0,-1) (1,0) 6 already visited skip
3 (1,1)* 5 U (-1,0) (0,1) 4 (R,D) D? yes enqueue (0,1)
4 (0,1)* 4 R (0,1) (0,2) 3 (L,D) L? yes enqueue (0,2)
4 (0,1)* 4 D (1,0) (1,1) 5 already visited skip
5 (0,2)* 3 L (0,-1) (0,1) 4 already visited skip
5 (0,2)* 3 D (1,0) (1,2) 2 (U,D) U? yes enqueue (1,2)
6 (1,2)* (1,2) == (m-1, n-1) return true

Solution — C

/*
 * Algorithm / 演算法:
 *   Treat the grid as a graph: each cell exposes two direction openings based on its street type.
 *   BFS from (0,0); we may step into a neighbor only if both cells' openings face each other.
 *   把網格視為圖,每格依街道類型擁有兩個方向開口;從 (0,0) 做 BFS,
 *   兩格開口互相對應才算連通;若能走到 (m-1, n-1) 即回傳 true。
 */

#include <stdbool.h>
#include <stdlib.h>

bool hasValidPath(int** grid, int gridSize, int* gridColSize) {
    int m = gridSize;                    // 列數 / number of rows
    int n = gridColSize[0];              // 行數 / number of columns (all rows同寬)

    // dirs[t][k] = 第 t 種街道的第 k 個開口方向 (dr, dc)
    // dirs[t][k] = the k-th opening direction (dr, dc) for street type t
    // 索引 0 不使用,留空 / index 0 is unused (street types are 1..6)
    int dirs[7][2][2] = {
        {{0,0},{0,0}},      // 0: 不使用 / unused
        {{0,-1},{0, 1}},    // 1: 左 / left, 右 / right
        {{-1,0},{1, 0}},    // 2: 上 / up,   下 / down
        {{0,-1},{1, 0}},    // 3: 左 / left, 下 / down
        {{0, 1},{1, 0}},    // 4: 右 / right,下 / down
        {{0,-1},{-1,0}},    // 5: 左 / left, 上 / up
        {{0, 1},{-1,0}}     // 6: 右 / right,上 / up
    };

    // visited[r*n + c] 表示 (r,c) 是否已加入佇列 / has (r,c) been enqueued?
    // calloc 會把記憶體初始化為 0 (false) / calloc zero-initializes memory
    bool* visited = (bool*)calloc((size_t)m * n, sizeof(bool));

    // 用陣列模擬 FIFO 佇列,每格存兩個 int (r, c),最多 m*n 格 / array-backed FIFO queue
    int* queue = (int*)malloc((size_t)m * n * 2 * sizeof(int));
    int head = 0, tail = 0;              // head 取出位置 / pop index, tail 寫入位置 / push index

    queue[tail++] = 0;                   // 推入起點 row / push start row
    queue[tail++] = 0;                   // 推入起點 col / push start col
    visited[0] = true;                   // 標記 (0,0) 已訪問 / mark start visited

    bool found = false;                  // 結果旗標 / result flag
    while (head < tail) {                // 佇列非空 / while queue is non-empty
        int r = queue[head++];           // 取出 row / pop row
        int c = queue[head++];           // 取出 col / pop col

        if (r == m - 1 && c == n - 1) {  // 已到達終點 / reached the bottom-right cell
            found = true;
            break;
        }

        int t = grid[r][c];              // 當前格的街道類型 / current cell's street type
        for (int k = 0; k < 2; k++) {    // 嘗試此街道的兩個開口 / try both openings
            int dr = dirs[t][k][0];      // 方向向量 row 分量 / row delta
            int dc = dirs[t][k][1];      // 方向向量 col 分量 / col delta
            int nr = r + dr;             // 鄰居 row / neighbor row
            int nc = c + dc;             // 鄰居 col / neighbor col

            // 越界則跳過 / skip if out of bounds
            if (nr < 0 || nr >= m || nc < 0 || nc >= n) continue;
            // 已訪問則跳過,避免無窮迴圈 / skip if visited, prevents revisits
            if (visited[nr * n + nc]) continue;

            int nt = grid[nr][nc];       // 鄰居的街道類型 / neighbor's type
            bool connects = false;       // 雙方是否真的相連 / mutual connection?

            // 鄰居必須有 (-dr, -dc) 方向的開口才算對接成功
            // neighbor must expose the OPPOSITE direction (-dr, -dc) for the link to be valid
            for (int j = 0; j < 2; j++) {
                if (dirs[nt][j][0] == -dr && dirs[nt][j][1] == -dc) {
                    connects = true;
                    break;               // 找到匹配就跳出 / found a match, stop scanning
                }
            }

            if (connects) {              // 雙向連通才入隊 / enqueue only if mutually connected
                visited[nr * n + nc] = true;        // 先標記再入隊 / mark before push
                queue[tail++] = nr;                  // 推入 row / push row
                queue[tail++] = nc;                  // 推入 col / push col
            }
        }
    }

    free(visited);                       // 釋放記憶體避免洩漏 / free to avoid memory leak
    free(queue);                         // 同上 / same
    return found;
}

Solution — C++

/*
 * Algorithm / 演算法:
 *   Same idea as the C version: precompute each street type's two opening directions,
 *   then BFS from (0,0), advancing only when both cells' openings face each other.
 *   思路同 C 版:預存六種街道的開口方向,從 (0,0) 做 BFS,
 *   只在「雙向開口對應」時才前進,看能否到達 (m-1, n-1)。
 */

#include <vector>
#include <queue>
#include <utility>
using namespace std;

class Solution {
public:
    bool hasValidPath(vector<vector<int>>& grid) {
        int m = grid.size();                 // 列數 / rows
        int n = grid[0].size();              // 行數 / cols

        // 每種街道類型的兩個開口方向 / two opening directions per street type
        // 用 vector<vector<pair<int,int>>>,索引 1..6 / indexed 1..6, slot 0 unused
        vector<vector<pair<int,int>>> dirs(7);
        dirs[1] = {{0,-1}, {0, 1}};          // 左右 / left, right
        dirs[2] = {{-1,0}, {1, 0}};          // 上下 / up, down
        dirs[3] = {{0,-1}, {1, 0}};          // 左下 / left, down
        dirs[4] = {{0, 1}, {1, 0}};          // 右下 / right, down
        dirs[5] = {{0,-1}, {-1,0}};          // 左上 / left, up
        dirs[6] = {{0, 1}, {-1,0}};          // 右上 / right, up

        // 訪問標記,避免重複入隊 / visited matrix
        vector<vector<bool>> visited(m, vector<bool>(n, false));

        // std::queue 是 FIFO 容器,push 加到尾端,front/pop 從前端取出
        // std::queue is a FIFO; push() appends, front()/pop() retrieve and remove
        queue<pair<int,int>> q;
        q.push({0, 0});                      // 起點入隊 / enqueue start
        visited[0][0] = true;

        while (!q.empty()) {
            // C++17 結構化綁定,把 pair 拆成 r, c 兩個名字
            // C++17 structured bindings: split a pair into two named variables
            auto [r, c] = q.front();
            q.pop();

            if (r == m - 1 && c == n - 1) return true;   // 已抵達終點 / target reached

            int t = grid[r][c];               // 當前街道類型 / current street type
            // range-for 走訪 dirs[t] 中的兩個方向 / iterate the two openings
            for (auto [dr, dc] : dirs[t]) {
                int nr = r + dr;              // 鄰居 row / neighbor row
                int nc = c + dc;              // 鄰居 col / neighbor col
                if (nr < 0 || nr >= m || nc < 0 || nc >= n) continue;  // 越界 / OOB
                if (visited[nr][nc]) continue;                          // 已訪問 / seen

                int nt = grid[nr][nc];        // 鄰居街道類型 / neighbor type
                bool connects = false;        // 雙向是否連通 / mutual link?

                // 鄰居必須有反方向 (-dr, -dc) 的開口 / neighbor needs the opposite opening
                for (auto [ddr, ddc] : dirs[nt]) {
                    if (ddr == -dr && ddc == -dc) {
                        connects = true;
                        break;
                    }
                }

                if (connects) {
                    visited[nr][nc] = true;   // 先標記再入隊,防重複 / mark before push
                    q.push({nr, nc});         // 入隊 / enqueue neighbor
                }
            }
        }

        return false;                         // 終點不可達 / target unreachable
    }
};

複雜度 / Complexity

  • Time: O(m · n) — 每個格子最多入隊一次(受 visited 保護),出隊時做 O(1) 的方向檢查(兩個開口、每個開口比對鄰居最多兩個開口,共四次比較)。n 在這裡是行數、m 是列數。Each cell is enqueued at most once, and each dequeue performs a constant amount of work (2 openings × at most 2 neighbor openings = 4 comparisons), so total work scales linearly with the number of cells.
  • Space: O(m · n)visited 陣列大小為 m·n,BFS 佇列最壞情況也可能存到 O(m·n) 個格子。dirs 表為常數大小。Dominated by the visited array and the BFS queue, both bounded by the grid size; the direction table is O(1).

Pitfalls & Edge Cases

  • 單向連通的陷阱 / One-way connection trap:只檢查當前格朝某方向有開口是不夠的,鄰居那一格也必須有對應的反向開口。例如 (0,0)=1 朝右,但 (0,1)=2 是上下街,沒有左開口,就走不過去。Always verify both sides — many wrong solutions accept any neighbor whose direction is in range.
  • 單格網格 / Single-cell grid (m == n == 1):起點即終點。BFS 第一次取出 (0,0) 就直接回傳 true,不需特殊判斷。Handled naturally by checking the target before exploring neighbors.
  • 越界判斷漏掉一邊 / Half-checked boundsnr < 0 || nr >= m || nc < 0 || nc >= n 四個條件缺一不可,否則會讀到非法記憶體。Use all four bounds checks; missing one causes out-of-bounds reads.
  • 忘記標記 visited / Missing visited mark:若不標記已入隊的格子,BFS 會在環狀街道中無限循環、或重複入隊導致 TLE。Mark before pushing (not after popping) to avoid duplicates in the queue.
  • C 中忘記 free / Forgetting free in C:每次呼叫 hasValidPath 都會配置記憶體;若不釋放,在 LeetCode 多筆測資下會洩漏。Always pair malloc/calloc with free.
  • gridColSize 當成單一整數 / Misreading gridColSize:在 C 介面中 gridColSizeint*,需用 gridColSize[0] 取行數。It's an array (one entry per row); use gridColSize[0] since all rows share the same width.