1. 程式人生 > >廣度優先搜尋原理與實踐

廣度優先搜尋原理與實踐

概論

在 深度優先搜尋原理與實踐(java)文章介紹了深度優先搜尋演算法的理論和實踐。本文將介紹與其原理類似的廣度優先搜尋演算法。

廣度優先搜尋(也稱寬度優先搜尋,縮寫 BFS,以下采用廣度來描述)是連通圖的一種遍歷演算法這一演算法也是很多重要的圖的演算法的原型。Dijkstra 單源最短路徑演算法和 Prim 最小生成樹演算法都採用了和寬度優先搜尋類似的思想。其別名又叫 BFS,屬於一種盲目搜尋法,目的是系統地展開並檢查圖中的所有節點,以找尋結果。換句話說,它並不考慮結果的可能位置,徹底地搜尋整張圖,直到找到結果為止。基本過程,BFS 是從根節點開始,沿著樹(圖)的寬度遍歷樹(圖)的節點。如果所有節點均被訪問,則演算法中止。一般用佇列資料結構來輔助實現 BFS 演算法。

基本原理

對於下面的樹而言,BFS 方法首先從根節點1開始,其搜尋節點順序是 1,2,3,4,5,6,7,8。

 

 

BFS 使用佇列 (queue) 來實施演算法過程,佇列 (queue) 有著先進先出 FIFO (First Input First Output)的特性,

BFS 操作步驟如下:

  • 把起始點放入 queue;

  • 重複下述2步驟,直到 queue 為空為止:

    • 從queue中取出佇列頭的點;
    • 找出與此點鄰接的且尚未遍歷的點,進行標記,然後全部放入queue中。

下面結合一個圖 (graph) 的例項,說明 BFS 的工作過程和原理:
(1)將起始節點1放入佇列中,標記為已遍歷:

 (2)從queue中取出佇列頭的節點1,找出與節點1鄰接的節點2,3,標記為已遍歷,然後放入queue中。

(3)從queue中取出佇列頭的節點2,找出與節點2鄰接的節點1,4,5,由於節點1已遍歷,排除;標記4,5為已遍歷,然後放入queue中。

 

(4)從queue中取出佇列頭的節點3,找出與節點3鄰接的節點1,6,7,由於節點1已遍歷,排除;標記6,7為已遍歷,然後放入queue中。

(5)從queue中取出佇列頭的節點4,找出與節點4鄰接的節點2,8,2屬於已遍歷點,排除;因此標記節點8為已遍歷,然後放入queue中。

 

(6)從queue中取出佇列頭的節點5,找出與節點5鄰接的節點2,8,2,8均屬於已遍歷點,不作下一步操作。

 

(7)從queue中取出佇列頭的節點6,找出與節點6鄰接的節點3,8,9,3,8屬於已遍歷點,排除;因此標記節點9為已遍歷,然後放入queue中。

(8)從queue中取出佇列頭的節點7,找出與節點7鄰接的節點3, 9,3,9屬於已遍歷點,不作下一步操作。

(9)從queue中取出佇列頭的節點8,找出與節點8鄰接的節點4,5,6,4,5,6屬於已遍歷點,不作下一步操作。

(10)從queue中取出佇列頭的節點9,找出與節點9鄰接的節點6,7,6,7屬於已遍歷點,不作下一步操作。

(11)queue 為空,則遍歷結束

上面過程可以用下面的程式碼來表示:

    private Map<String, Boolean> status = new HashMap<String, Boolean>();
    private Queue<String> queue = new LinkedList<String>();
    public void BFSSearch(String startPoint) {
        //1.把起始點放入queue;
        queue.add(startPoint);
        status.put(startPoint, false);
        bfsLoop();
    }
    
    private void bfsLoop() {
        while(!queue.isEmpty()) {
            //  1) 從queue中取出佇列頭的點;更新狀態為已經遍歷。
            String currentQueueHeader = queue.poll(); //出隊
            status.put(currentQueueHeader, true);
            System.out.println(currentQueueHeader);
            //  2) 找出與此點鄰接的且尚未遍歷的點,進行標記,然後全部放入queue中。
            List<String> neighborPoints = graph.get(currentQueueHeader);
            for (String poinit : neighborPoints) {
                if (!status.getOrDefault(poinit, false)) { //未被遍歷
                    if (queue.contains(poinit)) continue;
                    queue.add(poinit);
                    status.put(poinit, false);
                }
            }
        }
    }

通用框架

其通用框架可以概括為:

void bfs(起始點) {
    將起始點放入佇列中;
    標記起點訪問;
    while (如果佇列不為空) {  // 一般採用while ,當然也可以使用遞迴
        訪問佇列中隊首元素x;
        刪除隊首元素;
        for (x 所有相鄰點) {
            if (該點未被訪問過且合法) {
                將該點加入佇列末尾;
              if  (該結點是目標狀態) {  // 達到目標,提前結束終止迴圈
                    置 flag= true;    
            break; } } } } 佇列為空,廣搜結束; }

下面來總結下寫出 BFS 演算法規則:

通過這個 bfs 框架可以看出該方法主要有以下幾個規律:

  1. 起點條件。從哪個點開始訪問?是否每個點都需要當作起點?第一次 bfs 呼叫至關重要。

  2. 鄰接點。如何去獲取鄰接點?通過起點可到達的點。如何儲存鄰接點?先進先出。一般採用佇列。
  3. 迴圈引數。佇列不為空。一個點的所有鄰接點都是在一個 while 裡面進行新增的,才會進入

  4. 訪問標誌。為了避免重複訪問,需要對已經訪問過的節點加上標記,避免重複訪問。

講完了理論,下面開始進入實戰。

200. 島嶼數量

給你一個由 '1'(陸地)和 '0'(水)組成的的二維網格,請你計算網格中島嶼的數量。

島嶼總是被水包圍,並且每座島嶼只能由水平方向或豎直方向上相鄰的陸地連線形成。

此外,你可以假設該網格的四條邊均被水包圍。

示例 1:

// 輸入:
11110
11010
11000
00000
// 輸出: 1

示例 2:

// 輸入:
11000
11000
00100
00011
// 輸出: 3

解釋: 每座島嶼只能由水平和/或豎直方向上相鄰的陸地連線而成。


題目解答如下:

class Solution {
    public int numIslands(char[][] grid) {
        if (grid == null || grid.length < 1 || grid[0].length<1) {
            return 0;
        }
        int num = 0;
        int nr = grid.length;
        int nc = grid[0].length;
     // 每個點都可能是起點 for (int x =0;x<nr;x++) { for (int y =0;y<nc;y++) { if (grid[x][y]=='1') { bfs(grid,x,y); num++; } } } return num; }    // 對於 bfs 來說,只要佇列不為空,就可以一直走到頭, private void bfs(char[][] grid, int r, int c) { int nr = grid.length; int nc = grid[0].length;
     // 佇列,用於儲存鄰接點 Queue<Integer> neighbors = new LinkedList<>();
     // 這裡可以學下,對於二維可以將座標轉化為一個數字 neighbors.add(r * nc + c); while (!neighbors.isEmpty()) {
       // 每次迴圈開始的時候,需要移出一個點 int id = neighbors.remove(); int row = id / nc; int col = id % nc;
       // 四個鄰接點都是在一個while迴圈裡的 if (row - 1 >= 0 && grid[row-1][col] == '1') { neighbors.add((row-1) * nc + col); grid[row-1][col] = '0'; } if (row + 1 < nr && grid[row+1][col] == '1') { neighbors.add((row+1) * nc + col); grid[row+1][col] = '0'; } if (col - 1 >= 0 && grid[row][col-1] == '1') { neighbors.add(row * nc + col-1); grid[row][col-1] = '0'; } if (col + 1 < nc && grid[row][col+1] == '1') { neighbors.add(row * nc + col+1); grid[row][col+1] = '0'; } } } } 

695. 島嶼的最大面積

給定一個包含了一些 0 和 1 的非空二維陣列 grid 。

一個 島嶼 是由一些相鄰的 1 (代表土地) 構成的組合,這裡的「相鄰」要求兩個 1 必須在水平或者豎直方向上相鄰。你可以假設 grid 的四個邊緣都被 0(代表水)包圍著。

找到給定的二維陣列中最大的島嶼面積。(如果沒有島嶼,則返回面積為 0 。)

示例 1:

[[0,0,1,0,0,0,0,1,0,0,0,0,0],
[0,0,0,0,0,0,0,1,1,1,0,0,0],
[0,1,1,0,1,0,0,0,0,0,0,0,0],
[0,1,0,0,1,1,0,0,1,0,1,0,0],
[0,1,0,0,1,1,0,0,1,1,1,0,0],
[0,0,0,0,0,0,0,0,0,0,1,0,0],
[0,0,0,0,0,0,0,1,1,1,0,0,0],
[0,0,0,0,0,0,0,1,1,0,0,0,0]]

對於上面這個給定矩陣應返回 6。注意答案不應該是 11 ,因為島嶼只能包含水平或垂直的四個方向的 1 。

示例 2:

[[0,0,0,0,0,0,0,0]]

對於上面這個給定的矩陣, 返回 0。

注意: 給定的矩陣grid 的長度和寬度都不超過 50。


這道題目和上面的很類似。題目解答如下:

class Solution {
    public int maxAreaOfIsland(int[][] grid) {
        if (grid == null || grid.length <1 || grid[0].length<1) {
            return 0;
        }
        int rx = grid.length;
        int cy = grid[0].length;
        int max = 0;
        for (int x =0; x< rx; x++) {
            for (int y= 0;y<cy; y++) {
                if (grid[x][y]==1) {
                    int num = bfs(grid,x,y);
                    max = Math.max(max, num);
                }
            }
        }
        return max;
    }

    private int  bfs (int[][] grid, int x, int y){
        int rx = grid.length;
        int cy = grid[0].length;
     // 每次呼叫就是一個面積 int num = 1; grid[x][y] = 0; Queue<Integer> neQueue = new LinkedList<>();
     // 這裡注意乘以的是col的長度 neQueue.add(x*cy + y);
     // 佇列不為空 while(!neQueue.isEmpty()) { int point = neQueue.remove(); int nx = point / cy; int ny = point % cy;
       // 每一個方向都要判斷邊界 if (nx - 1 >= 0 && grid[nx-1][ny] == 1) { neQueue.add((nx-1) * cy + ny); grid[nx-1][ny] = 0; num++; } if (nx + 1 < rx && grid[nx+1][ny] == 1) { neQueue.add((nx+1) * cy + ny); grid[nx+1][ny] = 0; num++; } if (ny - 1 >= 0 && grid[nx][ny-1] == 1) { neQueue.add(nx * cy + ny-1); grid[nx][ny-1] = 0; num++; } if (ny + 1 < cy && grid[nx][ny+1] == 1) { neQueue.add(nx * cy + ny+1); grid[nx][ny+1] = 0; num++; } } return num; } }

 

 

參考文章

廣度優先遍歷(BFS )(轉