高级数据结构与算法 | 深度遍历搜索(DFS)与广度遍历搜索(BFS)
文章目錄
- 深度優先搜索(DFS)
- 員工的重要性
- 圖像渲染
- 島嶼的周長
- 被圍繞的區域
- 島嶼數量
- 島嶼的最大面積
- 廣度優先搜索(BFS)
- N叉樹的層序遍歷
- 腐爛的橘子
- 單詞接龍
- 最小基因變化
- 打開轉盤鎖
深度優先搜索(DFS)
深度優先搜索的核心思想就是一條道走到黑,其實就和二叉樹的前、中、后序遍歷一樣,都會先一直沿著一個方向走,當走不通了再往回找其他的路。
員工的重要性
給定一個保存員工信息的數據結構,它包含了員工唯一的id,重要度 和 直系下屬的id。
比如,員工1是員工2的領導,員工2是員工3的領導。他們相應的重要度為15, 10, 5。那么員工1的數據結構是[1, 15, [2]],員工2的數據結構是[2, 10, [3]],員工3的數據結構是[3, 5, []]。注意雖然員工3也是員工1的一個下屬,但是由于并不是直系下屬,因此沒有體現在員工1的數據結構中。
現在輸入一個公司的所有員工信息,以及單個員工id,返回這個員工和他所有下屬的重要度之和。
來源:力扣(LeetCode)
鏈接:https://leetcode-cn.com/problems/employee-importance
圖像渲染
有一幅以二維整數數組表示的圖畫,每一個整數表示該圖畫的像素值大小,數值在 0 到 65535 之間。
給你一個坐標 (sr, sc) 表示圖像渲染開始的像素值(行 ,列)和一個新的顏色值 newColor,讓你重新上色這幅圖像。
為了完成上色工作,從初始坐標開始,記錄初始坐標的上下左右四個方向上像素值與初始坐標相同的相連像素點,接著再記錄這四個方向上符合條件的像素點與他們對應四個方向上像素值與初始坐標相同的相連像素點,……,重復該過程。將所有有記錄的像素點的顏色值改為新的顏色值。
最后返回經過上色渲染后的圖像。
來源:力扣(LeetCode)
鏈接:https://leetcode-cn.com/problems/flood-fill
這道題的思路很簡單,就是從標記點開始往四周進行搜索,如果搜索的位置符合要求,則進行染色
例如下圖
class Solution { public://向量矩陣int nextPos[4][2] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};void DFS(vector<vector<int>>& image, vector<vector<bool>>& visited, int row, int col, int sr, int sc, int oldColor, int newColor){//染色并進行標記image[sr][sc] = newColor;visited[sr][sc] = false;//向上下左右四個方向進行渲染for(int i = 0; i < 4; i++){int newRow = sr + nextPos[i][0];int newCol = sc + nextPos[i][1];//如果越界則跳過if(newRow < 0 || newRow >= row || newCol < 0 || newCol >= col){continue;}//如果該位置為老顏色,并且沒有渲染過則進行渲染if(image[newRow][newCol] == oldColor && visited[newRow][newCol]){DFS(image, visited, row, col, newRow, newCol, oldColor, newColor);}} }vector<vector<int>> floodFill(vector<vector<int>>& image, int sr, int sc, int newColor) {if(image.empty() || image[0].empty()){return image;}int row = image.size(), col = image[0].size();vector<vector<bool>> visited(row, vector<bool>(col, true)); //標記矩陣 int oldColor = image[sr][sc];DFS(image, visited, row, col, sr, sc, oldColor, newColor);return image;} };島嶼的周長
給定一個包含 0 和 1 的二維網格地圖,其中 1 表示陸地 0 表示水域。
網格中的格子水平和垂直方向相連(對角線方向不相連)。整個網格被水完全包圍,但其中恰好有一個島嶼(或者說,一個或多個表示陸地的格子相連組成的島嶼)。
島嶼中沒有“湖”(“湖” 指水域在島嶼內部且不和島嶼周圍的水相連)。格子是邊長為 1 的正方形。網格為長方形,且寬度和高度均不超過 100 。計算這個島嶼的周長。
來源:力扣(LeetCode)
鏈接:https://leetcode-cn.com/problems/island-perimeter
這道題是圖像渲染的類似問題,這道題的核心就是首先找到一個陸地的位置,順著陸地的位置進行搜索,如果搜索到邊界或者海洋則進行計數即可。
為了方便理解我直接套用了上題的模板進行修改,但是這種方法效率并不高(但是能過),如果追求效率還可以看下面的方法。
class Solution { public://向量矩陣int nextPos[4][2] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};void DFS(vector<vector<int>>& grid, vector<vector<bool>>& visited, int row, int col, int& count, int sr, int sc){ visited[sr][sc] = false;for(int i = 0; i < 4; i++){int newRow = sr + nextPos[i][0];int newCol = sc + nextPos[i][1];//如果為邊界或者為水域,則計數后跳過if(newRow < 0 || newRow >= row || newCol < 0 || newCol >= col || grid[newRow][newCol] == 0){count++;continue;}if(grid[newRow][newCol] == 1 && visited[newRow][newCol]){DFS(grid, visited, row, col, count, newRow, newCol);}}}int islandPerimeter(vector<vector<int>>& grid) {if(grid.empty() || grid[0].empty()){return 0;}int row = grid.size(), col = grid[0].size();vector<vector<bool>> visited(row, vector<bool>(col, true));int count = 0;//找到一個水池的邊界,沿著邊界搜索for(int i = 0; i < row; i++){for(int j = 0; j < col; j++){if(visited[i][j] && grid[i][j] == 1){DFS(grid, visited, row, col, count, i, j);break;}}}return count;} };簡化版本
class Solution { public:int islandPerimeter(vector<vector<int>>& grid) {if(grid.empty() || grid[0].empty()){return 0;}int row = grid.size(), col = grid[0].size();int count = 0;for(int i = 0; i < grid.size(); i++){for(int j = 0; j < grid[0].size(); j++){//如果為島嶼則搜索邊界與海洋if(grid[i][j] == 1){if(i == 0 || grid[i - 1][j] == 0) count++; //向上搜索if(i + 1 == row || grid[i + 1][j] == 0) count++; //向下搜索if(j == 0 || grid[i][j - 1] == 0) count++; //向左搜索if(j + 1 == col || grid[i][j + 1] == 0) count++; //向右搜索}}}return count;} };被圍繞的區域
給定一個二維的矩陣,包含 'X' 和 'O'(字母 O)。
找到所有被 'X' 圍繞的區域,并將這些區域里所有的 'O' 用 'X' 填充。
來源:力扣(LeetCode)
鏈接:https://leetcode-cn.com/problems/surrounded-regions/
沒錯,這還是渲染的變形。
從題目可以得出,我們需要將所有與邊界接壤的保留下來,而將內部獨立的小島全部渲染成X。
如果直接渲染小島這個題目就變得非常麻煩,因為還需要對邊界進行處理。
所以我們可以換一個思路,從邊界下手,將所有與邊界接壤的島嶼全部渲染為一個臨時值Y,此時被渲染為Y的陸地就被隔離開來,而剩下沒有被渲染為Y的陸地就是完全獨立的島嶼,我們只需要將這一部分島嶼全部變為X即可。之后再將我們渲染為臨時值Y的邊界給恢復為O即可。
class Solution { public://向量矩陣int nextPos[4][2] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};void DFS(vector<vector<char>>& board, vector<vector<bool>>& visited, int row, int col, int sr, int sc){//將邊界上的全部渲染為Yboard[sr][sc] = 'Y';visited[sr][sc] = false;//向上下左右四個方向進行搜索for(int i = 0; i < 4; i++){int newRow = sr + nextPos[i][0];int newCol = sc + nextPos[i][1];//如果越界則跳過if(newRow < 0 || newRow >= row || newCol < 0 || newCol >= col){continue;}//渲染與邊界相鄰的位置if(board[newRow][newCol] == 'O' && visited[newRow][newCol]){DFS(board, visited, row, col, newRow, newCol);}} }void solve(vector<vector<char>>& board) {if(board.empty() || board[0].empty()){return;}int row = board.size(), col = board[0].size();vector<vector<bool>> visited(row, vector<bool>(col, true)); //標記矩陣//將與邊界接壤的島嶼全部渲染為臨時值Y//尋找左右邊界for(int i = 0; i < row; i++){//左邊界if(board[i][0] == 'O'){DFS(board, visited, row, col, i, 0);}//右邊界if(board[i][col - 1] == 'O'){DFS(board, visited, row, col, i, col - 1);}}//尋找上下邊界for(int j = 0; j < col; j++){//上邊界if(board[0][j] == 'O'){DFS(board, visited, row, col, 0, j);}//下邊界if(board[row - 1][j] == 'O'){DFS(board, visited, row, col, row - 1, j);}}//狀態更替for(int i = 0; i < row; i++){for(int j = 0; j < col; j++){//將所有沒有被渲染到的內部島嶼變為Xif(board[i][j] == 'O'){board[i][j] = 'X';}//恢復所有與邊界接壤的島嶼if(board[i][j] == 'Y'){board[i][j] = 'O';}}}} };島嶼數量
給你一個由 ‘1’(陸地)和 ‘0’(水)組成的的二維網格,請你計算網格中島嶼的數量。
島嶼總是被水包圍,并且每座島嶼只能由水平方向或豎直方向上相鄰的陸地連接形成。
此外,你可以假設該網格的四條邊均被水包圍。
來源:力扣(LeetCode)
鏈接:https://leetcode-cn.com/problems/number-of-islands
我們發現,這道題其實還是渲染的那個模板,唯一不同的是變成了求島嶼的個數,也就是我們渲染的次數。
所以當我們找到一個陸地時,查詢他是否被標記過,如果未被標記則開始進行一次渲染,然后計數。
class Solution { public://向量矩陣int nextPos[4][2] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};void DFS(vector<vector<char>>& grid, vector<vector<bool>>& visited, int row, int col, int sr, int sc){visited[sr][sc] = false;//向上下左右四個方向進行搜索for(int i = 0; i < 4; i++){int newRow = sr + nextPos[i][0];int newCol = sc + nextPos[i][1];//如果越界則跳過if(newRow < 0 || newRow >= row || newCol < 0 || newCol >= col){continue;}//如果該位置為老顏色,并且沒有渲染過則進行渲染if(grid[newRow][newCol] == '1' && visited[newRow][newCol]){DFS(grid, visited, row, col, newRow, newCol);}} }int numIslands(vector<vector<char>>& grid) {if(grid.empty() || grid[0].empty()){return 0;}int row = grid.size(), col = grid[0].size();vector<vector<bool>> visited(row, vector<bool>(col, true)); //標記矩陣int count = 0;//當找到陸地,并且沒有被標記時,說明這是一個島嶼,進行搜索后,統計島嶼數量for(int i = 0; i < row; i++){for(int j = 0; j < col; j++){if(grid[i][j] == '1' && visited[i][j]){count++;DFS(grid, visited, row, col, i, j); }}}return count;} };島嶼的最大面積
給定一個包含了一些 0 和 1 的非空二維數組 grid 。
一個 島嶼 是由一些相鄰的 1 (代表土地) 構成的組合,這里的「相鄰」要求兩個 1 必須在水平或者豎直方向上相鄰。你可以假設 grid 的四個邊緣都被 0(代表水)包圍著。
找到給定的二維數組中最大的島嶼面積。(如果沒有島嶼,則返回面積為 0 。)
來源:力扣(LeetCode)
鏈接:https://leetcode-cn.com/problems/max-area-of-island
這道題是島嶼數量的變形,此時我們要求的是島嶼的最大面積,所以只需要統計每個島嶼的大小,然后再進行多次的搜索,選出最大的一個就行。本質就是多次渲染 + 大小統計
class Solution { public://向量矩陣int nextPos[4][2] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};void DFS(vector<vector<int>>& grid, vector<vector<bool>>& visited, int& count, int row, int col, int sr, int sc){count++;visited[sr][sc] = false;//向上下左右四個方向進行搜索for(int i = 0; i < 4; i++){int newRow = sr + nextPos[i][0];int newCol = sc + nextPos[i][1];//如果越界則跳過if(newRow < 0 || newRow >= row || newCol < 0 || newCol >= col){continue;}//如果該位置為老顏色,并且沒有渲染過則進行渲染if(grid[newRow][newCol] == 1 && visited[newRow][newCol]){DFS(grid, visited, count, row, col, newRow, newCol);}} }int maxAreaOfIsland(vector<vector<int>>& grid) {if(grid.empty() || grid[0].empty()){return 0;}int row = grid.size(), col = grid[0].size();vector<vector<bool>> visited(row, vector<bool>(col, true)); //標記矩陣int max = 0; //當找到陸地,并且沒有被標記時,說明這是一個島嶼,進行搜索后,統計島嶼數量for(int i = 0; i < row; i++){for(int j = 0; j < col; j++){if(grid[i][j] == 1 && visited[i][j]){int count = 0;DFS(grid, visited, count, row, col, i, j);max = max > count ? max : count;}}}return max;} };廣度優先搜索(BFS)
深度優先搜索的核心思想就是一石激起千層浪,就和二叉樹的層序遍歷一樣,他會先嘗試當前的所有可能,接著再嘗試由當前步引出的下一步的所有可能,也就是從內向外,一環一環不斷搜索的過程。
這里就拿前面DFS第一題員工的重要性來進行一個示范
/* // Definition for Employee. class Employee { public:int id;int importance;vector<int> subordinates; }; */class Solution { public:int getImportance(vector<Employee*> employees, int id) {//用哈希表進行存儲方便查詢unordered_map<int, Employee*> info;for(const auto& ep : employees){info[ep->id] = ep;}int sum = 0;queue<int> q;q.push(id); //隊首入隊//當前的所有可能while(!q.empty()){int cur = q.front();q.pop();sum += info[cur]->importance; //加上當前結果//下一步的所有可能for(auto subId : info[cur]->subordinates){q.push(subId);}}return sum;} };N叉樹的層序遍歷
給定一個 N 叉樹,返回其節點值的層序遍歷。 (即從左到右,逐層遍歷)。
來源:力扣(LeetCode)
鏈接:https://leetcode-cn.com/problems/n-ary-tree-level-order-traversal/
腐爛的橘子
在給定的網格中,每個單元格可以有以下三個值之一:
值 0 代表空單元格;
值 1 代表新鮮橘子;
值 2 代表腐爛的橘子。
每分鐘,任何與腐爛的橘子(在 4 個正方向上)相鄰的新鮮橘子都會腐爛。
返回直到單元格中沒有新鮮橘子為止所必須經過的最小分鐘數。如果不可能,返回 -1。
來源:力扣(LeetCode)
鏈接:https://leetcode-cn.com/problems/rotting-oranges
從題目的得出,每分鐘任何與腐爛橘子接觸的橘子都會變腐爛,也就是從最開始的腐爛橘子開始,由它往它的周圍進行擴散,直到所有的橘子被感染。
這個描述是不是和我們一開始說的BFS一模一樣,而我們要求的是最小分鐘數,所以這道題的題目其實就是讓我們求BFS經歷的次數。所以我們只需要在每輪BFS后進行判斷,如果當前由橘子感染,則進行一次計數。
class Solution { public:int nextPos[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; //方向矩陣int orangesRotting(vector<vector<int>>& grid) {if(grid.empty() || grid[0].empty()){return 0;}int row = grid.size(), col = grid[0].size();queue<pair<int, int>> q;//將所有腐爛的橘子入隊for(int i = 0; i < row; i++){for(int j = 0; j < col; j++){if(grid[i][j] == 2){q.push(make_pair(i, j));}}}int count = 0;while(!q.empty()){bool flag = false;int size = q.size();//由已腐爛的橘子帶入被感染的橘子while(size--){auto cur = q.front();q.pop();//本輪感染的橘子for(int i = 0; i < 4; i++){int newRow = cur.first + nextPos[i][0];int newCol = cur.second + nextPos[i][1];//如果越界則跳過if(newRow < 0 || newRow >= row || newCol < 0 || newCol >= col){continue;}//腐爛新鮮橘子if(grid[newRow][newCol] == 1){flag = true;grid[newRow][newCol] = 2;q.push(make_pair(newRow, newCol));}}}//如果有橘子腐爛才計數if(flag){count++;} }for(int i = 0; i < row; i++){for(int j = 0; j < col; j++){//如果有任何一個新鮮橘子,則說明不可能完成if(grid[i][j] == 1){return -1;}}}return count;} };單詞接龍
給定兩個單詞(beginWord 和 endWord)和一個字典,找到從 beginWord 到 endWord 的最短轉換序列的長度。轉換需遵循如下規則:
每次轉換只能改變一個字母。
轉換過程中的中間單詞必須是字典中的單詞。
說明:
如果不存在這樣的轉換序列,返回 0。
所有單詞具有相同的長度。
所有單詞只由小寫字母組成。
字典中不存在重復的單詞。
你可以假設 beginWord 和 endWord 是非空的,且二者不相同。
來源:力扣(LeetCode)
鏈接:https://leetcode-cn.com/problems/word-ladder
要求最短路徑,那么首先就能想到使用BFS,因為BFS每層相當于進行了一次變化的操作,而下一層都是在上一層的基礎上完成了一次變化后得到的序列,所以每次得到符合條件的序列都是在上層一步轉換得到的。
class Solution { public:int ladderLength(string beginWord, string endWord, vector<string>& wordList) {if(beginWord == endWord){return 0;}unordered_set<string> dict(wordList.begin(), wordList.end()); //哈希映射,方便查找unordered_set<string> visited; //標記矩陣queue<string> q;q.push(beginWord);visited.insert(beginWord);int res = 1; //序列長度包含原字符串//用BFS必定能求出最小序列,因為BFS是一輪一輪從內往外,每層的結果都只進行了一次變化。while(!q.empty()){int size = q.size();//對上輪的結果進行一次變化while(size--){string curStr = q.front();q.pop();//對每個位一次變化for(int i = 0; i < curStr.size(); i++){string newStr = curStr; //副本for(int j = 0; j < 26; j++){newStr[i] = 'a' + j;//如果訪問過或者詞典中不存在則跳過if(visited.count(newStr) || !dict.count(newStr)){continue;}//如果得到最終結果,則返回res + 1(本輪)if(newStr == endWord){return res + 1;}q.push(newStr);visited.insert(newStr);}}}res++;}return 0; //如果中間沒返回,說明不存在} };最小基因變化
一條基因序列由一個帶有8個字符的字符串表示,其中每個字符都屬于 “A”, “C”, “G”, "T"中的任意一個。
假設我們要調查一個基因序列的變化。一次基因變化意味著這個基因序列中的一個字符發生了變化。
例如,基因序列由"AACCGGTT" 變化至 “AACCGGTA” 即發生了一次基因變化。
與此同時,每一次基因變化的結果,都需要是一個合法的基因串,即該結果屬于一個基因庫。
現在給定3個參數 — start, end, bank,分別代表起始基因序列,目標基因序列及基因庫,請找出能夠使起始基因序列變化為目標基因序列所需的最少變化次數。如果無法實現目標變化,請返回 -1。
注意:
起始基因序列默認是合法的,但是它并不一定會出現在基因庫中。
所有的目標基因序列必須是合法的。
假定起始基因序列與目標基因序列是不一樣的。
來源:力扣(LeetCode)
鏈接:https://leetcode-cn.com/problems/minimum-genetic-mutation
與上題解法相同。
class Solution { public:int minMutation(string start, string end, vector<string>& bank) {if(start == end){return 0;}unordered_set<string> set(bank.begin(), bank.end());unordered_set<string> visited;int res = 0;queue<string> q;q.push(start);visited.insert(start);while(!q.empty()){int size = q.size();while(size--){string curStr = q.front();q.pop();for(int i = 0; i < start.size(); i++){string newStr = curStr;for(int j = 0; j < 26; j++){newStr[i] = 'A' + j;if(visited.count(newStr) || !set.count(newStr)){continue;}if(newStr == end){return res + 1;}q.push(newStr);visited.insert(newStr);}} }res++;}return -1;} };打開轉盤鎖
你有一個帶有四個圓形撥輪的轉盤鎖。每個撥輪都有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(vector<string>& deadends, string target) {unordered_set<string> set(deadends.begin(), deadends.end());unordered_set<string> visited;if(set.count("0000")){return -1;}int res = 0;queue<string> q;q.push("0000");visited.insert("0000");while(!q.empty()){int size = q.size();while(size--){string curStr = q.front();q.pop();if(curStr == target){return res;}for(int i = 0; i < target.size(); i++){string newStr1 = curStr; //往前加string newStr2 = curStr; //往后減newStr1[i] = (newStr1[i] == '9') ? '0' : newStr1[i] + 1;newStr2[i] = (newStr2[i] == '0') ? '9' : newStr2[i] - 1;if(!visited.count(newStr1) && !set.count(newStr1)){q.push(newStr1);visited.insert(newStr1);}if(!visited.count(newStr2) && !set.count(newStr2)){q.push(newStr2);visited.insert(newStr2);}} }res++;}return -1;} };總結
以上是生活随笔為你收集整理的高级数据结构与算法 | 深度遍历搜索(DFS)与广度遍历搜索(BFS)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 海量数据处理(二) :常见海量数据处理方
- 下一篇: 高级数据结构与算法 | 回溯算法(Bac