【Leetcode】130. 被包圍的區域

題目

給定一個二維的矩陣,包含 'X''O'字母 O)。java

找到全部被 'X' 圍繞的區域,並將這些區域裏全部的 'O''X' 填充。node

示例:面試

X X X X
X O O X
X X O X
X O X X

運行你的函數後,矩陣變爲:數組

X X X X
X X X X
X X X X
X O X X

解釋:數據結構

被圍繞的區間不會存在於邊界上,換句話說,任何邊界上的 'O' 都不會被填充爲 'X'。 任何不在邊界上,或不與邊界上的 'O' 相連的 'O' 最終都會被填充爲 'X'。若是兩個元素在水平或垂直方向相鄰,則稱它們是「相連」的。分佈式

題解

這道題咱們拿到基本就能夠肯定是圖的dfs、bfs遍歷的題目了。題目中解釋說被包圍的區間不會存在於邊界上,因此咱們會想到邊界上的o要特殊處理,只要把邊界上的o特殊處理了,那麼剩下的o替換成x就能夠了。問題轉化爲,如何尋找和邊界聯通的o,咱們須要考慮以下狀況。函數

X X X X
X O O X
X X O X
X O O X

這時候的o是不作替換的。由於和邊界是連通的。爲了記錄這種狀態,咱們把這種狀況下的o換成#做爲佔位符,待搜索結束以後,遇到o替換爲x(和邊界不連通的o);遇到#,替換回o(和邊界連通的o)。this

如何尋找和邊界聯通的o? 從邊界出發,對圖進行dfs和bfs便可。這裏簡單總結下dfs和dfs。spa

  • bfs遞歸。能夠想一想二叉樹中如何遞歸的進行層序遍歷。
  • bfs非遞歸。通常用隊列存儲。
  • dfs遞歸。最經常使用,如二叉樹的先序遍歷。
  • dfs非遞歸。通常用stack。

那麼基於上面這種想法,咱們有四種方式實現。code

dfs遞歸

class Solution {
    public void solve(char[][] board) {
        if (board == null || board.length == 0) return;
        int m = board.length;
        int n = board[0].length;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                // 從邊緣o開始搜索
                boolean isEdge = i == 0 || j == 0 || i == m - 1 || j == n - 1;
                if (isEdge && board[i][j] == 'O') {
                    dfs(board, i, j);
                }
            }
        }

        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (board[i][j] == 'O') {
                    board[i][j] = 'X';
                }
                if (board[i][j] == '#') {
                    board[i][j] = 'O';
                }
            }
        }
    }

    public void dfs(char[][] board, int i, int j) {
        if (i < 0 || j < 0 || i >= board.length  || j >= board[0].length || board[i][j] == 'X' || board[i][j] == '#') {
            // board[i][j] == '#' 說明已經搜索過了. 
            return;
        }
        board[i][j] = '#';
        dfs(board, i - 1, j); // 上
        dfs(board, i + 1, j); // 下
        dfs(board, i, j - 1); // 左
        dfs(board, i, j + 1); // 右
    }
}

dfs 非遞歸

非遞歸的方式,咱們須要記錄每一次遍歷過的位置,咱們用stack來記錄,由於它先進後出的特色。而位置咱們定義一個內部類Pos來標記橫座標和縱座標。注意的是,在寫非遞歸的時候,咱們每次查看stack頂,可是並不出stack,直到這個位置上下左右都搜索不到的時候出Stack。

class Solution {
    public class Pos{
        int i;
        int j;
        Pos(int i, int j) {
            this.i = i;
            this.j = j;
        }
    }
    public void solve(char[][] board) {
        if (board == null || board.length == 0) return;
        int m = board.length;
        int n = board[0].length;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                // 從邊緣第一個是o的開始搜索
                boolean isEdge = i == 0 || j == 0 || i == m - 1 || j == n - 1;
                if (isEdge && board[i][j] == 'O') {
                    dfs(board, i, j);
                }
            }
        }

        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (board[i][j] == 'O') {
                    board[i][j] = 'X';
                }
                if (board[i][j] == '#') {
                    board[i][j] = 'O';
                }
            }
        }
    }

    public void dfs(char[][] board, int i, int j) {
        Stack<Pos> stack = new Stack<>();
        stack.push(new Pos(i, j));
        board[i][j] = '#';
        while (!stack.isEmpty()) {
            // 取出當前stack 頂, 不彈出.
            Pos current = stack.peek();
            // 上
            if (current.i - 1 >= 0 
                && board[current.i - 1][current.j] == 'O') {
                stack.push(new Pos(current.i - 1, current.j));
                board[current.i - 1][current.j] = '#';
              continue;
            }
            // 下
            if (current.i + 1 <= board.length - 1 
                && board[current.i + 1][current.j] == 'O') {
                stack.push(new Pos(current.i + 1, current.j));
                board[current.i + 1][current.j] = '#';      
                continue;
            }
            // 左
            if (current.j - 1 >= 0 
                && board[current.i][current.j - 1] == 'O') {
                stack.push(new Pos(current.i, current.j - 1));
                board[current.i][current.j - 1] = '#';
                continue;
            }
            // 右
            if (current.j + 1 <= board[0].length - 1 
                && board[current.i][current.j + 1] == 'O') {
                stack.push(new Pos(current.i, current.j + 1));
                board[current.i][current.j + 1] = '#';
                continue;
            }
            // 若是上下左右都搜索不到,本次搜索結束,彈出stack
            stack.pop();
        }
    }
}

bfs 非遞歸

dfs非遞歸的時候咱們用stack來記錄狀態,而bfs非遞歸,咱們則用隊列來記錄狀態。和dfs不一樣的是,dfs中搜索上下左右,只要搜索到一個知足條件,咱們就順着該方向繼續搜索,因此你能夠看到dfs代碼中,只要知足條件,就入Stack,而後continue本次搜索,進行下一次搜索,直到搜索到沒有知足條件的時候出stack。而bfs中,咱們要把上下左右知足條件的都入隊,因此搜索的時候就不能continue。你們能夠對比下二者的代碼,體會bfs和dfs的差別。

class Solution {
    public class Pos{
        int i;
        int j;
        Pos(int i, int j) {
            this.i = i;
            this.j = j;
        }
    }
    public void solve(char[][] board) {
        if (board == null || board.length == 0) return;
        int m = board.length;
        int n = board[0].length;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                // 從邊緣第一個是o的開始搜索
                boolean isEdge = i == 0 || j == 0 || i == m - 1 || j == n - 1;
                if (isEdge && board[i][j] == 'O') {
                    bfs(board, i, j);
                }
            }
        }

        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (board[i][j] == 'O') {
                    board[i][j] = 'X';
                }
                if (board[i][j] == '#') {
                    board[i][j] = 'O';
                }
            }
        }
    }

    public void bfs(char[][] board, int i, int j) {
        Queue<Pos> queue = new LinkedList<>();
        queue.add(new Pos(i, j));
        board[i][j] = '#';
        while (!queue.isEmpty()) {
            Pos current = queue.poll();
            // 上
            if (current.i - 1 >= 0 
                && board[current.i - 1][current.j] == 'O') {
                queue.add(new Pos(current.i - 1, current.j));
                board[current.i - 1][current.j] = '#';
                  // 沒有continue.
            }
            // 下
            if (current.i + 1 <= board.length - 1 
                && board[current.i + 1][current.j] == 'O') {
                queue.add(new Pos(current.i + 1, current.j));
                board[current.i + 1][current.j] = '#';      
            }
            // 左
            if (current.j - 1 >= 0 
                && board[current.i][current.j - 1] == 'O') {
                queue.add(new Pos(current.i, current.j - 1));
                board[current.i][current.j - 1] = '#';
            }
            // 右
            if (current.j + 1 <= board[0].length - 1 
                && board[current.i][current.j + 1] == 'O') {
                queue.add(new Pos(current.i, current.j + 1));
                board[current.i][current.j + 1] = '#';
            }
        }
    }
}

bfs 遞歸

bfs 通常咱們不會去涉及,並且比較繞,以前咱們惟一A過的用bfs遞歸的方式是層序遍歷二叉樹的時候能夠用遞歸的方式。

並查集

並查集這種數據結構好像你們不太經常使用,實際上頗有用,我在實際的production code中用過並查集。並查集經常使用來解決連通性的問題,即將一個圖中連通的部分劃分出來。當咱們判斷圖中兩個點之間是否存在路徑時,就能夠根據判斷他們是否在一個連通區域。 而這道題咱們其實求解的就是和邊界的O在一個連通區域的的問題。

並查集的思想就是,同一個連通區域內的全部點的根節點是同一個。將每一個點映射成一個數字。先假設每一個點的根節點就是他們本身,而後咱們以此輸入連通的點對,而後將其中一個點的根節點賦成另外一個節點的根節點,這樣這兩個點所在連通區域又相互連通了。
並查集的主要操做有:

  • find(int m):這是並查集的基本操做,查找m的根節點。
  • isConnected(int m,int n):判斷m,n兩個點是否在一個連通區域。
  • union(int m,int n):合併m,n兩個點所在的連通區域。
class UnionFind {
    int[] parents;

    public UnionFind(int totalNodes) {
        parents = new int[totalNodes];
        for (int i = 0; i < totalNodes; i++) {
            parents[i] = i;
        }
    }
        // 合併連通區域是經過find來操做的, 即看這兩個節點是否是在一個連通區域內.
    void union(int node1, int node2) {
        int root1 = find(node1);
        int root2 = find(node2);
        if (root1 != root2) {
            parents[root2] = root1;
        }
    }

    int find(int node) {
        while (parents[node] != node) {
            // 當前節點的父節點 指向父節點的父節點.
            // 保證一個連通區域最終的parents只有一個.
            parents[node] = parents[parents[node]];
            node = parents[node];
        }

        return node;
    }

    boolean isConnected(int node1, int node2) {
        return find(node1) == find(node2);
    }
}

咱們的思路是把全部邊界上的O看作一個連通區域。遇到O就執行並查集合並操做,這樣全部的O就會被分紅兩類

  • 和邊界上的O在一個連通區域內的。這些O咱們保留。
  • 不和邊界上的O在一個連通區域內的。這些O就是被包圍的,替換。

因爲並查集咱們通常用一維數組來記錄,方便查找parants,因此咱們將二維座標用node函數轉化爲一維座標。

public void solve(char[][] board) {
        if (board == null || board.length == 0)
            return;

        int rows = board.length;
        int cols = board[0].length;

        // 用一個虛擬節點, 邊界上的O 的父節點都是這個虛擬節點
        UnionFind uf = new UnionFind(rows * cols + 1);
        int dummyNode = rows * cols;

        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                if (board[i][j] == 'O') {
                    // 遇到O進行並查集操做合併
                    if (i == 0 || i == rows - 1 || j == 0 || j == cols - 1) {
                        // 邊界上的O,把它和dummyNode 合併成一個連通區域.
                        uf.union(node(i, j), dummyNode);
                    } else {
                        // 和上下左右合併成一個連通區域.
                        if (i > 0 && board[i - 1][j] == 'O')
                            uf.union(node(i, j), node(i - 1, j));
                        if (i < rows - 1 && board[i + 1][j] == 'O')
                            uf.union(node(i, j), node(i + 1, j));
                        if (j > 0 && board[i][j - 1] == 'O')
                            uf.union(node(i, j), node(i, j - 1));
                        if (j < cols - 1 && board[i][j + 1] == 'O')
                            uf.union(node(i, j), node(i, j + 1));
                    }
                }
            }
        }

        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                if (uf.isConnected(node(i, j), dummyNode)) {
                    // 和dummyNode 在一個連通區域的,那麼就是O;
                    board[i][j] = 'O';
                } else {
                    board[i][j] = 'X';
                }
            }
        }
    }

    int node(int i, int j) {
        return i * cols + j;
    }
}

熱門閱讀

Leetcode名企之路

相關文章
相關標籤/搜索