跳到主要內容

演算法

同一個問題,不同的解題思路

主題 · 考題:島嶼數量

考題:島嶼數量

數地圖上有幾塊相連的陸地:BFS(廣度優先搜尋)、DFS(深度優先搜尋)、並查集都能解,差在額外記憶體,以及陸地一格一格加進來時能不能接著算。

解法
45%
題目
一張由 1(陸地)和 0(海)組成的格子地圖,數有幾座島:上下左右相連的陸地算同一座。例如 [[1,1,0],[0,1,0],[1,0,1]] → 3。
1/90
佇列(前端在最前)

    一列一列掃描地圖。點任何一格可以切換陸地/海。

    同一份輸入,所有解法
    解法答案看過的格子+指標步數額外記憶體Big O
    BFS(佇列)15 座252最多 2 格在等O(R × C)
    DFS(堆疊)15 座252最多 3 格在等O(R × C)
    並查集15 座19196 格 parentO(R × C)
    亮起來的是這一步執行的程式碼
    const DIRS = [[-1, 0], [1, 0], [0, -1], [0, 1]];
    function islandsBfs(grid: number[][]): number {
    const h = grid.length, w = grid[0].length;
    const seen = grid.map((row) => row.map(() => false));
    let islands = 0;
    for (let r = 0; r < h; r++) for (let c = 0; c < w; c++) {
    if (grid[r][c] !== 1 || seen[r][c]) continue;
    islands++;
    seen[r][c] = true;
    const queue = [[r, c]];
    while (queue.length) {
    const [cr, cc] = queue.shift()!;
    for (const [dr, dc] of DIRS) {
    const nr = cr + dr, nc = cc + dc;
    if (nr >= 0 && nc >= 0 && nr < h && nc < w && grid[nr][nc] === 1 && !seen[nr][nc]) {
    seen[nr][nc] = true;
    queue.push([nr, nc]);
    }
    }
    }
    }
    return islands;
    }
    // Same walk with a stack: no recursion, so no stack overflow on big islands.
    function islandsDfs(grid: number[][]): number {
    const h = grid.length, w = grid[0].length;
    const seen = grid.map((row) => row.map(() => false));
    let islands = 0;
    for (let r = 0; r < h; r++) for (let c = 0; c < w; c++) {
    if (grid[r][c] !== 1 || seen[r][c]) continue;
    islands++;
    seen[r][c] = true;
    const stack = [[r, c]];
    while (stack.length) {
    const [cr, cc] = stack.pop()!;
    for (const [dr, dc] of DIRS) {
    const nr = cr + dr, nc = cc + dc;
    if (nr >= 0 && nc >= 0 && nr < h && nc < w && grid[nr][nc] === 1 && !seen[nr][nc]) {
    seen[nr][nc] = true;
    stack.push([nr, nc]);
    }
    }
    }
    }
    return islands;
    }
    class UnionFind {
    parent: number[];
    size: number[];
    constructor(n: number) {
    this.parent = Array.from({ length: n }, (_, i) => i);
    this.size = new Array(n).fill(1);
    }
    find(x: number): number {
    while (this.parent[x] !== x) {
    this.parent[x] = this.parent[this.parent[x]];
    x = this.parent[x];
    }
    return x;
    }
    union(a: number, b: number): boolean {
    let ra = this.find(a), rb = this.find(b);
    if (ra === rb) return false;
    if (this.size[ra] < this.size[rb]) [ra, rb] = [rb, ra];
    this.parent[rb] = ra;
    this.size[ra] += this.size[rb];
    return true;
    }
    }
    function islandsUnionFind(grid: number[][]): number {
    const h = grid.length, w = grid[0].length;
    const uf = new UnionFind(h * w);
    let islands = 0;
    for (let r = 0; r < h; r++) for (let c = 0; c < w; c++) {
    if (grid[r][c] !== 1) continue;
    islands++;
    // Only the neighbours already seen: up and left.
    if (r > 0 && grid[r - 1][c] === 1 && uf.union(r * w + c, (r - 1) * w + c)) islands--;
    if (c > 0 && grid[r][c - 1] === 1 && uf.union(r * w + c, r * w + c - 1)) islands--;
    }
    return islands;
    }
    // Land arrives one cell at a time; the count is kept up to date.
    class IslandCounter {
    uf: UnionFind;
    land: boolean[];
    islands = 0;
    constructor(private h: number, private w: number) {
    this.uf = new UnionFind(h * w);
    this.land = new Array(h * w).fill(false);
    }
    addLand(r: number, c: number): number {
    const i = r * this.w + c;
    if (this.land[i]) return this.islands;
    this.land[i] = true;
    this.islands++;
    for (const [dr, dc] of DIRS) {
    const nr = r + dr, nc = c + dc;
    if (nr < 0 || nc < 0 || nr >= this.h || nc >= this.w) continue;
    if (this.land[nr * this.w + nc] && this.uf.union(i, nr * this.w + nc)) this.islands--;
    }
    return this.islands;
    }
    }

    追問:陸地一格一格加進來

    已加入
    0 / 53
    島的數量
    0
    並查集累計工作
    0
    每次都用 BFS 重數
    0

    每加一格陸地,島可能變多或合併。並查集只看新格子的四個鄰居就能更新數量;BFS 或 DFS 則得把整張地圖重掃一遍。

    亮起來的是這一步執行的程式碼
    const DIRS = [[-1, 0], [1, 0], [0, -1], [0, 1]];
    function islandsBfs(grid: number[][]): number {
    const h = grid.length, w = grid[0].length;
    const seen = grid.map((row) => row.map(() => false));
    let islands = 0;
    for (let r = 0; r < h; r++) for (let c = 0; c < w; c++) {
    if (grid[r][c] !== 1 || seen[r][c]) continue;
    islands++;
    seen[r][c] = true;
    const queue = [[r, c]];
    while (queue.length) {
    const [cr, cc] = queue.shift()!;
    for (const [dr, dc] of DIRS) {
    const nr = cr + dr, nc = cc + dc;
    if (nr >= 0 && nc >= 0 && nr < h && nc < w && grid[nr][nc] === 1 && !seen[nr][nc]) {
    seen[nr][nc] = true;
    queue.push([nr, nc]);
    }
    }
    }
    }
    return islands;
    }
    // Same walk with a stack: no recursion, so no stack overflow on big islands.
    function islandsDfs(grid: number[][]): number {
    const h = grid.length, w = grid[0].length;
    const seen = grid.map((row) => row.map(() => false));
    let islands = 0;
    for (let r = 0; r < h; r++) for (let c = 0; c < w; c++) {
    if (grid[r][c] !== 1 || seen[r][c]) continue;
    islands++;
    seen[r][c] = true;
    const stack = [[r, c]];
    while (stack.length) {
    const [cr, cc] = stack.pop()!;
    for (const [dr, dc] of DIRS) {
    const nr = cr + dr, nc = cc + dc;
    if (nr >= 0 && nc >= 0 && nr < h && nc < w && grid[nr][nc] === 1 && !seen[nr][nc]) {
    seen[nr][nc] = true;
    stack.push([nr, nc]);
    }
    }
    }
    }
    return islands;
    }
    class UnionFind {
    parent: number[];
    size: number[];
    constructor(n: number) {
    this.parent = Array.from({ length: n }, (_, i) => i);
    this.size = new Array(n).fill(1);
    }
    find(x: number): number {
    while (this.parent[x] !== x) {
    this.parent[x] = this.parent[this.parent[x]];
    x = this.parent[x];
    }
    return x;
    }
    union(a: number, b: number): boolean {
    let ra = this.find(a), rb = this.find(b);
    if (ra === rb) return false;
    if (this.size[ra] < this.size[rb]) [ra, rb] = [rb, ra];
    this.parent[rb] = ra;
    this.size[ra] += this.size[rb];
    return true;
    }
    }
    function islandsUnionFind(grid: number[][]): number {
    const h = grid.length, w = grid[0].length;
    const uf = new UnionFind(h * w);
    let islands = 0;
    for (let r = 0; r < h; r++) for (let c = 0; c < w; c++) {
    if (grid[r][c] !== 1) continue;
    islands++;
    // Only the neighbours already seen: up and left.
    if (r > 0 && grid[r - 1][c] === 1 && uf.union(r * w + c, (r - 1) * w + c)) islands--;
    if (c > 0 && grid[r][c - 1] === 1 && uf.union(r * w + c, r * w + c - 1)) islands--;
    }
    return islands;
    }
    // Land arrives one cell at a time; the count is kept up to date.
    class IslandCounter {
    uf: UnionFind;
    land: boolean[];
    islands = 0;
    constructor(private h: number, private w: number) {
    this.uf = new UnionFind(h * w);
    this.land = new Array(h * w).fill(false);
    }
    addLand(r: number, c: number): number {
    const i = r * this.w + c;
    if (this.land[i]) return this.islands;
    this.land[i] = true;
    this.islands++;
    for (const [dr, dc] of DIRS) {
    const nr = r + dr, nc = c + dc;
    if (nr < 0 || nc < 0 || nr >= this.h || nc >= this.w) continue;
    if (this.land[nr * this.w + nc] && this.uf.union(i, nr * this.w + nc)) this.islands--;
    }
    return this.islands;
    }
    }

    模型假設與範圍

    • Big O 與實測步數以頁面列出的基本操作為單位;視覺化的快照、畫圖、程式高亮和輸出成本另計,不是執行時間 benchmark。

    什麼時候用

    • 面試官要看你能不能把格子看成圖:每格是節點、上下左右是邊,數島就是數連通分量。BFS 或 DFS 都是標準答案。
    • 被追問「陸地一格一格加進來」時換並查集:每次更新只看四個鄰居,不必整張重掃。

    和其他主題的關係

    時間與空間複雜度(Big O)

    操作平均最差
    BFS(佇列)
    佇列最多約 min(R, C) 到 R×C 格
    O(R × C)O(R × C)
    DFS(堆疊)
    遞迴寫法的呼叫深度最差是 R×C
    O(R × C)O(R × C)
    並查集
    α 是反阿克曼函數,實務上 ≤ 4
    O(R × C · α)O(R × C · α)
    逐格加入(並查集)
    重數一次是 O(R × C)
    O(α)O(α)

    空間:O(R × C),標記走過的格子或 parent 陣列

    Big O 實測:n 變大時步數怎麼長

    數的是:看過的格子+指標步數(n 是格子數,45% 是陸地)

    Big On = 1,024n = 4,096n = 16,384成長倍數:實測(理論)
    BFS(佇列)O(n)2,86511,36345,288×16 (×16)
    DFS(堆疊)O(n)2,86511,36345,288×16 (×16)
    並查集O(n)2,3139,04335,949×16 (×16)

    和其他做法比

    數字
    逐格加入 450 格:並查集總工作2,140
    同上:每次都用 BFS 重數799,530
    全是陸地的 60×60:BFS 佇列最長60
    同上:DFS 堆疊最長1,771
    同上:遞迴寫法的 DFS 呼叫深度3,600

    30×30 的地圖逐格加入陸地時,並查集只看新格子的鄰居,總共 2,140 步;每次重新 BFS 要 799,530 步。遞迴寫的 DFS 在 3,600 格全是陸地的地圖上會深到 3,600 層,超過 Python 預設的 1,000 層上限。

    真實世界裡的它

    • LeetCode 200(島嶼數量)、305(逐格加入的版本)、695(最大島嶼面積)、130(被包圍的區域)。
    • 實務:影像處理的連通區域標記(connected-component labeling)、地圖上的區塊分群。

    取捨與陷阱

    • 放進佇列時就要標記,不是取出時才標:否則同一格會被加好幾次。
    • 遞迴寫的 DFS 在大島上會超過呼叫深度上限(Python 預設 1,000):用明確的堆疊,或改用 BFS。
    • 直接把 grid 改成 0 當作「走過」很省記憶體,但會改到呼叫者的資料,面試時要說明。

    LeetCode 練習