深搜(DFS)

深度優先搜索——不撞南牆不回頭

深搜是作爲一種遍歷或搜索圖和樹的算法。

思想:不撞南牆不回頭!

首先選取一個未訪問的點作爲源節點。從源節點選取一條路一直往下走,沿着當前頂點的邊,訪問這條路線,直到走不下去爲止。這時返回上一頂點,繼續試探訪問此頂點的其餘子頂點,直到當前頂點的子頂點都被訪問過,那麼,返回上一頂點,繼續重複。從而實現遍歷。

來個簡單的例子模擬DFS的過程:

(首先,我們默認優先選擇靠左或靠下的元素進行訪問 )
在這裏插入圖片描述
選擇A作爲源節點------>(繼續下一層深搜)訪問到了B------->(繼續下一層深搜)訪問到了D------>(繼續下一次深搜)搜索到了C------>(此時發現C的所有下一節點都已經被訪問過了)返回C的上以節點D(因爲是從D訪問到C來的,所以不要走回到C的其他上節點)------>我們再遍歷D的其他子節點E------->(發現E沒有子節點)返回上一節點D------>(發現D沒有未被訪問的子節點)返回B------>(B沒有被訪問的子節點)返回A------>(A沒有未被訪問的子節點)DFS完畢
因此,此圖的深搜遍歷順序:A—>B—>D—>C—>E

總結深搜的思路:沿着某條路徑遍歷,直到末端,然後回溯,再遍歷另一條路徑(走沒有走過的岔路口)做同樣的遍歷,直到所有的節點都被訪問,即回溯到源節點並且源節點已無未被訪問的子節點。

舉個例子:數的全排列問題,輸入一個數n,輸出1-n的全排列。
做如下分析:

我們把問題抽象:也就是說,有1-n個數字,要分別放到固定一列的n個盒子裏,問分別有哪些不同的放法。
在這裏插入圖片描述

採用深搜的思想(不撞南牆不回頭):
首先我們默認優先放置最小的小數字-----這步相當於選擇先走靠左或靠下的路。
開始放置:1放在第一個,2放在第二個···n放在第n個,這樣我們已經走到了頭,所有的盒子我們都放過數字了。回溯一步並把第n個盒子裏的數字n取出,我們當前處於第n-1個盒子處。當前盒子放的是n-1這個數字,我們把它取出來。這時,我們手裏有n-1和n兩個數字。採用最小數放置原則,但是數字n-1當前剛從這個盒子裏取出來。爲了避免重複,就不能再放進去了。所以,將目前未放置的數中除了n-1之外的最小數n放入第n-1個盒子裏面,再往下走,走到了第n個盒子,手裏剩下了一個數字n-1,自然放入第n個了。
此時我們已經擁有了兩種排列:
1,2,3,···,n-1,n 和 1,2,3,···,n,n-1
繼續回溯:當前在第n個盒子,回溯走到了第n-1個盒子(並取出了第n個盒子裏的數字n-1),我們發現這兩數字都在第n-1盒子放過,那就只能再回溯了(順便拿起第n-1個盒子的數字n),走到了第n-2個盒子上,取出這個盒子的數n-2。當前,我們的狀態時,在第n-2盒子上,手裏有數字,n-2,n-2,n;那麼遵循放小原則,並且不能重複,把n-1這個數字放入n-2這個盒子中,繼續往下走,先把n-2放入第n-1盒子裏,故第n個盒子放數字n了。
此時又出現了一種排列:
1,2,3,···,n-1,n-2,n;
再次從第n盒子回溯到n-1盒子:同理產生了 1,2,3,···,n-1,n,n-2 排列
因爲不能往下走了,只能再次回溯到第n-2個盒子,放入n,········
不斷重複如上操作,終會回溯到第一個盒子,第一個盒子把最大的數字n都放完之後,再次回溯到第一個盒子,第一個盒子已經無法放別的數字了。那便是全排列結束。

附上此題完整代碼:

#include <stdio.h>
int a[101];//盒子,目前只能放最多100個盒子,你可以自己加,但是要同時修改book的個數 
int book[101]={0};//標記數組,標記是否已經放入 
void dfs(int step,int n)//當前在第幾個盒子上,總共有多少數 
{
	int i;
	if(step==n+1){//如果站在第n+1個盒子面前了,那說明,已經放好所有的了 
		for(i=1;i<=n;i++){//輸出這個組合 
			printf("%d ",a[i]);
		}
		printf("\n");
		return;//返回上一層,也就是調用它的地方 (回溯) 
	}
	for(i=1;i<=n;i++){//遵循最小原則  放入數字 
		//判斷數字是否已經放入到之前的盒子
		if(book[i]==0){//數字並未放入 
			a[step]=i;//放入
			book[i]=1;//標記已經放入 
			//繼續需要往下一個盒子走
			dfs(step+1,n); 
			//走完這個盒子之後的所有操作,已經又回溯到這個盒子了,馬上就要在此盒子放入下一個數了,所以我們需要將剛纔的數字收回 
			book[i]=0; 
		} 
	} 
	return;
} 
int main(){
	int n;
	scanf("%d",&n);
	dfs(1,n);
	return 0;
} 

通過上述,可以總結出來深搜的基本模型:

void  dfs(int  step)//當前狀態
{
	//判斷邊界條件
	if(xxx){
		return//返回上一步
	}
	//嘗試每一種可能
	for(xxx){
		//判斷是否符合條件
		if(xxx){
			//繼續走下一步
			dfs(step+1);
		}
	}
	return//全部遍歷做完後返回
}

進入經典----->DFS解決迷宮問題
迷宮有n行m列的單元格組成,每個單元格要麼是空地,要麼是障礙物,找到一條從起點到終點的最短路徑。
輸入迷宮地圖和始末點座標。
比如說是這樣的圖:
在這裏插入圖片描述
菱形爲起始點,心形爲終點,六角星爲障礙物。我們的解決方法便是,從起始點開始,往下深搜,每次往進搜索一步便讓步數+1,搜索到心形時記錄下步數,並回溯上一步,搜索別的路徑。直到搜索完。
分析得知,我們每次往進遍歷時有四個方向(上下左右)。我們模仿深搜的基本模型:

void  dfs(int x,int y,int  step)//當前狀態
{
	int next[4][2]={{0,1},//向右 
				{1,0},//向下 
				{0,-1},//向左 
				{-1,0}};//向上
	int tx,ty,i;
	//判斷邊界條件
	if(x==p&&y==q){//走到了終點 
		if(step<min){
			min=step;
		} 
		return;//返回上一步
	}
	//嘗試每一種可能(枚舉四種走法) 
	for(i=0;i<4;i++){
		//計算下一點座標 
		tx=x+next[i][0];
		ty=y+next[i][1];
		//判斷是否符合條件(下一點座標是否超範圍,是否是障礙物或已在路徑中) 
		if(tx<1||ty<1||tx>n||ty>m){//超範圍,則continue下一種走法 
			continue; 
		}
		//可以思考一下爲何不把超範圍和下一個判斷條件放在一起呢 ??? 
		if(book[tx][ty]==0&&a[tx][ty]==0){//是否是障礙物或已在路徑中,0代表通暢,1代表障礙 
			book[tx][ty]=1;//標記 
			//繼續走下一步
			dfs(tx,ty,step+1);
			book[tx][ty]=0;//消除標記 
		} 
		
	}
	return;//全部遍歷做完後返回
}

完整代碼:

#include <stdio.h>
int a[51][52];//地圖 
int book[52][52];//標記 
int n,m,p,q,min=99999999;//m,n爲地圖大小,p,q爲終點,min表示最短距離 
void  dfs(int x,int y,int  step)//當前狀態
{
	int next[4][2]={{0,1},//向右 
					{1,0},//向下 
					{0,-1},//向左 
					{-1,0}};//向上
	int tx,ty,i;
	 
	//判斷邊界條件
	if(x==p&&y==q){//走到了終點 
		if(step<min){
			min=step;
		} 
		return;//返回上一步
	}
	//嘗試每一種可能(枚舉四種走法) 
	for(i=0;i<4;i++){
		//計算下一點座標 
		tx=x+next[i][0];
		ty=y+next[i][1];
		//判斷是否符合條件(下一點座標是否超範圍,是否是障礙物或已在路徑中) 
		if(tx<1||ty<1||tx>n||ty>m){//超範圍,則continue下一種走法 
			continue; 
		}
		//可以思考一下爲何不把超範圍和下一個判斷條件放在一起呢 ??? 
		if(book[tx][ty]==0&&a[tx][ty]==0){//是否是障礙物或已在路徑中,0代表通暢,1代表障礙 
			book[tx][ty]=1;//標記 
			//繼續走下一步
			dfs(tx,ty,step+1);
			book[tx][ty]=0;//消除標記 
		} 
		
	}
	return;//全部遍歷做完後返回
}
int main(){
	int i,j,sx,sy;//i,j用於循環,sx,xy表示初始座標
	scanf("%d %d",&n,&m);//輸入地圖大小
	//讀入迷宮地圖 ,0代表通暢,1代表障礙 
	for(i=1;i<=n;i++){
		for(j=1;j<=m;j++){
			scanf("%d",&a[i][j]);
		}
	} 
	scanf("%d %d %d %d",&sx,&sy,&p,&q);//輸入起始和結束座標
	book[sx][sy]=1;//起點已經在路徑中 
	dfs(sx,sy,0);//從起點深搜 
	printf("%d",min); 
	return 0 ;
}

注意:深搜時需要注意代碼中是否取消標記。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章