演算法~廣度優先搜尋(Breadth First Search)一石激起幹層浪(附帶6道練習題)

Listen-Y發表於2020-09-23

廣度優先搜尋模型

BFS() {
1.建立起始步驟,佇列初始化
2.遍歷佇列中的每一種可能,whlie(佇列不為空)
通過隊頭元素帶出下一步的所有可能,並且依次入隊
判斷當前情況是否達成目標:按照目標要求處理邏輯
繼續遍歷佇列中的剩餘情況
}

出迷宮

假設有一個迷宮,裡面有障礙物,迷宮用二維矩陣表示,標記為O的地方表示可以通過,標記為1的地方表示障礙物,不能通過。現在給一個迷宮出口,讓你判斷是否可以從入口進來之後,走出迷宮,每次可以向任意方向走。
假設是一個10*10的迷宮,入口在(1,1)的位置,出口在(8,10)的位置,通過(1,1)一步可以走到的位置有兩個(1,2),(2,1)·但是這兩個點並不是出口,需要繼續通過這兩個位置進一步搜尋,假設現在在(1,2),下一次一步可以到達的新的位置為(1,3),(2.2)。而通過(2,1)可以一步到達的新的位置為(2,2),(3,1),但是這裡(2,2)是重複的,所以每一個點在走的過程中需要標記是否已經走過了。
兩步之後,還沒沒有走到出口,這時候需要通過新加入的點再去探索下一步能走到哪些新的點上,重複這個過程,直到走到出口為止。

  • 程式碼
import java.util.LinkedList;
import java.util.Queue;
import java.util.Scanner;

/**
 * Created with IntelliJ IDEA.
 * Description: If you don't work hard, you will a loser.
 * User: Listen-Y.
 * Date: 2020-09-22
 * Time: 21:34
 */
public class SolutionBreadth {

    static class Node {
        public int x;
        public int y;

        public Node(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }

    /**
     * 迷宮問題
     */
    public static void main(String[] args) {
        int sr, sc, endR, endC;
        Scanner scanner = new Scanner(System.in);
        System.out.print("輸入迷宮起點與終點:");
        sr = scanner.nextByte();
        sc = scanner.nextByte();
        endR = scanner.nextByte();
        endC = scanner.nextByte();
        int[][] gird = {{0,1,0,0},
                        {0,0,0,1},
                        {0,1,0,0},
                        {0,0,1,0}};
        System.out.println(DFS(gird, sr, sc, endR, endC));

    }

    private static int[][] next = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};

    private static boolean DFS(int[][] gird, int sr, int sc, int endR, int endC) {

        int row = gird.length;
        if (row == 0) {
            return false;
        }
        int col = gird[0].length;
        //需要一個used儲存訪問過得點
        boolean[][] used = new boolean[row][col];
        //需要一個佇列儲存需要遍歷的點
        Queue<Node> queue = new LinkedList<>();
        queue.offer(new Node(sr, sc));
        //標記起點已被使用
        used[sr][sc] = true;

        //只要佇列不為空就說明還有機會到達終點
        while (!queue.isEmpty()) {
            //檢視起點的四周, 看哪個方向可以走
            for (int i = 0; i < 4; i++) {
                int newX = queue.peek().x + next[i][0];
                int newY = queue.peek().y + next[i][1];
                //判斷邊界
                if (newX < 0 || newX >= row || newY < 0 || newY >= col) {
                    continue;
                }
                //如果此時位置無障礙, 並且未被訪問就入佇列
                if (gird[newX][newY] == 0 && !used[newX][newY]) {
                    queue.offer(new Node(newX, newY));
                    //並標記這點被訪問過
                    used[newX][newY] = true;
                }
                //如果此時已經是終點就結束方法
                if (newX == endR && newY == endC) {
                    return true;
                }
            }
            //否則就出佇列判斷下一個點
            queue.poll();
        }
        return false;
    }
}

員工的重要性

  • 題目描述
    給定一個儲存員工資訊的資料結構,它包含了員工唯一的id,重要度 和 直系下屬的id。

比如,員工1是員工2的領導,員工2是員工3的領導。他們相應的重要度為15, 10, 5。那麼員工1的資料結構是[1, 15, [2]],員工2的資料結構是[2, 10, [3]],員工3的資料結構是[3, 5, []]。注意雖然員工3也是員工1的一個下屬,但是由於並不是直系下屬,因此沒有體現在員工1的資料結構中。

現在輸入一個公司的所有員工資訊,以及單個員工id,返回這個員工和他所有下屬的重要度之和。

示例 1:

輸入: [[1, 5, [2, 3]], [2, 3, []], [3, 3, []]], 1
輸出: 11
解釋:
員工1自身的重要度是5,他有兩個直系下屬2和3,而且2和3的重要度均為3。因此員工1的總重要度是 5 + 3 + 3 = 11。

來源:力扣(LeetCode)
連結:https://leetcode-cn.com/problems/employee-importance
著作權歸領釦網路所有。商業轉載請聯絡官方授權,非商業轉載請註明出處。

  • 程式碼
/*
// Definition for Employee.
class Employee {
    public int id;
    public int importance;
    public List<Integer> subordinates;
};
*/

class Solution {
    public int getImportance(List<Employee> employees, int id) {
        //將所有員工儲存在map中
        Map<Integer, Employee> map = new HashMap<>();
        for (Employee em : employees) {
            map.put(em.id, em);
        }
        //需要返回的重要性
        int importance = 0;
        //建立一個佇列儲存下屬員工
        Queue<Employee> queue = new LinkedList<>();
        queue.offer(map.get(id));
        while (!queue.isEmpty()) {
            //獲得當前有佇列裡有幾個下屬員工
            int size = queue.size();
            while (size-- > 0) {
                //獲取下屬
                Employee employ = queue.poll();
                importance += employ.importance;
                //判斷當前員工還有沒有下屬 如果有就加到佇列中
                if (employ.subordinates != null) {
                    for (int curId : employ.subordinates) {
                        queue.offer(map.get(curId));
                    }
                }
            }
        }
        return importance;
    }
}

N叉樹的遍歷

  • 題目描述
    給定一個 N 叉樹,返回其節點值的層序遍歷。 (即從左到右,逐層遍歷)。

例如,給定一個 3叉樹 :
在這裡插入圖片描述

返回其層序遍歷:

[
[1],
[3,2,4],
[5,6]
]

來源:力扣(LeetCode)
連結:https://leetcode-cn.com/problems/n-ary-tree-level-order-traversal
著作權歸領釦網路所有。商業轉載請聯絡官方授權,非商業轉載請註明出處。

  • 程式碼
/*
// Definition for a Node.
class Node {
    public int val;
    public List<Node> children;

    public Node() {}

    public Node(int _val) {
        val = _val;
    }

    public Node(int _val, List<Node> _children) {
        val = _val;
        children = _children;
    }
};
*/

class Solution {
    public List<List<Integer>> levelOrder(Node root) {
        
        //建立一個佇列儲存每一層的節點資料
        Queue<Node> queue = new LinkedList<>();
        queue.offer(root);
        List<List<Integer>> ret = new ArrayList<>();
        if (root == null) return ret;
        while (!queue.isEmpty()) {
            //獲取當前層的結點個數
            int size = queue.size();
            List<Integer> list = new ArrayList<>();
            while (size-- > 0) {
                //將該層所有結點的資料儲存在連結串列中
                Node cur = queue.poll();
                list.add(cur.val);
                //如果當前結點下一層還有結點就入佇列
                if (cur.children != null) {
                    for (Node node : cur.children) {
                        queue.offer(node);
                    }
                }
            }
            ret.add(list);
        }
        return ret;
    }
}

腐爛的橘子

  • 題目描述
    在給定的網格中,每個單元格可以有以下三個值之一:

值 0 代表空單元格;
值 1 代表新鮮橘子;
值 2 代表腐爛的橘子。
每分鐘,任何與腐爛的橘子(在 4 個正方向上)相鄰的新鮮橘子都會腐爛。

返回直到單元格中沒有新鮮橘子為止所必須經過的最小分鐘數。如果不可能,返回 -1。

來源:力扣(LeetCode)
連結:https://leetcode-cn.com/problems/rotting-oranges
著作權歸領釦網路所有。商業轉載請聯絡官方授權,非商業轉載請註明出處。

  • 程式碼
class Solution {

    static class Node {
        public int x;
        public int y;

        public Node(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }

    private int[][] next = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};

    public int orangesRotting(int[][] grid) {
        
        int row = grid.length;
        if (row == 0) return 0;
        int col = grid[0].length;

        //需要一個佇列儲存壞的橘子
        Queue<Node> queue = new LinkedList<>();
        //遍歷找到壞的橘子
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                if (grid[i][j] == 2) {
                    queue.offer(new Node(i, j));
                }
            }
        }
        int time = 0;
        //如果此時有壞的橘子就進行處理感染
        while (!queue.isEmpty()) {
            //用一個資料判斷是否進行了感染
            boolean ok = false;
            int size = queue.size();
            //進行一分鐘的感染
            while (size-- > 0) {
            Node cur = queue.poll();
            //遍歷該橘子的四周
            for (int i = 0; i < 4; i++) {
                int newR = cur.x + next[i][0];
                int newC = cur.y + next[i][1];
                //判斷邊界
                if (newR < 0 || newR >= row || newC < 0 || newC >= col) {
                    continue;
                }
                //判斷如果當前是新鮮橘子就進行感染
                if (grid[newR][newC] == 1) {
                    grid[newR][newC] = 2;
                    ok = true;
                    //並將感染後的橘子放到佇列中
                    queue.offer(new Node(newR, newC));
                }
            }
        }
        //如果有被感染
        if (ok) {
            time++;
        }
        }

        //檢查如果還有沒被感染的橘子就返回-1
        for (int i = 0; i < row; i++) {
            for (int j = 0; j < col; j++) {
                if (grid[i][j] == 1) {
                    return -1;
                }
            }
        }

        return time;
    }
}

單詞接龍

  • 題目描述
    給定兩個單詞(beginWord 和 endWord)和一個字典,找到從 beginWord 到 endWord 的最短轉換序列的長度。轉換需遵循如下規則:

每次轉換隻能改變一個字母。
轉換過程中的中間單詞必須是字典中的單詞。
說明:

如果不存在這樣的轉換序列,返回 0。
所有單詞具有相同的長度。
所有單詞只由小寫字母組成。
字典中不存在重複的單詞。
你可以假設 beginWord 和 endWord 是非空的,且二者不相同。

來源:力扣(LeetCode)
連結:https://leetcode-cn.com/problems/word-ladder
著作權歸領釦網路所有。商業轉載請聯絡官方授權,非商業轉載請註明出處。

  • 程式碼
class Solution {
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
        //將所有單詞放到hash表中 便於查詢
        Set<String> dict = new HashSet<>();
        for (String word : wordList) {
            dict.add(word);
        }
        //判斷此時字典是否有這個endWord
        if (!dict.contains(endWord)) {
            return 0;
        }
        //使用一個set去儲存訪問過的單詞
        Set<String> used = new HashSet<>();
        used.add(beginWord);
        //使用一個佇列去儲存替換一次的單詞
        Queue<String> queue = new LinkedList<>();
        queue.offer(beginWord);
        //計數器
        int step = 1;

        while (!queue.isEmpty()) {
            //取出一次變化後的說有單詞
            int size = queue.size();
            while (size-- > 0) {
                String curWord = queue.poll();
                //對這個單詞進行每一個位置的每一個字元的替換
                for (int i = 0; i < curWord.length(); i++) {
                    StringBuilder builder = new StringBuilder(curWord);
                    for (char ch = 'a'; ch <= 'z'; ch++) {
                        builder.setCharAt(i, ch);
                        String newWord = builder.toString();
                        //如果此時就是endWord就結束
                        if (newWord.equals(endWord)) {
                            return step + 1;
                        }
                        //判斷這個新單詞是否在字典中 沒有被訪問過
                        if (dict.contains(newWord) && !used.contains(newWord)) {
                            //否則入佇列
                            queue.offer(newWord);
                            used.add(newWord);
                        }
                    }
                }
            }
            //完成所有size表示進行了一次改變
            step++;
        }
        //如果到這還沒有返回說明不能扎到
        return 0;
    }
}

開啟轉盤鎖

  • 題目描述
    你有一個帶有四個圓形撥輪的轉盤鎖。每個撥輪都有10個數字: ‘0’, ‘1’, ‘2’, ‘3’, ‘4’, ‘5’, ‘6’, ‘7’, ‘8’, ‘9’ 。每個撥輪可以自由旋轉:例如把 ‘9’ 變為 ‘0’,‘0’ 變為 ‘9’ 。每次旋轉都只能旋轉一個撥輪的一位數字。

鎖的初始數字為 ‘0000’ ,一個代表四個撥輪的數字的字串。

列表 deadends 包含了一組死亡數字,一旦撥輪的數字和列表裡的任何一個元素相同,這個鎖將會被永久鎖定,無法再被旋轉。

字串 target 代表可以解鎖的數字,你需要給出最小的旋轉次數,如果無論如何不能解鎖,返回 -1。

來源:力扣(LeetCode)
連結:https://leetcode-cn.com/problems/open-the-lock
著作權歸領釦網路所有。商業轉載請聯絡官方授權,非商業轉載請註明出處。

  • 程式碼
class Solution {
    public int openLock(String[] deadends, String target) {
        //用一個hash去儲存dead便於查詢
        Set<String> deadSet = new HashSet<>();
        for (String str : deadends) {
            deadSet.add(str);
        }
        //如果0000是死亡數字那麼永遠有達不到
        if (deadSet.contains("0000")) {
            return -1;
        }
        //如果當前就是0000 就返回0
        if (target.equals("0000")) {
            return 0;
        }
        //用一個佇列去儲存此時的轉盤上的數字
        Queue<String> queue = new LinkedList<>();
        queue.offer("0000");
        //計數器
        int step = 0;
        //用一個set儲存訪問過得密碼
        Set<String> used = new HashSet<>();

        while (!queue.isEmpty()) {
            int size = queue.size();
            while (size-- > 0) {
                String curStr = queue.poll();
                //進行一次的撥盤
                for (int i = 0; i < 4; i++) {
                    //一次波動有倆種可能
                    char newOne;
                    char newTwo;
                    //對curStr當前的字元是0/9要進行特殊處理
                    if (curStr.charAt(i) == '0' || curStr.charAt(i) == '9') {
                        if (curStr.charAt(i) == '0') {
                            newOne = '1';
                            newTwo = '9';
                        } else {
                            newOne = '0';
                            newTwo = '8';
                        }
                    } else {
                        newOne = (char) (curStr.charAt(i) + 1);
                        newTwo = (char) (curStr.charAt(i) - 1);
                    }
                    //將撥好的單個數字進行與其他三個字元想組合
                    StringBuilder oneBuilder = new StringBuilder(curStr);
                    StringBuilder twoBuilder = new StringBuilder(curStr);
                    oneBuilder.setCharAt(i, newOne);
                    twoBuilder.setCharAt(i, newTwo);
                    String oneStr = oneBuilder.toString();
                    String twoStr = twoBuilder.toString();
                    //對此時這倆個字串進行判斷
                    // 如果已經是target就返回操作步數
                    if (oneStr.equals(target) || twoStr.equals(target)) {
                        return step + 1;
                    }
                    //分別對倆個字串進行判斷是否為鎖死密碼和是否已經遍歷過
                    if (!deadSet.contains(oneStr) && !used.contains(oneStr)) {
                        queue.offer(oneStr);
                        used.add(oneStr);
                    }
                    if (!deadSet.contains(twoStr) && !used.contains(twoStr)) {
                        queue.offer(twoStr);
                        used.add(twoStr);
                    }
                }
            }
            step++;
        }
        return -1;
    }
}

相關文章