LCA的離線快速求法

Ofnoname發表於2022-04-22

最常見的LCA(樹上公共祖先)都是線上演算法,往往帶了一個log。有一種辦法是轉化為“+-1最值問題”得到O(n)+O(1)的複雜度,但是原理複雜,常數大。今天介紹一種允許離線時接近線性求LCA的方法。

一個點和其他點的LCA必定是它到root路徑上的所有節點之一,而另一個節點剛好在哪個節點下,LCA就是誰:

image

如圖,標粗的箭頭為當前搜尋的路徑,左邊為已經搜尋完畢的路徑,右邊的黑色節點尚未搜尋。現在要求節點cur和節點a的LCA,顯然a是什麼顏色,LCA就也是這個顏色,如果a還沒有被搜尋到,那就不處理,把這個詢問留給搜尋到a的時候處理(那個時候cur肯定已經訪問過了)。

那怎麼做這個染色呢?我們對所有節點做一個並查集,每當一個節點搜尋完畢,處理完了自己的答案,就把自己合併到父親fa裡面,那麼在我搜完之後,父節點fa搜完之前,fa的其他所有兒子的公共祖先都是fa了:
image

當cur節點搜尋完畢後,回到fa,講cur修改為橙色併入到fa裡(而且我們使用了並查集,此後查詢cur的子節點也將得到fa),之後在fa搜尋其他兒子節點時,他們和cur子樹裡的節點的LCA一定是fa,而當fa全部搜尋完成後,他又被併入上級節點,以此類推,就可以在一遍dfs中就獲取所有詢問的答案。

參考程式碼:

int N, Q, p[MAX], qa[MAX], qb[MAX], ans[MAX];
vector<int> has[MAX];

struct ufs {
	int in[MAX];

	ufs() {
		std::iota(in, in + N, 0);
	}
	void merge(int v, int u) { //! v合併給u
		in[v] = u;
	}
	int find(int u) {
		return in[u]==u ? u : (in[u] = find(in[u])); //! 帶路徑壓縮
	}
};

class Tree
{
	std::vector<int> son[MAX];
	ufs f;

	void getans(int u) {
		for (auto v: son[u]) {
			getans(v); f.merge(v, u); //! 處理子樹後,將其併入
		}
		for (auto i: has[u]) {
			auto v (qa[i]^qb[i]^u); //! 該詢問的另一個點
			if (f.find(v) != v) ans[i] = f.find(v);
		}
	}

public:
	#define root 0
	Tree() {
		for (int i = 1; i < N; ++i) son[p[i]].push_back(i);
		getans(root);
	}
	#undef root
};

main() {
	scanf("%d%d", &N, &Q);
	for (int i = 1; i < N; ++i) scanf("%d", p + i);
	for (int i = 0; i < Q; ++i) {
		scanf("%d%d", qa + i, qb + i); 
		has[qa[i]].push_back(i);//! 把詢問歸到qa和qb下
		has[qb[i]].push_back(i);
	}

	auto tr = new Tree;
	for (int i = 0; i < Q; ++i)
		printf("%d\n", ans[i]);
}

一個提交地址:https://judge.yosupo.jp/problem/lca

相關文章