DFN 序求 LCA

alloverzyt發表於2024-10-26

冷門科技 —— DFS 序求 LCA

轉載自qAlex_Weiq冷門科技 —— DFS 序求 LCA - qAlex_Weiq - 部落格園

部落格園裡直接搜 dfn 序求 lca 老是找不到這篇文章,故直接轉載過來,這篇初始的才是寫的最好的。

正文

DFS 序求 LCA 無論是從時間常數,空間常數還是好寫程度方面均吊打尤拉序。

定義

DFS 序表示對一棵樹進行深度優先搜尋得到的 結點序列,而 時間戳 DFN 表示每個結點在 DFS 序中的位置。這兩個概念需要著重區分。

演算法介紹

考慮樹上的兩個結點 u,v 及其最近公共祖先 d,我們不得不使用尤拉序求 LCA 的原因是在尤拉序中,d 在 u,v 之間出現過,但在 DFS 序中,d 並沒有在 u,v 之間出現過。對於 DFS 序而言,祖先一定出現在後代之前(性質)。

不妨設 u 的 DFN 小於 v 的 DFN(假設)。

u 不是 v 的祖先 時(情況 1),DFS 的順序為從 d 下降到 u,再回到 d,再往下降到 v。

根據性質,任何 d 以及 d 的祖先均不會出現在 u∼v 的 DFS 序中。

考察 d 在 v 方向上的第一個結點 v′,即設 v′ 為 d 的 / 子樹包含 v 的 / 兒子。根據 DFS 的順序,顯然 v′ 在 u∼v 的 DFS 序之間。

這意味著什麼?我們只需要求在 u 的 DFS 序和 v 的 DFS 序之間深度最小的任意一個結點,那麼 它的父親 即為 u,v 的 LCA。

這樣做的正確性依賴於在 DFS 序 u 到 v 之間,d 以及 d 的祖先必然不會存在,且必然存在 d 的兒子。

u,v 成祖先後代關係(情況 2)是容易判斷的,但這不優美,不能體現出 DFS 求 LCA 的優勢:簡潔。為了判斷還要記錄每個結點的子樹大小,但我們自然希望求 LCA 的方法越簡單越快越好。

根據假設,此時 u 一定是 v 的祖先。因此考慮令查詢區間從 [dfnu,dfnv] 變成 [dfnu+1,dfnv]。

對於情況 1,u 顯然一定不等於 v′,所以情況 2 對於演算法進行的修改仍然適用於情況 1。

綜上,若 u≠v,則 u,v 之間的 LCA 等於在 DFS 序中,位置在 dfnu+1 到 dfnv 之間的深度最小的結點的父親。若 u=v,則它們的 LCA 就等於 u,這是唯一需要特判的情況。

預處理 ST 表的複雜度仍為 O(nlog⁡n),但常數減半。以下是模板題 P3379 的程式碼。

#include <bits/stdc++.h>
using namespace std;
constexpr int N = 5e5 + 5;
int n, m, R, dn, dfn[N], mi[19][N];
vector<int> e[N];
int get(int x, int y) {return dfn[x] < dfn[y] ? x : y;}
void dfs(int id, int f) {
  mi[0][dfn[id] = ++dn] = f;
  for(int it : e[id]) if(it != f) dfs(it, id); 
}
int lca(int u, int v) {
  if(u == v) return u;
  if((u = dfn[u]) > (v = dfn[v])) swap(u, v);
  int d = __lg(v - u++);
  return get(mi[d][u], mi[d][v - (1 << d) + 1]);
}
int main() {
  scanf("%d %d %d", &n, &m, &R);
  for(int i = 2, u, v; i <= n; i++) {
    scanf("%d %d", &u, &v);
    e[u].push_back(v), e[v].push_back(u);
  }
  dfs(R, 0);
  for(int i = 1; i <= __lg(n); i++)
  for(int j = 1; j + (1 << i) - 1 <= n; j++)
    mi[i][j] = get(mi[i - 1][j], mi[i - 1][j + (1 << i - 1)]);
  for(int i = 1, u, v; i <= m; i++) scanf("%d %d", &u, &v), printf("%d\n", lca(u, v));
  return 0;
}

和各種 LCA 演算法的對比

對比 DFS 序和尤拉序,不僅預處理的時間常數砍半(尤拉序 LCA 的瓶頸恰好在於預處理,DFS 是線性),空間常數也砍半(核心優勢),而且還更好寫(對於一些題目就不需要再同時求尤拉序和 DFS 序了),也不需要擔心忘記開兩倍空間,可以說前者從各個方面吊打後者。

對比 DFS 序和倍增,前者單次查詢複雜度更優。

對於 DFS 序和四毛子,前者更好寫,且單次查詢常數更小(其實差不多)。

對於 DFS 序和樹剖,前者更好寫,且單次查詢複雜度更優(但樹剖常數較小)。

我的易懂程式碼(沒有壓行):

#include<bits/stdc++.h>
using namespace std;
#define N 500005
int n,m,st,dft,dfn[N],mi[N][19];
vector<int>e[N];
int get(int x,int y){
	return dfn[x] < dfn[y] ? x : y;
}

void dfs(int u,int fa){
	dfn[u]=++dft;
	mi[dft][0]=fa;
	for(auto v:e[u]) if(v!=fa) dfs(v,u);
}

int lca(int u,int v){
	if(u==v) return u;
	u=dfn[u];v=dfn[v];
	if(u>v) swap(u,v);
	int d=__lg(v-u);
	return get(mi[u+1][d],mi[v-(1<<d)+1][d]);
}

int main(){
	scanf("%d%d%d",&n,&m,&st);
	for(int i=1;i<n;i++){
		int u,v;
		scanf("%d%d",&u,&v);
		e[u].push_back(v);e[v].push_back(u);
	}
	dfs(st,0);
	for(int i=1;i<=__lg(n);i++){
		for(int j=1;j+(1<<i)-1<=n;j++){
			mi[j][i]=get(mi[j][i-1],mi[j+(1<<(i-1))][i-1]);
		}
	}
	for(int i=1;i<=m;i++){
		int u,v;
		scanf("%d%d",&u,&v);
		printf("%d\n",lca(u,v));
	}
}

將 DFS 序求 LCA 發揚光大,讓尤拉序求 LCA 成為時代的眼淚!

相關文章