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

1559. Detect Cycles in 2D Grid

題目 / Problem

中文: 給定一個 m x n 的 2D 字元陣列 grid。如果存在一條由 相同字元 組成、長度 ≥ 4 的環路(path),則回傳 true

從某格出發,每一步只能往上下左右相鄰且字元相同的格子移動,並且 不能立刻退回上一步走過的那一格(例如 (1,1) → (1,2) → (1,1) 不算環)。

English: Given an m x n 2D character array grid, return true if there exists any cycle of length 4 or more made of the same character. From a cell you may move to any of its four neighbors that share the same value, but you may not move back to the cell you just came from.

Constraints: - 1 ≤ m, n ≤ 500 - grid only contains lowercase English letters.

Example (來自題目 Example 1):

grid = [["a","a","a","a"],
        ["a","b","b","a"],
        ["a","b","b","a"],
        ["a","a","a","a"]]
Output: true

The outer ring of 'a' forms a cycle of length 12, and the inner 'b' block forms a cycle of length 4.


名詞解釋 / Glossary

  • Grid graph / 網格圖:把每個格子當作一個節點 (node),相鄰且字元相同的兩個格子之間連一條邊 (edge),整張圖即為一個無向圖 (undirected graph)。
  • Cycle / 環路:在無向圖中,從某點出發、不重複經過邊、最後又走回起點的路徑。題目要求長度 ≥ 4。
  • DFS(深度優先搜尋)/ BFS(廣度優先搜尋):兩種遍歷圖的方法。DFS 用 stack(或遞迴)一條路走到底;BFS 用 queue 一層一層擴展。本題兩者皆可。
  • Parent / 父節點:在遍歷過程中,把「上一步是哪一格」記下來,避免被當成新的鄰居重新走回去。
  • Visited array / 已訪問標記:一個與 grid 同大小的布林陣列,記錄每一格是否已被搜尋過。每格只進一次 queue/stack,整體時間是線性的。
  • Connected component / 連通分量:圖中由相連節點所組成的最大子圖。同樣字元、彼此相連的格子屬於同一個分量;不同分量要分別啟動 BFS。
  • Queue / 佇列:先進先出 (FIFO) 的容器。C 用陣列加 head/tail 指標自製,C++ 用 std::queue
  • Iterative search / 迭代式搜尋:用顯式 queue/stack 取代遞迴。本題格子最多 250000,遞迴 DFS 容易 stack overflow,所以改用迭代。

思路

最直覺的想法是「對每個格子嘗試所有可能的路徑」,但路徑數量會爆炸成指數級,500×500 的 grid 完全跑不動。觀察題目本質:把每個格子當成節點、把「相鄰且字元相同」當成邊,問題就變成「這張無向圖裡有沒有環」。對無向圖來說,標準做法是 DFS 或 BFS 加上 parent 記錄:從某個未訪問的格子出發,走訪所有同字元的相連格子;走到鄰居時,如果鄰居就是「我剛剛來的那一格」(parent),跳過;如果鄰居 已被訪問且不是 parent,那就代表存在另一條路徑通往它,這條另一路徑加上現在這條邊形成了環。為了避免遞迴在 500×500 grid 爆掉 stack,這份題解用 iterative BFS,queue 的元素是 (r, c, pr, pc) —— 當前格與其 parent。每格只會進 queue 一次,因此整體 O(m·n)。題目要求環長 ≥ 4,但 4-directional grid 是二分圖 (bipartite)(依 (i+j) 的奇偶分兩色),所有環長必為偶數,最短就是 2×2 的 4-長度方環,所以「找到任何環」等同於「找到合法環」,不必另外計算長度。最外層的雙重迴圈確保每個連通分量(不同字元、或相同字元但不相連的區塊)都會被啟動一次 BFS。

The brute force of enumerating every walk blows up exponentially and cannot cope with a 500×500 grid, so we reframe the problem as graph cycle detection: each cell is a node, and an edge exists between two adjacent cells iff they share the same letter. Cycle detection in an undirected graph has a clean linear-time solution — run DFS or BFS while remembering each node's parent (the cell we just came from). When we look at a neighbor, if it equals the parent we skip it (that's the trivial back-and-forth the problem explicitly forbids); if it is already visited and not the parent, then it must have been reached by a different route from the same component, and that alternate route plus the current edge closes a cycle. Because a 500×500 grid can contain up to 250 000 same-letter cells, recursive DFS risks blowing the call stack, so we use an explicit BFS queue holding (r, c, parentR, parentC). Each cell is enqueued at most once, giving O(m·n) overall. We never need to verify the cycle's length is ≥ 4 because a 4-directional grid is bipartite (2-colorable by parity of i+j), so every cycle has even length and the smallest possible is 4. The outer double loop guarantees every connected component — including same-letter regions that are not connected to each other — gets its own BFS.


逐步走查 / Walkthrough

We trace BFS on Example 1, starting at (0,0) (letter 'a'). V means "marked visited"; parent (-1,-1) is a sentinel for the start.

Step Pop Parent Action on neighbors (same letter 'a') Queue tail after step
1 Enqueue (0,0,-1,-1), mark V [(0,0)]
2 (0,0) (-1,-1) (1,0) enqueue p=(0,0); (0,1) enqueue p=(0,0) [(1,0),(0,1)]
3 (1,0) (0,0) (0,0) is parent → skip. (2,0) enqueue p=(1,0). (1,1) is 'b' → skip [(0,1),(2,0)]
4 (0,1) (0,0) (0,0) parent → skip. (1,1) 'b' → skip. (0,2) enqueue p=(0,1) [(2,0),(0,2)]
5 (2,0) (1,0) (1,0) parent → skip. (3,0) enqueue p=(2,0). (2,1) 'b' → skip [(0,2),(3,0)]
6 (0,2) (0,1) (0,1) parent → skip. (1,2) 'b' → skip. (0,3) enqueue p=(0,2) [(3,0),(0,3)]
7 (3,0) (2,0) (2,0) parent → skip. (3,1) enqueue p=(3,0) [(0,3),(3,1)]
8 (0,3) (0,2) (0,2) parent → skip. (1,3) enqueue p=(0,3) [(3,1),(1,3)]
9 (3,1) (3,0) (3,0) parent → skip. (2,1) 'b'. (3,2) enqueue p=(3,1) [(1,3),(3,2)]
10 (1,3) (0,3) (0,3) parent → skip. (1,2) 'b'. (2,3) enqueue p=(1,3) [(3,2),(2,3)]
11 (3,2) (3,1) (3,1) parent → skip. (2,2) 'b'. (3,3) enqueue p=(3,2) [(2,3),(3,3)]
12 (2,3) (1,3) (1,3) parent → skip. (3,3) is already V, and (3,3) ≠ parent (1,3)cycle detected! return true

The two routes meeting at (3,3) are (0,0)→(0,1)→(0,2)→(0,3)→(1,3)→(2,3)→(3,3) and (0,0)→(1,0)→(2,0)→(3,0)→(3,1)→(3,2)→(3,3). Together they trace the outer ring — a length-12 cycle.


Solution — C

/*
 * Algorithm / 演算法:
 * 把同字元相鄰格子視為無向圖的邊,做 iterative BFS 並記錄 parent。
 * Treat same-letter adjacencies as edges of an undirected graph; run BFS
 * with parent tracking. If we ever reach an already-visited neighbor that
 * is not our parent, the graph contains a cycle. Time O(m*n), space O(m*n).
 */

#include <stdbool.h>   // bool 型別 / brings in `bool`, `true`, `false`
#include <stdlib.h>    // malloc / calloc / free

bool containsCycle(char** grid, int gridSize, int* gridColSize) {
    int m = gridSize;              // 列數 / number of rows
    int n = gridColSize[0];        // 行數(每列同寬) / columns (rows are same width)

    // visited[i*n + j] = true 表示 (i,j) 已經被某次 BFS 走過
    // visited[i*n + j] is true once cell (i,j) has been enqueued
    bool* visited = (bool*)calloc((size_t)m * n, sizeof(bool));

    // queue 用一塊大陣列模擬,每格存 4 個 int:r, c, parentR, parentC
    // We back the BFS queue with one big int array; each slot stores
    // (r, c, pr, pc). 4 * m * n is the worst-case capacity (each cell once).
    int* queue = (int*)malloc((size_t)m * n * 4 * sizeof(int));

    // 四個方向的位移:上、下、左、右
    // Direction deltas for up/down/left/right neighbors
    int dr[4] = {-1, 1, 0, 0};
    int dc[4] = { 0, 0,-1, 1};

    bool result = false;           // 找到環就設為 true / set true once a cycle is found

    // 雙層迴圈嘗試從每一個尚未拜訪的格子啟動一次 BFS(每個連通分量一次)
    // Outer loops launch a BFS from each unvisited cell — one per component
    for (int i = 0; i < m && !result; i++) {
        for (int j = 0; j < n && !result; j++) {
            if (visited[i * n + j]) continue; // 已屬於某個分量 / belongs to a component already

            int head = 0, tail = 0;          // queue 的前後指標 / BFS queue pointers
            // 把起點推進 queue:parent 用 (-1,-1) 代表「沒有上一步」
            // Push the start with sentinel parent (-1,-1) meaning "no previous step"
            queue[tail * 4 + 0] = i;
            queue[tail * 4 + 1] = j;
            queue[tail * 4 + 2] = -1;
            queue[tail * 4 + 3] = -1;
            tail++;
            visited[i * n + j] = true;       // 入隊就立刻標記 / mark on enqueue

            while (head < tail && !result) {
                // 取出隊首 / dequeue front
                int r  = queue[head * 4 + 0];
                int c  = queue[head * 4 + 1];
                int pr = queue[head * 4 + 2]; // parent row
                int pc = queue[head * 4 + 3]; // parent col
                head++;

                // 嘗試四個方向的鄰居 / try all four neighbors
                for (int d = 0; d < 4; d++) {
                    int nr = r + dr[d];      // neighbor 的 row
                    int nc = c + dc[d];      // neighbor 的 col

                    // 越界檢查 / skip out-of-bounds
                    if (nr < 0 || nr >= m || nc < 0 || nc >= n) continue;

                    // 字元不同 → 不算同色邊 / different letter, no edge
                    if (grid[nr][nc] != grid[r][c]) continue;

                    // 不能立刻走回 parent / cannot step back to where we came from
                    if (nr == pr && nc == pc) continue;

                    // 已訪問且不是 parent ⇒ 找到另一條路徑回到此格 ⇒ 有環
                    // Already visited and not the parent ⇒ alternate path ⇒ cycle
                    if (visited[nr * n + nc]) {
                        result = true;
                        break;
                    }

                    // 否則:標記、入隊,把當前 (r,c) 當作新格的 parent
                    // Otherwise: mark, enqueue, recording (r,c) as the new cell's parent
                    visited[nr * n + nc] = true;
                    queue[tail * 4 + 0] = nr;
                    queue[tail * 4 + 1] = nc;
                    queue[tail * 4 + 2] = r;
                    queue[tail * 4 + 3] = c;
                    tail++;
                }
            }
        }
    }

    free(visited);   // 釋放堆積記憶體 / free heap memory
    free(queue);
    return result;
}

Solution — C++

/*
 * Algorithm / 演算法:
 * 與 C 版本相同:把 grid 視為無向圖,迭代式 BFS + parent 追蹤。
 * Same as the C version: iterative BFS over the implicit undirected graph
 * with parent tracking. Use std::queue and std::array to keep the code
 * idiomatic C++. Time O(m*n), space O(m*n).
 */

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

class Solution {
public:
    bool containsCycle(vector<vector<char>>& grid) {
        int m = (int)grid.size();              // 列數 / row count
        int n = (int)grid[0].size();           // 行數 / column count

        // 2D bool 陣列,預設皆為 false / 2D visited matrix, default-initialised to false
        vector<vector<bool>> visited(m, vector<bool>(n, false));

        // 四方向位移;用 const 與大括號初始化 / direction deltas, brace-initialized
        const int dr[4] = {-1, 1, 0, 0};
        const int dc[4] = { 0, 0,-1, 1};

        // 對每個 (i,j) 嘗試啟動一次 BFS / launch one BFS per connected component
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (visited[i][j]) continue;   // 已屬於某分量 / already explored

                // std::queue<array<int,4>>:std::array 是固定大小的容器,比 tuple 易讀
                // queue of length-4 int arrays — std::array is a fixed-size container,
                // simpler to read here than std::tuple
                queue<array<int, 4>> q;
                q.push({i, j, -1, -1});         // 推入起點 / enqueue start with sentinel parent
                visited[i][j] = true;           // 入隊立刻標記 / mark on enqueue

                while (!q.empty()) {
                    // structured binding:把 array 拆成 4 個變數,C++17 起支援
                    // Structured binding (C++17) unpacks the array into 4 named variables
                    auto [r, c, pr, pc] = q.front();
                    q.pop();

                    for (int d = 0; d < 4; d++) {
                        int nr = r + dr[d];
                        int nc = c + dc[d];

                        // 邊界檢查 / out-of-bounds guard
                        if (nr < 0 || nr >= m || nc < 0 || nc >= n) continue;

                        // 不同字元就不是同色邊 / different letter ⇒ no same-value edge
                        if (grid[nr][nc] != grid[r][c]) continue;

                        // 不能走回 parent / forbidden U-turn
                        if (nr == pr && nc == pc) continue;

                        // 已訪問且不是 parent → 存在另一條路徑回到此格 → 有環
                        // Visited but not the parent ⇒ another route reached it ⇒ cycle
                        if (visited[nr][nc]) return true;

                        visited[nr][nc] = true;       // 標記 / mark before enqueueing
                        q.push({nr, nc, r, c});       // 入隊;當前格成為其 parent / enqueue with current cell as parent
                    }
                }
            }
        }
        return false;                                  // 沒有任何分量含環 / no component contained a cycle
    }
};

複雜度 / Complexity

  • Time: O(m · n) — 每個格子最多入 queue 一次、出 queue 一次,並檢查 4 個方向;總工作量正比於格子數 m·n。Each cell is enqueued and dequeued at most once and examines its 4 neighbors, so total work scales linearly with the number of cells.
  • Space: O(m · n)visited 陣列為 m·n 個布林值;BFS queue 最壞情況下也存到 O(m·n) 個三元組。visited is m·n booleans and the BFS queue can hold up to O(m·n) entries in the worst case (e.g., a grid that's all one letter).

Pitfalls & Edge Cases

  • 忘記紀錄 parent / Forgetting parent tracking — 沒有 parent 的話,從 A 走到鄰居 B 後,B 會把 A 視為「已訪問鄰居」而誤報環。Without pr/pc, every two-cell back-and-forth would falsely look like a cycle. The check if (nr == pr && nc == pc) continue; blocks this.
  • 遞迴 DFS 可能 stack overflow / Recursive DFS may blow the stack — 500×500 = 250 000 個格子若全部同字元相連,遞迴深度足以使預設 ~8 MB stack 溢出。改用 iterative BFS(顯式 queue 在 heap 上)即可避免。Iterative BFS sidesteps this by keeping the frontier on the heap.
  • 要為每個 component 啟動搜尋 / Must restart BFS per component — 不同字元的區塊,或同字元但被其他字元隔開的區塊,是獨立連通分量;外層 for 迴圈搭配 if (visited) continue 確保每個分量都被檢查、且每格只啟動一次。Same-letter regions separated by other letters form independent components.
  • 環長 ≥ 4 不必額外驗證 / No need to verify cycle length ≥ 4 — 4-directional grid 是 bipartite,任何環必為偶數長且最短為 4。所以「偵測到環」⇒「合法環」。
  • 不要在 dequeue 時才 mark visited / Don't defer marking until dequeue — 那樣同一格可能被多次入隊、且看似 visited 的條件會誤判。本解法 enqueue 立刻標記,保證每格只入隊一次。Marking on enqueue keeps each cell's queue insertions to exactly one and keeps the cycle test correct.
  • 單行或單列 grid / Single-row or single-column grid — 沒有環是可能的(最短環需 2×2 區塊),程式自然會走完所有格子並回傳 false,無需特判。
  • 記憶體釋放 / Free your heap allocations in Ccalloc/malloc 配上 free,避免 LeetCode 多次測試後 OOM。In C, always pair allocations with free so repeated test cases don't leak.