1. 程式人生 > 實用技巧 >深度優先搜尋

深度優先搜尋

深度優先搜尋

概念

深度優先搜尋屬於演算法的一種,是一個針對圖和樹的遍歷演算法,英文縮寫為DFS即 Depth First Search。

例如,在下面的樹結構中找出節點1。

採取的策略是按照深度優先的方式進行,也就是一條路走到底。每次進入都先走左邊,直到左邊不能走了,退回一步,選擇沒有走過的路(右邊)。

其中搜索實際上指得是一種窮舉策略,按照該策略,將所有的可行方案全部列舉出來,不斷進行嘗試,直到找到問題的解。

舉例說明

1、全排列

【題目描述】

給定一個由不同的小寫字母組成的字串,輸出這個字串的所有全排列。

我們假設對於小寫字母有‘a’ <‘b’ < ... <‘y’<‘z’,而且給定的字串中的字母已經按照從小到大的順序排列。

【輸入】

只有一行,是一個由不同的小寫字母組成的字串,已知字串的長度在1到6之間。

【輸出】

輸出這個字串的所有排列方式,每行一個排列。要求字母序比較小的排列在前面。

【樣例輸入】

abc

【樣例輸出】

abc
acb
bac
bca
cab
cba

題目分析:對於3個字元,我們可以假定有3個格子,每個格子中放一個字元,要求形成所有的排序順序。明顯按照字母順序,我們從前往後選擇即可,這意味著先將a放在第一個格子,然後放b在第二個格子,然後c。當3個格子都被放滿了,說明形成了第一個順序,然後考慮第二個格子放c,第三個放b,依次類推。

需要注意的是,我們的策略是,從沒有選擇過的字元中選擇一個放入格子中,因此,我們需要給每一個字元做一個標記,用於表示這個字元的選擇狀態。我們在進行回退操作時,也要將選擇狀態重置為未選擇。

#include <iostream>
#include <cstring>
using namespace std;
#define N 7
char a[N], b[N];
int n;
bool vst[N]; //用於記錄選擇狀態

void dfs (int step) { //step當前放的格子數
	if (step == n) {
		for (int i = 0; i < n; i++)
			cout << a[i];
		cout << endl;
		return ;  
	}
	for (int i = 0; i < n; i++) { // 往step格子中存
		if (!vst[i]) {
			vst[i] = 1;
			a[step] = b[i]; // 第step個格子中存字元b[i]
			dfs(step+1);
			vst[i] = 0;

		}
	}
}

int main () {
	cin >> b;
	n = strlen(b);
	dfs (0);
	return 0;
}

2、N皇后

【問題描述】
在N*N的棋盤上放置N個皇后(n<=10)而彼此不受攻擊(即在棋盤的任一行,任一列和任一對角線上不能放置2個皇后),程式設計求解所有的擺放方法。
【輸入格式】
輸入:n
【輸出格式】
輸出共有多少種擺放方案。
【輸入樣例】

4

【輸出樣例】

2

對於樣例4皇后來說,我們的搜尋策略一定是試著去窮舉所有的可能性,那麼在整個棋盤上,一定會逐行的進行嘗試,嘗試該放置是否能放置皇后。如下圖所示

首先將皇后 放置1行1列的位置,那麼相應的對角線,橫豎列都不能放置。接下來從第2行找到第一個能放置的位置放。

放完後發現第3行沒有位置了,不滿足條件。回退一步,放置2行末尾。

接著從3行放置,發現4行不滿足條件,繼續回退到第一個皇后的放置位置,嘗試放置第1行第2個位置。

繼續往後放置皇后

可以發現,成功的找出了一種答案。

此時需要的考慮的問題就是,如何在程式中表示?思考上述的過程發現

1、從第一個位置開始放置皇后,嘗試放置,並且放好後,在同行、同列、同對角線做好不能放置皇后的標記。

  • 對於某個位置的皇后而言,可以通過陣列row[i]表示第i行是否放置了皇后,col[i]表示在第i列是否放置了皇后
  • 而對角線注意有兩條,這兩條對角線的特點是一條對角線橫縱座標之和相等,另外一條橫縱座標之差相等,因此可以用陣列d1[i]、d2[i]表示兩條對角線上是否放置了皇后。

2、當無法完成放置的時候,需要進行回退操作。實際上利用好遞迴的過程即可。

搜尋時可以按層進行

參考程式

#include <iostream>
#define N 15
using namespace std;

int cnt, n;
int r[N], c[N], d1[N*2], d2[N*2];

void dfs (int floor) {
    if (floor == n+1) { //順利填完最後一層。方案數加1
        cnt ++; 
        return ;
    }
    for (int i = 1; i <= n; i++) { //在floor層中尋找可以放的位置
        if (!c[i] && !d1[floor+i] && !d2[n+floor-i]) { // 同行、列、對角線沒有皇后
            c[i] = d1[floor+i] = d2[n+floor-i] = 1;
            dfs (floor+1); //找下一層
            c[i] = d1[floor+i] = d2[n+floor-i] = 0;
        }
    }
}
int main () {
    cin >> n;
    dfs (1);
    cout << cnt;
    return 0;
}

小結

  • 深度優先搜尋的做法是從一個起點開始一直按照某種策略搜尋遍歷下去,直到滿足邊界條件或者沒有資料遍歷,則從第二個點開始遍歷搜尋,直到所有資料情況遍歷完成。
  • 實際上深搜是根據初始條件和搜尋策略構造一棵解答樹並在過程中不斷尋找目標狀態的節點的過程。

題目練習

1、選數

已知 n 個整數 x1,x2,…,xn,以及1個整數k(k<n)。從n個整數中任選k個整數相加,可分別得到一系列的和。例如當n=4、k=3、4個整數分別為3、7、12、19時,可得全部的組合與它們的和為:

3+7+12=22
3+7+19=29
7+12+19=38
3+12+19=34

現在,要求你計算出和為素數共有多少種。而上述例子中只有一種和為素數:3+7+19=29。

【輸入格式】

n k
x1 x2 ... xn

【輸出格式】

輸出滿足條件的數有多少種

【輸入樣例】

4 3
3 7 12 19

【輸出樣例】

1

易錯點:從n個數中選k個數,可能會有重複,例如x1,x2,x3,也可能選成x2,x1,x3。那麼為了去重,只要保證選擇的數在原序列的位置是遞增的即可。

參考程式

#include <iostream>
using namespace std;
#define N 25

int a[N];
int n, k, cnt;
bool vst[N];

bool is_prime (int x) {
    if (x < 2) return false;
    for (int i = 2; i * i <= x; i++) 
        if (x % i == 0)
            return false;
    return true;
}

void dfs (int sum, int step, int before) {
    if (step == k) {
        if (is_prime(sum)) 
            cnt ++;
        return ;
    }
    for (int i = 1; i <= n; i++) {
        if (!vst[i] && i > before) {
            vst[i] = 1;
            dfs(sum+a[i], step+1, i);
            vst[i] = 0;
        }
    }
}

int main () {
    cin >> n >> k;
    for (int i = 1; i <= n; i++) 
        cin >> a[i];
    dfs(0, 0, 0);
    cout << cnt;
    return 0;
}

2、Lake Counting

【問題描述】

有一個大小為N×M的園子,雨後積起了水。八連通的積水被認為是連線在一起的。請求出園子裡總共有多少水窪?假定'w'表示水窪,'.'表示沒有水窪。所謂八連通指的是,在'W'的上下左右8個方向如果也有水窪,那麼它們是連通的。例如下面的兩個水窪是連通的。

. . W

. W .

. . .

【限制條件】

N,M <= 100

【輸入格式】

第一行n和m

接下來n+1行,每行m個字元,表示園子的情況

【輸出格式】

一行,表示總共多少個水窪

【樣例輸入】

10 12
W........WW.
.WWW.....WWW
....WW...WW.
.........WW.
.........W..
..W......W..
.W.W.....WW.
W.W.W.....W.
.W.W......W.
..W.......W.

【樣例輸出】

3

參考程式

#include <iostream>
#include <string>
using namespace std;
const int MAXN = 100 + 5;
string a[MAXN];
int n, m;
void dfs(int x, int y)
{
    a[x][y] = '.';
    for (int dx = -1; dx <= 1; ++dx)
    {
        for (int dy = -1; dy <= 1; ++dy)
        {
            int nx = x + dx, ny = y + dy;
            if (nx >= 0 && nx < n &&
                ny >= 0 && ny < m &&
                a[nx][ny] == 'W')
                    dfs(nx, ny);
        }
    }
    return ;
}
int main()
{
    int ans = 0;
    cin >> n >> m;
    for (int i = 0; i < n; ++i) cin >> a[i];
    for (int i = 0; i < n; ++i) 
        for (int j = 0; j < m; ++j)
        	if (a[i][j] == 'W')
            {
                dfs(i,j);
                ans++;
            }
    cout << ans << endl;
}

框架

可以參考這個框架進行思考。

void dfs(....) { // DFS的引數由具體搜尋策略而定
    if(邊界條件) {//列印、結束、比較
        做相應處理
    } else {
        for(...) { // 列舉同層的每一種可能的情況
            if() {滿足條件
                設定條件    //存陣列、設約束(已訪問等)、加總量(求總和等)
                bfs(...) // 進入下一層進行搜尋
                恢復條件設定
            }
        }
    }
}