搜索详解
搜索
一.dfs和bfs簡介
深度優先遍歷(dfs)
本質:
遍歷每一個點。
遍歷流程:
從起點開始,在其一條分支上一條路走到黑,走不通了就往回走,只要當前有分支就繼續往下走,直到將所有的點遍歷一遍。
剪枝:
如果已經確定這條路沒有我們想要的答案,那么就不用繼續在這條路上走下去了,于是我們就開始走其他的分支或者往回走,這樣節省時間的方法稱之為剪枝。
回溯:
當我們一條路走到頭,往回走時,就叫做回溯。
恢復現場:
當我們回溯的時候,原來這個圖是什么樣的,我們還要變回什么樣。這樣做的目的: 當我們遍歷完這條分支,去遍歷下一條分支的時候,我們需要保證當前圖其他條件的一致性,也就是遍歷每一條分支的時候,當前圖的狀態都是一樣的。保證遍歷每一條分支的時候都是公平的。
廣度優先遍歷(bfs)
遍歷流程: 逐層逐層的遍歷,先遍歷第一層,再遍歷第二層…,也就是遍歷當前節點所能到達的所有子節點。直到遍歷所有的點。不存在剪枝,回溯和恢復現場的操作。
對比dfs和bfs
時間復雜度:
dfs: 因為我們需要枚舉每一個點,以及每一條邊,所示它的時間復雜度為O(n + e) 即點的個數+邊的個數
bfs:跟dfs時間復雜度一樣,都為O(n + e) 不同的是對每個點的訪問順序是不一樣的
用到的數據結構:
dfs: stack
bfs: queue
空間復雜度:
dfs: O(h) h為樹的深度
bfs: O(2^h)
特性:
dfs: 不具有最短性
bfs: 具有最短性
二. 樹與圖的深度優先遍歷(dfs)
樹與圖的深度優先遍歷:
樹其實也是圖的一種
圖: 分為有向圖和無向圖
圖的儲存:
第一種:鄰接矩陣,就是一個二維數組,缺點:當點和邊特別多的時候,存不下,一般用的比較少,而且非常浪費空間
第二種:鄰接表:由n個單鏈表組成,也可以用vector動態數組來實現,但vector有很大的缺點,當點和邊非常大時,用vector動態數組的方法很容易超時,所以我們常用n個但鏈表的方式來存儲圖
鄰接表如何存圖呢:
假設有這樣一個圖:
那么我們可以給每個節點開一個單鏈表,如下圖所示:
這樣我們就把圖用鄰接表的方法存了下來
樹與圖深度優先遍歷的大致流程:一條路走到黑,直到撞到南墻,走不通了,然后往回走,只要有分支就繼續往下走
1.樹與圖的遍歷模板:
鄰接表以h數組為表頭,使用 e 和 ne 數組分別存儲邊的終點和下一個節點
#include<iostream> #include<cstring> using namespace std; const int N = 1e6 + 10; int h[N], e[N], ne[N], idx, n;//這里跟單鏈表一樣,只不過這里是N個頭節點,H[N] bool vis[N]; //判斷是否遍歷過 void add(int a, int b) //鄰接表存樹與圖 {e[idx] = b, ne[idx] = h[a], h[a] = idx++; } void dfs(int cur) {vis[cur] = true;for(int i = h[cur]; i != -1; i = ne[i]){ //遍歷樹int u = e[i];if(!vis[u]){dfs(u);}} } int main() {int a, b;cin >> n;//初始化memset(h, -1, sizeof h);memset(vis, false, sizeof vis);for(int i = 0; i < n; i++){cin >> a >> b;//建樹,雙向圖add(a, b);add(b, a);}dfs(1);return 0; }這樣我們就遍歷了每個點。
2.樹的dfs序
? 一般來講,我們在對樹進行深度優先遍歷時,對于每個節點,在剛進入遞歸以后以及即將回溯前各記錄一次該點的編號,最后產生的長度為2N的節點序列被稱為樹的dfs序
void dfs(int x) {a[++m] = x;v[x] = 1;for(int i = h[x]; i != -1; i = ne[i]){int y = e[i];if(v[y])continue;dfs(y);}a[++m] = x; }3.樹的深度
? 我們已知根節點的深度為0.若節點x的深度為d[x],則它的子節點的深度為d[y] = d[x] + 1
void dfs(int x) {v[x] = 1;for(int i = h[x]; i != -1; i = ne[i]){int y = e[i];if(v[y])continue;d[y] = d[x] + 1;dfs(y);} }4.連通圖的劃分
? 假設從x點開始一次遍歷,就會訪問x能夠到達的所有的點和邊,因此,通過多次深度優先遍歷,可以劃分出一張無向圖中的各個連通圖。同理,對一個森林進行深度優先遍歷,可以劃分森林中每棵樹
? cnt表示無向圖包含的連通塊的個數, v數組標記了每一個點屬于哪個連通塊
void dfs(int x) {v[x] = cnt;for(int i = h[x]; i != -1; i = ne[i]){int y = e[i];if(v[y])continue;dfs(y);} } for(int i = 1; i <= n; i++){if(!v[i]){cnt++;dfs(i);} }三.樹與圖的廣度優先遍歷
? 樹與圖的廣度優先遍歷需要使用一個隊列來實現。起初,隊列中僅包含一個起點。在廣度優先遍歷過程中,我們不斷從隊頭取出一個節點x,對于x面對的多條分支,將所有x能夠達到的下一個節點插入隊尾,重復執行上述過程直到隊列為空
1.廣度優先遍歷模板
void dfs() {memset(d, 0, sizeof d);queue<int> q;q.push(1);d[1] = 1; //d數組表示節點的深度while(q.size()){ //只要隊列不為空int x = q.front(); //取出隊頭q.pop();for(int i = h[x]; i != -1; i = ne[i]){ //遍歷x能夠到達的所有下一個節點int y = e[i];if(d[y])continue; d[y] = d[x] + 1; //深度+1q.push(y);}} }? 在上面的代碼中,d數組表示從起點 1 走到點 x 需要經過的最少點數. 廣度優先遍歷是一種按照層次順序進行訪問的方法, 它具有如下倆個重要的性質:
? 1.在訪問完所有的第 i 層節點后,才會開始訪問第 i + 1 層節點
? 2.廣度優先遍歷隊列中的元素關于層次滿足倆段性和單調性, 即隊列中至多包含倆個層次的節點, 其中一部分屬于第 i 層, 一部分屬于 i + 1 層,并且所有的第 i 層節點都排在第 i + 1 層節點之前
2.拓撲排序
? 給定你一個無向圖,若一個由圖中所有點構成的序列A滿足:對于圖中的每條邊 (x, y),x 在A中都出現在y之前,則稱A是該有向圖的一個拓撲排序
? 入度: 在有向圖中,以節點 x 為終點的有向邊的條數被稱為 x 的入度
? 出度: 在有向圖中,以節點 x 為起點的有向邊的條數被稱為 x 的出度
? 拓撲排序非常簡單,我們只需要不斷選擇圖中入度為0的節點 x , 然后把 x 連向的點的入度減1,我們可以結合廣度優先遍歷的框架來實現:
? 1.建立空的拓撲排序A。
? 2.預處理出所有點的入度deg[i],起初把所有入度為0的點入隊
? 3.取出隊頭節點x,把x加入拓撲序列A的末尾
? 4.對于x出發的每條邊(x, y)把 deg[y] 減 1 。若被減為0, 則把y入隊
? 5.重復3~4過程,直到隊列為空,我們便求出了拓撲序列A
void add(int x, int y) //建邊 {e[cnt] = y, ne[cnt] = h[x], h[x] = cnt++; } void topsort() {queue<int> q;for(int i = 1; i <= n; i++)if(deg[i] == 0)q.push(i); //將入度為0的點加入到隊列中while(q.size()){int x = q.front();q.pop();a[++t] = x; //將x加入到拓撲序中for(int i = h[x]; i != -1; i = ne[i]){int y = e[i];deg[y]--; //入度--if(deg[y] == 0)q.push(y);//如果入度為0,添加到隊列中去}} } int main() {cin >> n >> m;for(int i = 1; i <= m; i++){int x, y;cin >> x >> y;add(x, y);}topsort();for(int i = 1; i <= n; i++)//輸出拓撲序printf("%d ", a[i]);cout << endl; }四.迭代加深
? 深度優先搜索每次選定一個分支,不斷深入,直至到達遞歸邊界才回溯。這種策略帶有一定的缺陷。如果搜索樹每個節點的分枝數非常多, 而答案在某個較淺的節點上。如果深搜在一開始選錯了分支,就很可能在不包含答案的深層子樹上浪費太多的時間
? 此時,我們可以從小到大限制搜索的深度如果在當前深度限制下找不到答案,就把深度限制增加,重新進行一次搜索,這就是迭代加深的思想。
例題:加成序列(poj2248)
? 滿足如下條件的序列X(序列中元素被標號為1、2、3…m)被稱為“加成序列”:
? 1、X[1]=1
? 2、X[m]=n
? 3、X[1]<X[2]<…<X[m-1]<X[m]
? 4、對于每個 k(2 ≤ k ≤ m)都存在兩個整數 i 和 j (1 ≤ i,j ≤ k?1,i 和 j 可相等),使得 X[k]=X[i]+X[j]。
? 你的任務是:給定一個整數n,找出符合上述條件的長度m最小的“加成序列”。
? 如果有多個滿足要求的答案,只需要找出任意一個可行解。
搜索框架:依次搜索序列中的每個位置k, 枚舉 i 和 j 作為分支,把 X[i] 和 X[j] 的和填到 X[k] 上,然后遞歸填寫下一個位置。
? 加入以下剪枝:
? 1.優化搜索順序:為了讓序列中的數盡快逼近n,在枚舉 i 和 j 時從大到小枚舉
? 2.排除等效冗余
? 對于不同的 i 和 j ,X[i] 和 X[j] 可能是相等的,我們可以在枚舉是用一個 bool 類型的數組對 X[i] 和 X[j] 進行判重,避免重復搜索同一個和
? 3.我們可以采用迭代加深的方法進行搜索, 從1開始限制搜索深度,若搜索失敗就增加深度限制重新搜索,直到找到一組解時即可輸出答案
#include<iostream> #include<cstring> using namespace std;const int N = 110; int a[N]; bool vis[N]; int n, k; bool dfs(int u, int k) {//如果達到搜索限制,判斷a[u - 1]是否等于nif(u == k)return a[u - 1] == n;//遍歷前邊的元素for(int i = u - 1; i >= 0; i--){for(int j = i; j >= 0; j--){int s = a[i] + a[j];//如果和已經出現過,或者不滿足要求,剪枝掉if(vis[s] || s > n || s <= a[u - 1])continue;a[u] = s;if(dfs(u + 1, k))return true;}}return false; } int main() {a[0] = 1;while(cin >> n && n){int k = 1;while(!dfs(1, k)){ //不斷增加搜索限制k直到得到正確的答案memset(vis, false, sizeof vis);k++;}for(int i = 0; i < k; i++)cout << a[i] << ' ';cout << endl;}return 0; }總結
- 上一篇: 多进程与多线程通信同步机制
- 下一篇: 堆的简单实现