1. 程式人生 > >每天學一丟之 LCA-Tarjan

每天學一丟之 LCA-Tarjan

每天學一丟意為每天學一點丟一點。

LCA

LCA 即樹上最近公共祖先,Tarjan 可以離線地去求 LCA,並且可以維護樹上兩個點的距離。
Tarjan 的複雜度是 O(n+q)

Tarjan

Tarjan 不僅可以解決 LCA 問題,還可以解決強連通等其他問題,今天只學會了 LCA 問題。
之前在camp中曾經學到過,樹上的 dfs 序特別的有用,例如,dfs 正序表示第一次訪問到這個節點,dfs逆序表示訪問到這個節點時,它所有的子樹已經訪問過了。所以在做樹上dp的時候,這兩個細節對維護樹上的值就顯得特別有用。而

dfs 的正序和逆序,可以直接在 dfs 的時候就維護出來。就是在剛 dfs 到該點時維護,還是遞迴遍歷完所有子節點吼維護的區別。

Tarjan 的基本思想就是並查集 + dfs,而並查集的作用最主要的是維護當前子樹的根是哪裡。先做一個小小的證明,任意兩點的 LCA 說明,這兩點位於該點的兩顆不同子樹上,因為如果兩點同屬於一顆子樹的話,那顆子樹的根就是這兩點的 LCA,特例是其中一個節點是另一個節點的祖先,但是在這個演算法中並不影響。那麼我們繼續,如果我們訪問到了一個查詢 (u,v)u 節點,若此時 v 節點還沒有訪問過,則證明已訪問的子樹當中沒有

v,我們就繼續查到,如果 v 已經訪問過,那麼此兩點的最近公共祖先就是當前查到的子樹的根。也就是我們 dfs 逆序維護並查集,就可以得到當前子樹的根是哪個,然後每次遍歷完所有子樹後,查詢一下包含當前節點的所有詢問就可以了。也是 dfs 逆序。

程式碼

vector<pair<int, int> >G[maxn];
map<pair<int, int>, pair<int, int> >ans;
vector<int> ask[maxn];
int vis[maxn], dis[maxn], fa[maxn], x[maxn], y[maxn];

void
init(){ ans.clear(); rep(i, 0, maxn) { G[i].clear(); ask[i].clear(); } mm(vis, 0); mm(dis, 0); } void addedge(int u, int v, int w){ G[u].push_back(mk(v, w)); G[v].push_back(mk(u, w)); } void addask(int u, int v){ ask[u].push_back(v); ask[v].push_back(u); } int found(int x){ return x==fa[x]?x:(fa[x]=found(fa[x])); } void Tarjan(int u) { //dis[root] = 0; fa[u] = u, vis[u] = 1; rep(i, 0, G[u].size()) { if(!vis[G[u][i].fi]){ dis[G[u][i].fi] = dis[u] + G[u][i].se; Tarjan(G[u][i].fi); fa[G[u][i].fi] = u; } } rep(i, 0, ask[u].size()){ if(vis[ask[u][i]]){ int lca = found(ask[u][i]); int len = dis[u]+dis[ask[u][i]]-2*dis[lca]; ans[mk(u, ask[u][i])] = ans[mk(ask[u][i], u)] = mk(lca, len); } } }

小結

Tarjan 其實就是一個玩出花來的 dfs,而我的程式碼當中因為使用了map,所以複雜度上是多了一個log的,一些不太嚴格的資料還是可以過的= =