廣度優先搜尋相關面試演算法總結(非圖論方面)

guihunkun發表於2020-09-24

前言

此處總結的廣度優先搜尋相關面試演算法題前三道主要側重在樹的各種“層序遍歷”上,後面幾道則側重在對二維矩陣進行一些的操作,從而求得相應的值或矩陣,類似“被圍繞的區域”,“最短的橋”,“腐爛的橘子”等類似題目。其幾乎有統一的模板,文章後面會總結呈現給大家,重點在於領會廣度優先搜尋的演算法思想。

演算法題

1. LeetCode 劍指 Offer 32 - III : 從上到下列印二叉樹 III

LeetCode 劍指 Offer 32 - III

請實現一個函式按照之字形順序列印二叉樹,即第一行按照從左到右的順序列印,第二層按照從右到左的順序列印,第三行再按照從左到右的順序列印,其他行以此類推。
例如:
給定二叉樹: [3,9,20,null,null,15,7],
返回其層次遍歷結果:
[
[3],
[20,9],
[15,7]
]
提示:
節點總數 <= 1000

/*
 * 1
 * LeetCode 劍指 Offer 32 - III : 從上到下列印二叉樹 III
 * https://leetcode-cn.com/problems/cong-shang-dao-xia-da-yin-er-cha-shu-iii-lcof/
 */
vector<vector<int>> levelOrder(TreeNode* root)
{
    vector<vector<int> > res;
    if(root == nullptr)
    {
        return res;
    }
    queue<TreeNode*> q;
    q.push(root);
    int i = 1;
    while(!q.empty())
    {
        int num = q.size();
        vector<int> tmp;
        for(int i = 0; i < num; i++)
        {
            TreeNode* node = q.front();
            q.pop();
            tmp.push_back(node->val);
            if(node->left != nullptr) {
                q.push(node->left);
            }
            if(node->right != nullptr) {
                q.push(node->right);
            }
        }
        if((i++%2) == 0)
        {
            reverse(tmp.begin(), tmp.end());
        }
        res.push_back(tmp);
    }
    return res;
}


2. LeetCode 429 : N叉樹的層序遍歷

LeetCode 429

給定一個 N 叉樹,返回其節點值的層序遍歷。 (即從左到右,逐層遍歷)。
說明:
樹的深度不會超過 1000。
樹的節點總數不會超過 5000。

/*
 * 2
 * LeetCode 429 : N叉樹的層序遍歷
 * https://leetcode-cn.com/problems/n-ary-tree-level-order-traversal/
 */
vector<vector<int>> levelOrder(Node* root)
{
    vector<vector<int> > res;
    vector<int> tmp;
    if(root == NULL)
        return  res;
    queue<Node*> q;
    q.push(root);
    while(!q.empty())
    {
        tmp.clear();
        int count = q.size();
        while(count > 0)
        {
            Node* node = q.front();
            tmp.push_back(node->val);
            q.pop();
            count--;
            if(node->children.size() != 0)
                for(int i = 0; i < node->children.size(); i++)
                    q.push(node->children[i]);
        }
        res.push_back(tmp);
    }
    return res;
}


3. LeetCode 103. 二叉樹的鋸齒形層次遍歷

LeetCode 103

給定一個二叉樹,返回其節點值的鋸齒形層次遍歷。(即先從左往右,再從右往左進行下一層遍歷,以此類推,層與層之間交替進行)。

/*
 * 3
 * LeetCode 103. 二叉樹的鋸齒形層次遍歷
 * https://leetcode-cn.com/problems/binary-tree-zigzag-level-order-traversal/
 */
vector<vector<int>> zigzagLevelOrder(TreeNode* root)
{
    vector<vector<int> > res;
    if(root == NULL)
        return res;
    queue<TreeNode*> q;
    q.push(root);
    int level = 0;
    while(!q.empty())
    {
        vector<int> tmp;
        int count = q.size();
        for(int i = 0; i < count; i++)
        {
            TreeNode *t=q.front();
            q.pop();
            if(level%2 == 0)
                tmp.push_back(t->val);
            else
                tmp.insert(tmp.begin(),t->val);
            if(t->left)
                q.push(t->left);
            if(t->right)
                q.push(t->right);
        }
        level++;
        res.push_back(tmp);
    }
    return res;
}


4. LeetCode 130 : 被圍繞的區域

LeetCode 130

給定一個二維的矩陣,包含 ‘X’ 和 ‘O’(字母 O)。
找到所有被 ‘X’ 圍繞的區域,並將這些區域裡所有的 ‘O’ 用 ‘X’ 填充。
示例:
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’。如果兩個元素在水平或垂直方向相鄰,則稱它們是“相連”的。

/*
 * 4
 * LeetCode 130 : 被圍繞的區域
 * https://leetcode-cn.com/problems/surrounded-regions/
 */
const int dx[4] = {1, -1, 0, 0};
const int dy[4] = {0, 0, 1, -1};
void solve(vector<vector<char>>& board)
{
    int n = board.size();
    if(n == 0) {
        return;
    }
    int m = board[0].size();
    queue<pair<int, int>> que;
    for(int i = 0; i < n; i++) {
        if(board[i][0] == 'O') {
            que.emplace(i, 0);
        }
        if(board[i][m - 1] == 'O') {
            que.emplace(i, m - 1);
        }
    }
    for(int i = 1; i < m - 1; i++) {
        if(board[0][i] == 'O') {
            que.emplace(0, i);
        }
        if(board[n - 1][i] == 'O') {
            que.emplace(n - 1, i);
        }
    }
    while(!que.empty()) {
        int x = que.front().first, y = que.front().second;
        que.pop();
        board[x][y] = 'A';
        for(int i = 0; i < 4; i++) {
            int mx = x + dx[i], my = y + dy[i];
            if(mx < 0 || my < 0 || mx >= n || my >= m || board[mx][my] != 'O') {
                continue;
            }
            que.emplace(mx, my);
        }
    }
    for(int i = 0; i < n; i++) {
        for(int j = 0; j < m; j++) {
            if(board[i][j] == 'A') {
                board[i][j] = 'O';
            } else if(board[i][j] == 'O') {
                board[i][j] = 'X';
            }
        }
    }
}


5. LeetCode 934 : 最短的橋

LeetCode 934

在給定的二維二進位制陣列 A 中,存在兩座島。(島是由四面相連的 1 形成的一個最大組。)
現在,我們可以將 0 變為 1,以使兩座島連線起來,變成一座島。
返回必須翻轉的 0 的最小數目。(可以保證答案至少是 1。)
示例 1:
輸入:[[0,1],[1,0]]
輸出:1
示例 2:
輸入:[[0,1,0],[0,0,0],[0,0,1]]
輸出:2
示例 3:
輸入:[[1,1,1,1,1],[1,0,0,0,1],[1,0,1,0,1],[1,0,0,0,1],[1,1,1,1,1]]
輸出:1
提示:
1 <= A.length = A[0].length <= 100
A[i][j] == 0 或 A[i][j] == 1

Method 1 :DFS + BFS

  1. 找到第一個1的位置。
  2. 使用 DFS 找出所有相連的1, 並將所有相連的1都放入到一個佇列 queue 中,並且將該點的值改為2。
  3. 使用 BFS 進行層序遍歷,每遍歷一層,結果 res 都增加1,當遇到1時,直接返回 res 即可。
/*
 * 5
 * LeetCode 934 : 最短的橋
 * https://leetcode-cn.com/problems/shortest-bridge/
 */
/* Method 1 : DFS + BFS
1. 找到第一個1的位置。
2. 使用 DFS 找出所有相連的1, 並將所有相連的1都放入到一個佇列 queue 中,並且將該點的值改為2。
3. 使用 BFS 進行層序遍歷,每遍歷一層,結果 res 都增加1,當遇到1時,直接返回 res 即可。
*/
void dfs(vector<vector<int> >& A, int x, int y, queue<int>& q)
{
    int n = A.size();
    if(x < 0 || x >= n || y < 0 || y >= n || A[x][y] == 0 || A[x][y] == 2)
        return;
    A[x][y] = 2;
    q.push(x * n + y);
    dfs(A, x + 1, y, q);
    dfs(A, x, y + 1, q);
    dfs(A, x - 1, y, q);
    dfs(A, x, y - 1, q);
}
int shortestBridge(vector<vector<int> >& A)
{
    int res = 0, n = A.size(), startX = -1, startY = -1;
    queue<int> q;
    vector<vector<int> > dirs = {{-1,0},{0,1},{1,0},{0,-1}};
    for(int i = 0; i < n; i++)
    {
        for(int j = 0; j < n; j++)
        {
            if(A[i][j] == 0)
                continue;
            startX = i; startY = j;
            break;
        }
        if(startX != -1)
            break;
    }
    dfs(A, startX, startY, q);
    while(!q.empty())
    {
        for(int i = q.size(); i > 0; i--)
        {
            int t = q.front();
            q.pop();
            for(int k = 0; k < dirs.size(); k++)
            {
                int x = t / n + dirs[k][0];
                int y = t % n + dirs[k][1];
                if(x < 0 || x >= n || y < 0 || y >= n || A[x][y] == 2)
                    continue;
                if(A[x][y] == 1)
                    return res;
                A[x][y] = 2;
                q.push(x * n + y);
            }
        }
        ++res;
    }
    return res;
}

Method 2:BFS + BFS

  1. 找到第一個1的位置。
  2. 使用 BFS 找出所有相連的1, 並將所有相連的1都放入到一個佇列 queue 中,並且將該點的值改為2。
  3. 使用 BFS 進行層序遍歷,每遍歷一層,結果 res 都增加1,當遇到1時,直接返回 res 即可。
/* Method 2
1. 找到第一個1的位置。
2. 使用 BFS 找出所有相連的1, 並將所有相連的1都放入到一個佇列 queue 中,並且將該點的值改為2。
3. 使用 BFS 進行層序遍歷,每遍歷一層,結果 res 都增加1,當遇到1時,直接返回 res 即可。

**注意 :第一個 BFS 不需要是層序遍歷的,而第二個 BFS 是必須層序遍歷。
*/
int shortestBridge(vector<vector<int>>& A)
{
    int res = 0, n = A.size();
    queue<int> que; // first BFS used
    queue<int> q;   // secod=nd BFS used

    vector<vector<int> > dirs = {{-1,0},{0,1},{1,0},{0,-1}};
    for(int i = 0; i < n; i++)
    {
        for(int j = 0; j < n; j++)
        {
            if(A[i][j] == 0)
                continue;
            A[i][j] = 2;
            que.push(i * n + j);
            break;
        }
        if(!que.empty())
            break;
    }
    // first BFS
    while(!que.empty())
    {
        int t = que.front();
        que.pop();
        q.push(t);
        for(int k = 0; k < 4; k++)
        {
            int x = t / n + dirs[k][0];
            int y = t % n + dirs[k][1];
            if(x < 0 || x >= n || y < 0 || y >= n || A[x][y] == 0 || A[x][y] == 2)
                continue;
            A[x][y] = 2;
            que.push(x * n + y);
        }
    }
    // second BFS
    while(!q.empty())
    {
        for(int i = q.size(); i > 0; i--)
        {
            int t = q.front();
            q.pop();
            for(int k = 0; k < 4; k++)
            {
                int x = t / n + dirs[k][0];
                int y = t % n + dirs[k][1];
                if(x < 0 || x >= n || y < 0 || y >= n || A[x][y] == 2)
                    continue;
                if(A[x][y] == 1)
                    return res;
                A[x][y] = 2;
                q.push(x * n + y);
            }
        }
        ++res;
    }
    return res;
}


6. LeetCode 994 : 腐爛的橘子

LeetCode 994

在給定的網格中,每個單元格可以有以下三個值之一:
值 0 代表空單元格;
值 1 代表新鮮橘子;
值 2 代表腐爛的橘子。
每分鐘,任何與腐爛的橘子(在 4 個正方向上)相鄰的新鮮橘子都會腐爛。
返回直到單元格中沒有新鮮橘子為止所必須經過的最小分鐘數。如果不可能,返回 -1。
示例 1:
輸入:[[2,1,1],[1,1,0],[0,1,1]]
輸出:4
示例 2:
輸入:[[2,1,1],[0,1,1],[1,0,1]]
輸出:-1
解釋:左下角的橘子(第 2 行, 第 0 列)永遠不會腐爛,因為腐爛只會發生在 4 個正向上。
示例 3:
輸入:[[0,2]]
輸出:0
解釋:因為 0 分鐘時已經沒有新鮮橘子了,所以答案就是 0 。
提示:
1 <= grid.length <= 10
1 <= grid[0].length <= 10
grid[i][j] 僅為 0、1 或 2

/*
 * 6
 * LeetCode 994 : 腐爛的橘子
 * https://leetcode-cn.com/problems/rotting-oranges/
 */
int orangesRotting(vector<vector<int> >& grid)
{
    int ct = 0, res = -1;
    queue<vector<int> > q;
    vector<vector<int> > dir = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
    for(int i = 0; i < grid.size(); i++)
    {
        for(int j = 0; j < grid[i].size(); j++)
        {
            if(grid[i][j] > 0)
                ct++;
            if(grid[i][j] == 2)
                q.push({i, j});
        }
    }
    while(!q.empty())
    {
        res++;
        int n = q.size();
        for(int k = 0; k < n; k++)
        {
            vector<int> cur = q.front();
            ct--;
            q.pop();
            for(int i = 0; i < dir.size(); i++)
            {
                int x = cur[0] + dir[i][0];
                int y = cur[1] + dir[i][1];
                if(x >= grid.size() || x < 0 || y >= grid[0].size() || y < 0 || grid[x][y] != 1)
                    continue;
                grid[x][y] = 2;
                q.push({x, y});
            }
        }
    }
    if(ct == 0)
        return max(0, res);
    return -1;
}


7. LeetCode 542 : 01 矩陣

LeetCode 542

給定一個由 0 和 1 組成的矩陣,找出每個元素到最近的 0 的距離。
兩個相鄰元素間的距離為 1 。
示例 1:
輸入:
0 0 0
0 1 0
0 0 0
輸出:
0 0 0
0 1 0
0 0 0
示例 2:
輸入:
0 0 0
0 1 0
1 1 1
輸出:
0 0 0
0 1 0
1 2 1
注意:
給定矩陣的元素個數不超過 10000。
給定矩陣中至少有一個元素是 0。
矩陣中的元素只在四個方向上相鄰: 上、下、左、右。

/*
 * 7
 * LeetCode 542 : 01 矩陣
 * https://leetcode-cn.com/problems/01-matrix/
 */
/*
BFS:
從每一個0位置向上下左右四個方向進行廣播,如果廣播到的位置距離0的距離大於當前位置距離0的距離加一,
則將廣播到的位置距離0的位置更新為當前位置距離0的距離加一。
*/
vector<vector<int>> updateMatrix(vector<vector<int>>& matrix)
{
    int R = matrix.size();
    if(R == 0)
        return matrix;
    int C = matrix[0].size();
    vector<vector<int> > dist(R, vector<int>(C, INT_MAX));
    queue<pair<int,int> > q;
    for(int i = 0; i < R; i++)
    {
        for(int j = 0; j < C; j++)
        {
            if(matrix[i][j] == 0)
            {
                dist[i][j] = 0;
                q.push({i, j});
            }
        }
    }
    vector<vector<int> > dir = {{-1,0},{0,1},{1,0},{0,-1}};
    while(!q.empty())
    {
        pair<int, int> curr = q.front();
        q.pop();
        for(int i = 0; i < 4; i++)
        {
            int new_r = curr.first + dir[i][0];
            int new_c = curr.second + dir[i][1];
            if(new_r >= 0 && new_c >= 0 && new_r < R && new_c < C)
            {
                if(dist[new_r][new_c] > dist[curr.first][curr.second] + 1)
                {
                    dist[new_r][new_c] = dist[curr.first][curr.second] + 1;
                    q.push({ new_r, new_c });
                }
            }
        }
    }
    return dist;
}


8. LeetCode 1293 : 網格中的最短路徑(Hard)

LeetCode 1293

給你一個 m * n 的網格,其中每個單元格不是 0(空)就是 1(障礙物)。每一步,您都可以在空白單元格中上、下、左、右移動。
如果您 最多 可以消除 k 個障礙物,請找出從左上角 (0, 0) 到右下角 (m-1, n-1) 的最短路徑,並返回通過該路徑所需的步數。如果找不到這樣的路徑,則返回 -1。
示例 1:
輸入:
grid =
[[0,0,0],
[1,1,0],
[0,0,0],
[0,1,1],
[0,0,0]],
k = 1
輸出:6
解釋:
不消除任何障礙的最短路徑是 10。
消除位置 (3,2) 處的障礙後,最短路徑是 6 。該路徑是 (0,0) -> (0,1) -> (0,2) -> (1,2) -> (2,2) -> (3,2) -> (4,2).
示例 2:
輸入:
grid =
[[0,1,1],
[1,1,1],
[1,0,0]],
k = 1
輸出:-1
解釋:
我們至少需要消除兩個障礙才能找到這樣的路徑。
提示:
grid.length == m
grid[0].length == n
1 <= m, n <= 40
1 <= k <= m*n
grid[i][j] == 0 or 1
grid[0][0] == grid[m-1][n-1] == 0

/*
 * Hard
 * LeetCode 1293 : 網格中的最短路徑
 * https://leetcode-cn.com/problems/shortest-path-in-a-grid-with-obstacles-elimination/
 */
struct Nagato
{
    int x, y;
    int rest;
    Nagato(int _x, int _y, int _r): x(_x), y(_y), rest(_r) {}
};

class Solution
{
public:
    vector<vector<int> > dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
    int shortestPath(vector<vector<int>>& grid, int k)
    {
        if(grid.empty() || grid[0].empty())
        {
            return 0;
        }
        int m = grid.size(), n = grid[0].size();
        if(m == 1 && n == 1)
        {
            return 0;
        }
        k = min(k, m + n - 3);
        vector<vector<vector<bool> > > vis(m, vector<vector<bool> >(n, vector<bool>(k+1, false)));
        queue<Nagato> q;
        q.push(Nagato(0, 0, k));
        vis[0][0][k] = true;
        for(int step = 1; q.size() > 0; ++step)
        {
            int cnt = q.size();
            for(int j = 0; j < cnt; ++j)
            {
                Nagato cur = q.front();
                q.pop();
                for(int i = 0; i < dirs.size(); ++i)
                {
                    int nx = cur.x + dirs[i][0];
                    int ny = cur.y + dirs[i][1];
                    if(nx >= 0 && nx < m && ny >= 0 && ny < n)
                    {
                        if(grid[nx][ny] == 0 && !vis[nx][ny][cur.rest]) {
                            if(nx == m - 1 && ny == n - 1) {
                                return step;
                            }
                            q.push(Nagato(nx, ny, cur.rest));
                            vis[nx][ny][cur.rest] = true;
                        } else if(grid[nx][ny] == 1 && cur.rest > 0 && !vis[nx][ny][cur.rest - 1]) {
                            q.push(Nagato(nx, ny, cur.rest - 1));
                            vis[nx][ny][cur.rest - 1] = true;
                        }
                    }
                }
            }
        }
        return -1;
    }
};


注:最後一題“LeetCode 1293 : 網格中的最短路徑”,難度比較大,答案程式碼摘抄官方解答

模版

/* 
 *模版
 */
queue<T> q;
while(!q.empty())
{
    ...//
    int n = q.size();
    for(int k = 0; k < n; k++)
    {
        T t = q.front();
        ...//
        q.pop();
        for(擴充套件方式)
        {
            if(擴充套件方式所達到狀態合法)
            {
                ....//根據題意來新增
                q.push()}
        }
    }
}


總結

以上是廣度優先搜尋相關面試演算法題彙總,個別題目也給出瞭解題思路和註釋。
所有程式碼都可以去我的GitHub網站檢視,後續也將繼續補充其他演算法方面的相關題目,也會單獨寫一篇有關圖的面試演算法題文章。

相關文章