1. 程式人生 > >演算法學習--從深度優先搜尋到全排列問題(一)

演算法學習--從深度優先搜尋到全排列問題(一)

直接進入主題,關於深度優先搜尋,發源於資料結構,起初是用來進行圖的遍歷,經過科研人員長時間的研究和總結,已經運用到實際的生產生活中去,用以解決需要大量重複、排列組合的相關問題。

參考書目:

《演算法導論》、《啊哈!演算法》、《資料結構(李建忠翻譯版)》。

關於基本資料結構圖的相關內容,用於本演算法中的內容,主要是關於虛擬碼中的一些解釋:

1.首先理解鄰接連結串列:對於無向圖而言,G的鄰接連結串列表示千萬不能理解為方向。這幅圖僅僅表示這個結點與其他哪幾個結點相連線。只有對於有向圖,才是有確定方向的。

無向圖

有向圖

2.關於Adj[u]:就是與u相鄰接結點的集合。就是在這個集合內的點,與u都有線相連;

3.u.d 和 u.f  兩個時間戳,本文暫時不討論;

下面給出虛擬碼,再來分析:

DFS(G)
    for each vertex u ∈ G.V
        u.code = WHITE
        u.π = nul
    time = 0
    for each vertex u ∈ G.V
        if u.color == WHITE
                DFS-VISIT(G,u)

DFS-VISIT(G,u)
    time = time + 1
    u.d = time
    u.color = GRAY
    for each v ∈ G:Adj[u]
            if v.color == WHITE
                    v.π = u
                    DFS-VISIT(G,u)
    u.color = BLACK
    time = time + 1
    u.f = time

這是原版的虛擬碼,來自《演算法導論》,按照書上的說法,這就是最基本的深度優先演算法。輸入圖可以是有向圖,也可以是無向圖。

下面對虛擬碼進行分析,理解其原理:

//vertex:頂點;
//v.π 前驅結點
//WHITE:未被訪問,GARY:已被訪問,BLACK:該結點的所有鄰接點都訪問完
//關於 time 本文都不討論
DFS(G)
    for each vertex u ∈ G.V             //初始化圖的每一個頂點
        u.code = WHITE                   //為每個結點上白色,表示都未被訪問
        u.π = NUL                        //前驅結點均為空
    time = 0
    for each vertex u ∈ G.V             //遍歷每一個結點   
        if u.color == WHITE              //如果這個點未被訪問過
                DFS-VISIT(G,u)           //開始訪問

DFS-VISIT(G,u)
    time = time + 1
    u.d = time
    u.color = GRAY                       //將這個點塗成灰色,表示已被訪問
    for each v ∈ G:Adj[u]               //遍歷其所有的鄰接點
            if v.color == WHITE          //若該鄰接點未被訪問
                    v.π = u              //標記其前取電,避免迷路
                    DFS-VISIT(G,u)       //遍歷
    u.color = BLACK                      //該結點的所有鄰接點都訪問完,塗成黑色
    time = time + 1
    u.f = time

通過這個分析可以知道,在深度優先遍歷中,從一個點出發,開始往相關聯的鄰接點出發,然後再往相關聯的鄰接點出發;若此路不通,則返回前驅結點,再往前驅結點的相關聯的鄰接點。。。然後迴圈往復,由此可以看出遞迴的思想。

對每一個點重複與前面相同的操作,然後找到進入下一個點入口。這是深度優化搜尋的思想。

另外我認為重要的是,對搜尋的資料之間,應當存在必要的聯絡。能夠利用他們的關聯關係進行相關的遍歷。後面有個例子,我將說明如何在同一個陣列元素之間建立聯絡,不斷遍歷的。

那麼總結歸納深度優先搜尋的要點如下:

1.結點之間存在一些聯絡,這是我們遍歷的條件;

2.對每個結點進行的操作相同;

3.能夠找到進入下一個結點的入口;

4.遞迴的邊界應當清楚。

由上述四個要點,回過頭來看虛擬碼:

DFS(G)
    for each vertex u ∈ G.V
        u.code = WHITE
        u.π = nul
    time = 0
    for each vertex u ∈ G.V         //結點之間相互的聯絡,可以進行遍歷;  邊界
        if u.color == WHITE          //進入下一個結點的入口
                DFS-VISIT(G,u)       //操作

DFS-VISIT(G,u)
    time = time + 1
    u.d = time
    u.color = GRAY                   //塗色操作
    for each v ∈ G:Adj[u]           //結點之間相互的聯絡,可以進行遍歷;   邊界
            if v.color == WHITE      //進入下一個節點的入口
                    v.π = u
                    DFS-VISIT(G,u)   //操作
    u.color = BLACK
    time = time + 1
    u.f = time

均能夠找到相適應的語句。

 

下面看看《啊哈!演算法》提供的例項:

簡而言之題目如下:

1-9個數,全排列。

本題當然能用列舉的方法解決。

但我們對簡單情況,1,2,3 的全排列進行思考和歸納可以發現:

我們得到第一個排列:1,2,3,的過程是,

確定第一個數1,再確定第二個數2,再確定第三個數3;

我們得到第一個排列:1,3,2,的過程是,

確定第一個數1,在確定第二個數3,在確定第三個是2;

等等等等。。。

我們似乎可以得到這樣的過程:

確定一個數{

     確定一個數{

          確定一個數{

          }

     }

}
看吧,這也是一個遞迴的過程吧?這樣我們就能暫時認為,這是個遞迴的操作。

我們具體怎麼確定第一個數?第二個數?第三個數呢?

假定我們隨意確定一個數:1

那麼,我們再次確定數時,只能在剩下的2,3中尋找;

假定確定第二個數:2

那麼,我們再次確定數時,只能是3.

到此,我們大概能得出這樣一個方法:

確定一個數{

     遍歷(關聯的所有數){

        若(這個數沒被選定)

                我可以選擇

     }

}

這就是我們要寫函式的雛形。

//**************************************************************************************************************************************************

現在把這個雛形具體實現,

兩個要點:

一、找到關聯,實現遍歷;

二、選擇一個數;

因為這三個數都存放在陣列中,那我們遍歷的程式碼就可以是:

for(int i=0;i<3;i++){
}

那麼如何跟虛擬碼一樣,實現圖的連通關係呢?

利用迴圈巢狀,可以讓線性的陣列結構產生猶如圖般的多邊結構:

for(int i=0;i<3;i++){
       for(int j=0;j<3;j++)}
       {
{

在複雜情況時,迴圈巢狀過多,可以改成遞迴。

現在遍歷語句的模樣已經有了。再實現選擇語句。

有了上面可以參考深度優化搜尋,我們可以獲得靈感:塗色

為選中的數字塗色,在下一次選擇時不選他,而選擇其他未塗色的。

就可以得到這麼幾句程式碼:

if (color[i] == 0) {
			color[i] = 1;
}

約定0未被訪問,1被訪問;

然後,我們將迴圈巢狀改造成遞迴:

//arry為原資料陣列
dfs(int i,int n){
   for (i; i < n; i++) {
		if (color[i] == 0) {
			color[i] = 1;
			dfs(0);
			color[i] = 0;
		}
	}
}

我們在這裡實現了,遍歷所有的結點及其鄰接點;以及對他們進行選擇性的訪問。

下面完成所有的細節:

void dfs(int i, int *color, int *arry, int n,int *output,int k) {

	if (k == n) {
		for (int j = 0; j < n; j++) {
			cout << output[j];
		}
		cout << endl;
		
		return;
	}
	//output是輸出容器;
	for (i; i < n; i++) {
		if (color[i] == 0) {
			output[k] = arry[i];
			color[i] = 1;
			dfs(0, color, arry,n,output,k+1);
			color[i] = 0;
		}
	}
}

output[k]儲存資料輸出的樣式;

i總是為0能保證充分的遍歷,每個點都能與其他n-1個點發生聯絡;

k+1讓output能向下走,繼續向後儲存資料;

k作為輸出判據;

如果弄懂了上述虛擬碼的深度優先搜尋過程,全排列這個過程不難理解。

#include<iostream>


using namespace std;
void dfs(int i, int *color, int *arry, int n,  int *output, int k);


int main()
{
	int i = 0;
	int arry[3] = { 1,2,4};
	int color[] = { 0,0 ,0};
	int output[3];
	int cnt = 0;
	dfs(i, color, arry, 3, output, 0);
	return 0;
}

void dfs(int i, int *color, int *arry, int n,int *output,int k) {

	if (k == n) {
		for (int j = 0; j < n; j++) {
			cout << output[j];
		}
		cout << endl;
		
		return;
	}
	//output是輸出容器;

	for (i; i < n; i++) {
		if (color[i] == 0) {
			output[k] = arry[i];
			color[i] = 1;
			//cout << i << "sd" << endl;
			dfs(0, color, arry,n,output,k+1);
			color[i] = 0;
		}
	}
}

可以進行測試;

結果如下。

做個筆記,方便以後複習。