dfs好久之前就想寫一篇了,終於找到藉口了 爲了老弟來寫一篇深度優先搜索吧!
基本概念
深度優先搜索(deepth first search)又叫dfs,基本思想是從給定點出發,選定一條路就一直走下去,不撞南牆不回頭那種。不管你喜不喜歡,它就是這樣專一。
一直走到頭撞了南牆發現當前路徑不可行的時候,纔回頭這時候的回頭是通過 棧 + 回溯 來實現的,那麼具體怎麼搞↓
例題
dfs應用有很多,最基本的就是迷宮求路徑數的問題。比如我們選擇如下的簡單迷宮↓從a開始走,每次只能上下左右四個方向走一格,直到b終點結束。求所有可以走的路。
算法思維
首先確認當前位置沒有問題是可以走的。那麼從當前位置出發,選一個方向開始試探,如果試探的這個方向可以走,就繼續從這個新位置向下選個方向試探……直到撞了南牆就回頭,或者到達迷宮出口爲止
是不是很熟悉?對!每到一個位置我們要做的步驟都是一!樣!的!所以我們可以通過遞歸來實現。
遞歸就必須考慮遞歸出口(也叫基線條件baseline、邊界條件啥啥都行隨你開心)我們的遞歸出口是啥?走到哪裏就可以結束這個函數了?(其實上面已經說了)走到出口!
三個問題
看代碼之前,停下來思考三個問題:
- 什麼叫可以走?
- 如何選擇試探的方向?
- 怎麼回頭?
回答
- 一個點可以走就是滿足:(1)這個點在這個圖中 (2)這個點是通路(在簡單迷宮中,不是牆壁即可) 實現上用一個
if
判斷一下就好 - 選擇試探的方向,就是下一步應該往哪走有幾種可能的選擇,就要遍歷這些選擇(單個選擇直接加上以後遞歸,多個選擇用
for
循環來遍歷所有) - 回頭就是回溯,因爲要回頭說明你的這個點已經不可行了,那麼就把你對這個位置做過的事情全部撤銷。如果爲了走這個位置,需要把這個位置標記成x,那就還原這個位置原來的狀態。
比如我們的迷宮問題,從一個點可達的點就是它的上下左右四個位置所以我們遍歷這四個位置。在一個用二維數組表示的圖中如何實現?a點(x,y)的上面就是(x-1,y),下面是(x+1, y),左:(x, y-1) 右:(x, y+1)
當然你也可以用四個遞歸的調用語句來實現↓ 這裏 g 表示圖
g[x][y] = ' '; //標記通路
dfs(x + 1, y); //往下試探
dfs(x - 1, y); //往上試探
dfs(x, y + 1); //往右試探
dfs(x, y - 1); //往左試探
g[x][y] = 'o'; //回溯,撤銷操作
取巧一點的辦法(不想看就用上面那個法子試探,可以跳到代碼描述了):
設一個4x2
的二維數組(不理解二維數組的話用兩個一維也是一樣的,具體方法放在後面說),第一維度有四個量,表示上下左右四個方向,第二維度有兩個量一個表示x
一個表示y
,即往這個方向試探的話,x
或y
怎麼變化↓
int go[4][2] = {{1, 0},{-1, 0},{0, 1},{0, -1}};//上下左右試探四個方向
二維數組的試探方法如上,每個裏面的的花括號代表一個方向。比如,我站在(x, y)
這個位置如果想要往上走,用(x + go[1][0], y + go[1][1])
就能順利得到(x - 1, y)
。
用這樣一個數組是不是太麻煩了?不是,如果你用一個數組,上下左右試探這件事情就可以用一個 j = 0~3
的for循環來實現遍歷。循環內部(x + go[j][0], y + go[j][1])
g[x][y] = ' '; //標記通路
for(int j = 0; j < 4; j++) //試探四個方向
dfs(x + go[j][0], y + go[j][1]);
g[x][y] = 'o'; //回溯,,撤銷操作
用兩個一位數組來試探也是一樣的,只是把描述x和y的位置變化分成了兩個數組而不是放在第二維度而已。文末會提。
代碼描述
寫代碼之前,再來過一遍思路:
- 判斷當前這個位置能不能走
- 能走就標記,然後從這個位置開始上下左右四個方向試探
- 遞歸出口是走到了終點(當前位置==終點位置)
ps:這裏迷宮的通路用o
表示,牆壁用x
以及表示圖的二維矩陣g和一些變量我都用了全局的。不想這麼寫可以放在參數裏面
#include<bits/stdc++.h>
using namespace std;
const int N = 505; //假設地圖大小不超過這個範圍
char g[N][N];
int n, tx, ty; //圖的實際大小,終點座標
int go[4][2] = {{1, 0},{-1, 0},{0, 1},{0, -1}};//上下左右試探四個方向
void printGraph(){
//輸出圖
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++){
cout<<g[i][j];
}
cout<<endl;
}
}//printGraph()
void dfs(int x, int y){
//從圖g的當前位置(x,y)走到終點(tx,ty)的深搜
if(x == tx && y == ty){ //走到終點
g[x][y] = ' '; //終點也標記一次通路,或者你也可以用其它特殊字符標記終點
printGraph();
return;
}
if(g[x][y] == 'o' && x >= 0 && y >= 0 && x < n && y < n){//這個點可以走嗎?
g[x][y] = ' '; //標記通路
for(int j = 0; j < 4; j++){ //試探四個方向
dfs(x + go[j][0], y + go[j][1]);
}
g[x][y] = 'o'; //回溯,能執行到這裏說明這個點已經不可行或者可行解已經輸出。還原這個位置的狀態
}
}//dfs()
int main(){
int x, y;
cin>>n; //圖的邊長
cin>>x>>y; //起點
cin>>tx>>ty; //終點
for(int i = 0; i < n; i++)
for(int j = 0; j < n; j++)
cin>>g[i][j];
cout<<endl;
dfs(x, y);
return 0;
}
測試用例
8
0 0
7 7
oxxxxxxx
oooooxxx
xoxxooox
xoxxoxxo
xoxxxxxx
xoxxooox
xooooxoo
xxxxxxxo
好了就是這樣了,是不是還蠻簡單的?其實思路就三句話:
- 判斷當前點可不可行
- 可行就修改這個點的狀態,然後遞歸試探這個點可達的所有點
- 試探完成就回溯,還原這個點原始的狀態
如果需要求路徑條數,改一改遞歸出口的操作就可以了。深搜適合求路徑數量(可行解數),而廣搜適合求最短路(最優可行解),下次寫廣搜吧。。
兩個一位數組做試探
啊,差點忘了兩個一位數組做試探。就是分別用兩個一位數組來表示x和y的試探方向。
int dy[4] = {0,1,0,-1}; //控制左右移動
int dx[4] = {-1,0,1,0}; //控制上下移動
注意用兩個一位數組的時候,對應下標要匹配。即往上走的時候用的是(x + dx[j], y + dy[j])
這個j
對應的位置在兩個數組中要匹配