24暑假集訓day5下午

Yantai_YZY發表於2024-08-03

DFS

本質:一種用於遍歷或搜尋樹或圖的演算法。所謂深度優先,就是說每次都嘗試向更深的節點走。

該演算法講解時常常與 BFS 並列,但兩者除了都能遍歷圖的連通塊以外,用途完全不同,很少有能混用兩種演算法的情況。

關鍵:

遞迴呼叫自身

對其訪問過的點打上訪問標記,在遍歷圖時跳過已打過標記的點,以確保 每個點僅訪問一次

虛擬碼:

DFS(v) // v 可以是圖中的一個頂點,也可以是抽象的概念,如 dp 狀態等。
  在 v 上打訪問標記
  for u in v 的相鄰節點
    if u 沒有打過訪問標記 then
      DFS(u)
    end
  end
end

性質

該演算法通常的時間複雜度為 \(O(n+m)\),空間複雜度為 \(O(n)\),其中 \(n\) 表示點數,\(m\) 表示邊數。注意空間複雜度包含了棧空間,棧空間的空間複雜度是 \(O(n)\) 的。在平均 \(O(1)\) 遍歷一條邊的條件下才能達到此時間複雜度,例如用前向星或鄰接表儲存圖;如果用鄰接矩陣則不一定能達到此複雜度。

實現

用棧(Stack)為遍歷中節點

vector<vector<int>> adj;  // 鄰接表
vector<bool> vis;         // 記錄節點是否已經遍歷

void dfs(int s) {
  stack<int> st;
  st.push(s);
  vis[s] = true;

  while (!st.empty()) {
    int u = st.top();
    st.pop();

    for (int v : adj[u]) {
      if (!vis[v]) {
        vis[v] = true;  // 確保棧裡沒有重複元素
        st.push(v);
      }
    }
  }
}

遞迴實現

函式在遞迴呼叫時的求值如同對棧的新增和刪除元素的順序,故函式呼叫所佔據的虛擬地址被稱為函式呼叫棧(Call Stack),DFS 可用遞迴的方式實現。

以 鄰接表(Adjacency List) 作為圖的儲存方式:

vector<vector<int>> adj;  // 鄰接表
vector<bool> vis;         // 記錄節點是否已經遍歷

void dfs(const int u) {
  vis[u] = true;
  for (int v : adj[u])
    if (!vis[v]) dfs(v)
}

以 鏈式前向星 為例:

void dfs(int u) {
  vis[u] = 1;
  for (int i = head[u]; i; i = e[i].x) {
    if (!vis[e[i].t]) {
      dfs(v);
    }
  }
}

DFS 序列

DFS 序列是指 DFS 呼叫過程中訪問的節點編號的序列。

我們發現,每個子樹都對應 DFS 序列中的連續一段(一段區間)。

括號序列

DFS 進入某個節點的時候記錄一個左括號 (,退出某個節點的時候記錄一個右括號 )。

每個節點會出現兩次。相鄰兩個節點的深度相差 \(1\)

一般圖上 DFS

對於非連通圖,只能訪問到起點所在的連通分量。

對於連通圖,DFS 序列通常不唯一。

注:樹的 DFS 序列也是不唯一的。

在 DFS 過程中,透過記錄每個節點從哪個點訪問而來,可以建立一個樹結構,稱為 DFS 樹。DFS 樹是原圖的一個生成樹。


BFS

是圖上最基礎、最重要的搜尋演算法之一。

所謂寬度優先。就是每次都嘗試訪問同一層的節點。 如果同一層都訪問完了,再訪問下一層。

這樣做的結果是,BFS 演算法找到的路徑是從起點開始的 最短 合法路徑。換言之,這條路徑所包含的邊數最小。

在 BFS 結束時,每個節點都是透過從起點到該點的最短路徑訪問的。

演算法過程可以看做是圖上火苗傳播的過程:最開始只有起點著火了,在每一時刻,有火的節點都向它相鄰的所有節點傳播火苗。

實現

void bfs(int u) {
  while (!Q.empty()) Q.pop();
  Q.push(u);
  vis[u] = 1;
  d[u] = 0;
  p[u] = -1;
  while (!Q.empty()) {
    u = Q.front();
    Q.pop();
    for (int i = head[u]; i; i = e[i].nxt) {
      if (!vis[e[i].to]) {
        Q.push(e[i].to);
        vis[e[i].to] = 1;
        d[e[i].to] = d[u] + 1;
        p[e[i].to] = u;
      }
    }
  }
}

void restore(int x) {
  vector<int> res;
  for (int v = x; v != -1; v = p[v]) {
    res.push_back(v);
  }
  std::reverse(res.begin(), res.end());
  for (int i = 0; i < res.size(); ++i) printf("%d", res[i]);
  puts("");
}

具體來說,我們用一個佇列 \(Q\) 來記錄要處理的節點,然後開一個布林陣列 vis[] 來標記是否已經訪問過某個節點。

開始的時候,我們將所有節點的 \(vis\) 值設為 \(0\),表示沒有訪問過;然後把起點 \(s\) 放入佇列 \(Q\) 中並將 vis[s] 設為 \(1\)

之後,我們每次從佇列 \(Q\) 中取出隊首的節點 \(u\),然後把與 \(u\) 相鄰的所有節點 \(v\) 標記為已訪問過並放入佇列 \(Q\)

迴圈直至當佇列 \(Q\) 為空,表示 BFS 結束。

在 BFS 的過程中,也可以記錄一些額外的資訊。例如上述程式碼中,\(d\) 陣列用於記錄起點到某個節點的最短距離(要經過的最少邊數),\(p\) 陣列記錄是從哪個節點走到當前節點的。

有了 \(d\) 陣列,可以方便地得到起點到一個節點的距離。

有了 \(p\) 陣列,可以方便地還原出起點到一個點的最短路徑。上述程式碼中的 restore 函式使用該陣列依次輸出從起點到節點 \(x\) 的最短路徑所經過的節點。

時間複雜度 \(O(n + m)\)

空間複雜度 \(O(n)\)($vis4 陣列和佇列)

應用

在一個無權圖上求從起點到其他所有點的最短路徑。

\(O(n+m)\) 時間內求出所有連通塊。(我們只需要從每個沒有被訪問過的節點開始做 BFS,顯然每次 BFS 會走完一個連通塊)

如果把一個遊戲的動作看做是狀態圖上的一條邊(一個轉移),那麼 BFS 可以用來找到在遊戲中從一個狀態到達另一個狀態所需要的最小步驟。

在一個有向無權圖中找最小環。(從每個點開始 BFS,在我們即將抵達一個之前訪問過的點開始的時候,就知道遇到了一個環。圖的最小環是每次 BFS 得到的最小環的平均值。)

找到可能在 \((a, b)\) 最短路上的邊。(分別從 \(a\)\(b\) 進行 BFS,得到兩個 \(d\) 陣列。之後對每一條邊 \((u, v)\),如果 d_a[u]+1+d_b[v]=d_a[b],則說明該邊在最短路上)

找到可能在 \((a, b)\) 最短路上的點。(分別從 \(a\)\(b\) 進行 BFS,得到兩個 \(d\) 陣列。之後對每一個點 \(v\),如果 d_a[v]+d_b[v]=d_a[b],則說明該點在某條最短路上)

找到一條長度為偶數的最短路。(我們需要一個構造一個新圖,把每個點拆成兩個新點,原圖的邊 \((u, v)\) 變成 \(((u, 0), (v, 1))\)\(((u, 1), (v, 0))\)。對新圖做 BFS,\((s, 0)\)\((t, 0)\) 之間的最短路即為所求)

在一個邊權為 \(0/1\) 的圖上求最短路,見下方雙端佇列 BFS。


雙向搜尋

折半搜尋、meet in the middle

過程

Meet in the middle 演算法的主要思想是將整個搜尋過程分成兩半,分別搜尋,最後將兩半的結果合併。

性質

暴力搜尋的複雜度往往是指數級的,而改用 meet in the middle 演算法後複雜度的指數可以減半
即讓複雜度從 \(O(a^b)\) 降到 \(O(a^{b/2})\)

還是 N 皇后

思路:搜尋,記錄當前行數,哪些列被放了,哪些斜對角線被放了。

#include <iostream>
#include <queue>
#include <cmath>
#include <algorithm>
#include <vector>
#include <cstring>
#include <cstdio>
#include <set>
#include <map>
#include <unordered_map>
#include <bitset>
using namespace std;
const int maxn = 1e4 + 10, MAXN = 1e9 + 10;
int row_valid[maxn], n, ans =0;
char a[maxn][maxn];
void dfs(int row, int col_ban, int diag_ban_l, int diag_ban_r){
	if(row == n){
		ans++;
		return;
	}
	int tmp_valid = row_valid[row] & ~ (col_ban | diag_ban_l | diag_ban_r);
	for(int i = 0;i < n; i++){
		if(tmp_valid >> i & 1){
			dfs(row + 1, col_ban | (1 << i), 
			(diag_ban_l | (1 << i)) << 1,
			(diag_ban_r | (1 << i)) >> 1);
		}
	}
}
int main(){
	cin >> n;
	for(int i = 0;i < n; i++){
		cin >> a[i];
		for(int j = 0;j < n; j++){
			if(a[i][j] == '*'){
				row_valid[i] |= 1 << j;
			}
		}
	}
	dfs(0, 0, 0, 0);
	cout<< ans << endl;
	return 0;
}