(下面內容能夠略過。)
離線算法其實就是將多個詢問一次性解決。離線算法每每是與在線算法相對的。例如求LCA的算法中,樹上倍增屬於在線算法,在對樹進行$O(n)$預處理後,每一個詢問用$O(log_2n)$複雜度回答。而離線的Tarjan算法則是用$O(n+q)$時間將詢問一次性所有回答。node
下面是一棵樹,咱們將以這棵樹爲例子講解Tarjan算法,其中0號點爲根。
算法
假設對於這棵樹的詢問有4個,分別詢問:
$LCA(2,8)$
$LCA(5,6)$
$LCA(2,5)$
$LCA(4,9)$
首先咱們將這四個詢問順序調轉,再複製四份,如今就有8個詢問:
$LCA(2,8)$
$LCA(5,6)$
$LCA(2,5)$
$LCA(4,9)$
$LCA(8,2)$
$LCA(6,5)$
$LCA(5,2)$
$LCA(9,4)$
這一步是必須的,後面將會說明它。
而後對於每一個節點u,給它開一個鏈表,找到全部的詢問 $LCA(u,v)$ ,把v插入到u的鏈表後,同時把詢問編號插入,以便按照輸入順序輸出答案。
因而詢問就被離線了。
那麼到底怎麼求LCA呢?咱們對帶着詢問樹進行一次dfs。如圖:
第1步,0號點被遍歷:
spa
沒有與0相關的詢問,繼續dfs。
第2步,1號點被遍歷:
3d
沒有與1相關的詢問,繼續dfs。
第3步,2號點被遍歷:
code
2號點沒有兒子了,與2相關的詢問有 $LCA(2,5)$ 和 $LCA(2,8)$ 。
可是5號點和8號點都尚未遍歷過,咱們什麼也不知道,所以這兩個詢問不理它。
第4步,2號點回溯(遍歷完畢並回溯的點標爲藍色):
blog
第5步,3號點被遍歷:
圖片
沒3號點的事,繼續dfs。
第6步,4號點被遍歷:
get
關於4號點的詢問咱們也是一無所知,回溯。
第7步,4號點回溯:
it
第8步,5號點被遍歷:
io
關於5的詢問有 $LCA(5,6)$ 和 $LCA(5,2)$ 。
6號點的信息咱們還不知道,可是2號點,咱們已經知道它已經被訪問且回溯了。
5的祖先必定在當前正在訪問的節點中(也就是訪問了還沒回溯的點),那麼
$LCA(5,2)$ 其實也就是在圖上紅色的節點裏找出知足以下兩個條件的點:
1.它是2的祖先。
2.它深度最大。
很容易發現這個點就是1,因而這裏就能夠記錄下來 $LCA(5,2)=1$ 。
第9步,5號點回溯:
第10步,3號點回溯:
第11步,6號點被遍歷:
仍是跟以前同樣,對於跟6號點有關的詢問 $LCA(6,5)$ ,去找紅色點裏深度最大的5的祖先,顯然就是1,記下 $LCA(6,5)=1$。
第12步,6號點回溯。
第13步,1號點回溯:
第14步,7號點被遍歷:
第15步,8號點被遍歷:
按照以前作法,在紅色節點裏找出深度最大的2的祖先,能夠求出 $LCA(8,2)=0$ 。
第16步,8號點回溯:
第17步,9號點被遍歷:
顯然了,$LCA(9,4)=0$ 。
後面的過程就略過,由於至此咱們已經求出了四個詢問的答案。
$LCA(2,8)=0$
$LCA(5,6)=1$
$LCA(2,5)=1$
$LCA(4,9)=0$
也許你已經明白了,爲何要把$LCA(u,v)$複製一份$LCA(v,u)$,由於在上面過程當中,咱們不能保證遍歷u時v已經回溯,所以須要複製一個詢問。
上面的過程已經能夠離線求出LCA了,但複雜度不是最優的,問題就出在上面找「紅色節點中u的深度最大的祖先」,若是從u點一步步向上跳,複雜度爲$O(nq)$。
假如對於一個詢問$LCA(u,v)$,u已經被遍歷過,此時遍歷到v。容易發現$LCA(u,v)$必定是紅色的(也就是訪問了還未回溯)。那麼若是咱們在dfs的過程當中,在節點u的兒子遍歷完畢回溯時,將兒子的fa指向點u,那麼對於詢問$LCA(u,v)$,只須要從u開始,不斷往u的父親跳,跳到的深度最小一個節點,就是$LCA(u,v)$。
怎麼去證實呢?首先其必是u的祖先,這個不用說。但爲何是深度最小的那一個呢?不是要求深度最大的嗎?由於咱們是在回溯時將u的fa指向它的父親的,若是深度不是最小,則u的這個祖先的子樹裏確定沒有v。若是有v的話,其必然是深度最小的那一個。因爲u已訪問完畢,而v還在訪問中,所以u的父親裏不會有比$LCA(u,v)$ 深度更大的點,此時就能保證u的fa裏深度最小的那個就是$LCA(u,v)$
「將兒子的父親指向點u」這個操做用並查集完成,能夠保證在常數複雜度。所以對樹進行遍歷須要$O(n)$複雜度,而總共有q個詢問,每一個詢問能夠$O(1)$回答,複雜度爲$O(n+q)$。
看不懂不要緊,結合代碼來理解:
參考題目洛谷P3379【模板】最近公共祖先
#include <vector> #include <cstdio> using namespace std; inline int read() { int x = 0, f = 0; char c = getchar(); for (; c < '0' || c > '9'; c = getchar()) if (c == '-') f = 1; for (; c >= '0' && c <= '9'; c = getchar()) x = (x << 1) + (x << 3) + (c ^ '0'); return f ? -x : x; } const int N = 5e5 + 7; int n, m, u, v, s; int tot = 0, st[N], to[N << 1], nx[N << 1], fa[N], ans[N], vis[N]; struct note { int node, id; }; //詢問以結構體形式保存 vector<note> ques[N]; inline void add(int u, int v) { to[++tot] = v, nx[tot] = st[u], st[u] = tot; } inline int getfa(int x) { return fa[x] == x ? x : fa[x] = getfa(fa[x]); } //並查集的getfa操做,路徑壓縮 void dfs(int u, int from) { for (int i = st[u]; i; i = nx[i]) if (to[i] != from) dfs(to[i], u), fa[to[i]] = u; //將u的兒子合併到u int len = ques[u].size(); //處理與u有關的詢問 for (int i = 0; i < len; i++) if (vis[ques[u][i].node]) ans[ques[u][i].id] = getfa(ques[u][i].node); //對應的v已經訪問並回溯時,LCA(u,v)就是v的fa裏深度最小的一個也就是getfa(v) vis[u] = 1; //訪問完畢回溯 } int main() { n = read(), m = read(), s = read(); for (int i = 1; i < n; i++) u = read(), v = read(), add(u, v), add(v, u); for (int i = 1; i <= m; i++) u = read(), v = read(), ques[u].push_back((note){v, i}), ques[v].push_back((note){u, i}); //記下詢問編號便於輸出 for (int i = 1; i <= n; i++) fa[i] = i; dfs(s, 0); for (int i = 1; i <= m; i++) printf("%d\n", ans[i]); //輸出答案 return 0; }