演算法學習--從深度優先搜尋到全排列問題(一)
直接進入主題,關於深度優先搜尋,發源於資料結構圖,起初是用來進行圖的遍歷,經過科研人員長時間的研究和總結,已經運用到實際的生產生活中去,用以解決需要大量重複、排列組合的相關問題。
參考書目:
《演算法導論》、《啊哈!演算法》、《資料結構(李建忠翻譯版)》。
關於基本資料結構圖的相關內容,用於本演算法中的內容,主要是關於虛擬碼中的一些解釋:
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;
}
}
}
可以進行測試;
結果如下。
做個筆記,方便以後複習。