【演算法】深搜和廣搜
阿新 • • 發佈:2022-04-07
深搜和廣搜
1.概念
- 深度優先搜尋(Depth First Search, DFS):“不撞南牆不回頭”
- 廣度優先搜尋(Breath First Search, BFS):“一石激起千層浪”
2.DFS
2.1 特點
- 深度優先搜尋的主要思路是從一個未訪問過的節點開始,沿著一條路一直走,直到走到頭後沒法再走了,這時候回退到上一個節點,然後再換下一個節點接著走,不斷地去重複這個過程,直到所有的節點都走完,很明顯,DFS的特點就是不撞南牆不回頭,先走完一條路,再換下一條路接著走。
- 通過上面的敘述,發現DFS其實很多時候是和遞迴一起出現的,但凡是遞迴的,當然也可以用非遞迴的方法來實現,其實就是利用棧
- DFS常用於尋找所有解的問題,需要記錄並且完成整個搜尋,所以很多時候需要去進行剪枝
2.1 典例-樹
先來看一下樹的DFS,例如要遍歷上面那棵樹,DFS就是一條路走到黑,然後走不動往上個路口回,然後看有沒有其他選擇
遞迴實現
public class Solution { private static class Node { public int value; public Node left; public Node right; public Node(int value, Node left, Node right) { this.value = value; this.left = left; this.right = right; } } public static void dfs(Node treeNode) { if (treeNode == null) { return; } // 遍歷節點 process(treeNode) // 遍歷左節點 dfs(treeNode.left); // 遍歷右節點 dfs(treeNode.right); } }
非遞迴實現
壓入根節點
1.彈出就列印
2.if有右孩子,壓入右孩子
3.if有左孩子,壓入左孩子
重複1.2.3
/** * 使用棧來實現 dfs * @param root */ public static void dfsWithStack(Node root) { if (root == null) { return; } Stack<Node> stack = new Stack<>(); // 先把根節點壓棧 stack.push(root); while (!stack.isEmpty()) { Node treeNode = stack.pop(); // 遍歷節點 process(treeNode) // 先壓右節點 if (treeNode.right != null) { stack.push(treeNode.right); } // 再壓左節點 if (treeNode.left != null) { stack.push(treeNode.left); } } }
2.3 典例-圖
思路
假設上幅圖片要從起點(黑色)走到終點(紅色),找一條路徑。
if要用深度優先搜尋的例子來解這道題,那對於每一個格子,都有上下左右四個方向,從這個起始的位置可以發現,我們可以按照左->下->右->上這樣的順序去走路,也就是if左邊能走,那就往左邊走,到了下一個格子後,if左邊能走,那接著走,if左邊不能走了,那就看看下邊能不能走,依次進行,if四個方向都不能走了,那就需要回溯到上一個,然後再看上一個的其他方向能不能走;
具體到這個圖中:先向左走1格-》再向左走一格-》左邊無法走,下邊無法走,上邊無法走,右邊走過了,無路可走-》這條路死路解決不了問題-》回到上一個位置-》同樣無路可走再回退-》到起點後,左邊證明不行了,往下走-》。。。。重複這個過程
- 深度優先的特點就是不管有多少岔路口,都先選一條路走到底,不成功就返回上一個路口選擇下一條路,
程式實現
int goal_x = 9, goal_y = 9 //目標座標
int n = 10, m = 10 //地圖的長寬
int graph[n][m]
int used[n][m]
int px = {-1, 0, 1, 0}
int py = {0, -1. 0, 1} //通過這兩個來進行左下右中的移動
int flag = 0 //到達終點的標誌
void DFS(in graph[][], int used[][], int x, int y)
{
//if目標相同,那證明成功
if(graph[x][y] == graph[goal_x][goal_y]){
print("successful")
flag = 1
return
}
//向四個方向遍歷
for(int i = 0, i < 4, i++){
int new_x = x+px[i], new_y = y + py[i]
if(new_x >= 0 && new_x < n && new_y >= 0 && new_y < m && used[new_x][new_y] == 0 && !flag) { //沒有且滿足邊界的時候
used[new_x][new_y] = 1 //走這個格子
DFS(graph, used, new_x, new_y) //接著以這個格子接著走
used[new_x][new_y] = 0 //回溯設定為0
}
}
}
3.BFS
3.1 特點
- 廣度優先搜尋的主要思路是從一個或多個未訪問過的節點開始,先遍歷這個節點的所有相鄰節點,再遍歷每個相鄰節點的相鄰節點。BFS的特點是一石激起千層浪,從一個節點開始向外擴散。
- DFS是和遞迴或者說是棧一起出現的,而BFS很多時候其實是和佇列一起出現的。這就是兩者的區別。
- BFS常用於尋找單一的最短路徑問題,特點是:搜到就是最優解
3.2 典例-樹
仍然是上面第一個例子,我們換一種遍歷思路,剛才是從一條路先走到頭,這次一層一層的遍歷,這就是bfs
程式實現
/**
* 使用佇列實現 bfs
* @param root
*/
private static void bfs(Node root) {
if (root == null) {
return;
}
Queue<Node> stack = new LinkedList<>();
stack.add(root);
while (!stack.isEmpty()) {
Node node = stack.poll();
System.out.println("value = " + node.value);
Node left = node.left;
if (left != null) {
stack.add(left);
}
Node right = node.right;
if (right != null) {
stack.add(right);
}
}
}
3.3 典例-圖
思路
- 廣度優先在面臨岔路口的時候,會把所有路口都記住,然後向著外面去進行擴散。
程式實現
int n = 10, m = 10;
void BFS(){
queue que //用佇列來進行儲存
int graph[n][m]
int px[] = {-1, 0, 1, 0}
int py[] = {0, -1, 0, 1}
que.push(起點入隊);
while(!que.isEmpty()){
temp = que.pop();
for(int i = 0; i < 4; i++){
if(可以走){
//標記當前格子
//入隊
}
4.總結
- 在樹裡,用的比較多的是dfs,因為樹只有兩個節點,並且有明顯的路徑(向左或者向右),可以直接使用遞迴的方法一次性走完;但是在圖裡,用到較多的是bfs,對於樹而言,佇列裡存的是當前層的節點;對於圖而言,當前佇列裡存的是所有的鄰居節點。
BFS的框架
//計算起點start到終點target的最小距離
int BDS(Node start, Node target){
Queue<Node> queue; //核心結構:佇列;
Set<Node> visited; //在圖中都會用到,因為存在著交叉,會走回頭路,樹中則不需要,因為有next指標;
queue.offer(start); //起點入隊;
visited.add(start);
int step = 0; //記錄擴散步數;
while(queue.isEmpty()){
int size = queue.size();
//從當前佇列中所有節點向與其關聯的節點擴散;
for(int i = 0; i < size; i++){
Node cur = queue.poll();
if(cur == target){
return step; //注意不同題目裡這裡的判斷條件,是否到達終點;
}
for(Node x : cur.adj()){ //這裡指的是當前節點的鄰居節點;
if(!visited.contains(x)){ //還沒走過再加入;不走回頭路;
queue.offer(x);
visited.add(x);
}
}
}
step++; //注意在這裡更新步數;
}
}