1. 程式人生 > 實用技巧 >Tarjan求LCA

Tarjan求LCA

前言:

沒想到吧,\(tarjan\)不僅可以用來求割點和橋,縮點,還能求\(LCA\)。不過,\(tarjan\)\(LCA\)是離線的,要線上演算法的話還是學倍增吧。


正題:

這次的\(tarjan\)不需要回溯值和\(dfs\)序,本質的來說,其實\(tarjan\)\(LCA\)跟割點和橋,縮點沒有任何關係一個人發明的算不算

前置知識:並查集

當然,\(tarjan\)的其他兩個演算法跟\(dfs\)有關,求\(LCA\)也不例外,我們是在\(dfs\)的基礎上,一步一步求出來的。

步驟如下:

  1. 對這課樹進行\(dfs\),從根開始

  2. 對於每個節點,我們不先標記這個點走過,回溯的時候才標記

  3. 對於每個節點,遍歷與之相鄰且未走過的點,並把這些點的父親標記為當前節點,相當於合併這些點

  4. 對於每個節點,當與之相鄰的點遍歷完後,查詢在求LCA問題中與自己相關的問題,看它問題中的另外一個點有沒有被查詢到,有的話就把這兩個點的答案賦值為另外一個點的父親(當然是合併後的父親)

第三步的是否走過時指遍歷到了沒有,不是標記。對於合併,合併後查詢父親,我們就用並查集來完成。

這樣自然是不好理解的,來看個例子(以下的\(find\)函式就是普通並查集的\(find\)):

這是我們的圖,現在假設我們要求\(3\)\(4\)\(2\)\(6\)\(LCA\)

先進行第一步,此時我們先是從\(1\)

開始搜,先到的地方是\(3\),然後看與\(3\)相關的節點\(4\),\(4\)沒有被搜到,我們就退出,並標記\(3\),把他的父親標記為\(2\),合併掉\(3\)

此時,\(2\)的兒子沒遍歷完開始遍歷\(4\),與\(4\)相關的節點\(3\),是搜過的,此時\(LCA\)\(3\),\(4\)就是\(find\)\(3\))也就是\(2\)(注意不能\(find\)\(4\)),而是\(find\)與之相關的另外一個節點)。然後合併\(4\),把\(4\)的父親標記為\(2\)

這時應該遍歷\(2\)了,發現與之相關的\(6\)未找到,於是把\(2\)的父親標記為\(1\)

,自然,此時\(3\),\(4\)的父親也為\(1\)了。

後面的就以此類推了。這一步應該判斷\(6\),求出\(LCA2\),\(6\)\(1\),合併\(6\),標記父親。

\(5\)合併,父親為\(1\)

\(1\)了後就沒有了,演算法完結。

接下來講講實現。


例題:

洛谷 P3379 【模板】最近公共祖先(LCA)

這就是模板了吧,我把程式碼貼一貼,理解一下(特別短!!!而且這道題對於倍增和\(RMQ\)都需要卡卡常,而\(tarjan\)我用\(vector\)建圖不加快讀快寫就能過,當然得把註釋刪掉,不然會\(T\))。

程式碼:

#include <bits/stdc++.h>
using namespace std;
struct node{
	int p , id;
};	//代表與之相關的點和這一對LCA是第幾個答案 
int n , m , root;
int fa[500010] , vis[500010]/*標記+判斷是否走過*/ , ans[500010];
vector<int> e[500010];	//建邊 
vector<node> q[500010];	//儲存問題 
int find(int x){	//路徑壓縮 
	if(fa[x] == x) return x;
	return fa[x] = find(fa[x]);
}
void tarjan(int x){
	vis[x] = 2;	//2表示走過 
	for(int i = 0; i < e[x].size(); i++){
		int nx = e[x][i];
		if(vis[nx]) continue;
		tarjan(nx);
		fa[nx] = x;	//標記父親 
	}
	for(int i = 0; i < q[x].size(); i++){
		int px = q[x][i].p , ix = q[x][i].id;	//找與之相關的點 
		if(vis[px] == 1) ans[ix] = find(px);
	}
	vis[x] = 1;	//1表示標記過 
}
int main(){
	cin >> n >> m >> root;
	for(int i = 1; i <= n; i++) fa[i] = i;	//並查集初始化 
	for(int i = 1; i <= n - 1; i++){
		int x , y;
		cin >> x >> y;	//建圖,注意雙向邊(被坑過) 
		e[x].push_back(y);
		e[y].push_back(x);
	}
	for(int i = 1; i <= m; i++){
		int x , y;
		cin >> x >> y;	//因為我們在tarjan過程中不知道求的是第幾個答案,所以要儲存一下這一對LCA是第幾個答案 
		q[x].push_back((node){y/*與之相關的點*/ , i/*第幾個答案*/});
		q[y].push_back((node){x , i});
	}
	tarjan(root);	//跑LCA 
	for(int i = 1; i <= m; i++) cout << ans[i] << endl;	//輸出 
	return 0;
}

再來一道例題:

洛谷 P1967 貨車運輸

這道題還需要用到最小生成樹。

先講下思路吧。

對於一些道路,我們在保證圖的連通性時,是可以刪掉的,就如樣例的邊\(1\)\(3\),我們是肯定不會走這條路的,題目沒有要求路更短,那麼我們就可以刪掉一些小邊,只要不破壞圖的連通性就行(原來就不連通那可沒辦法了),這時,我們可以想到最大生成樹,把小邊刪掉,保留大邊,這樣就可以既保證圖的連通性,又減少冗餘的邊。接下來,對於一棵樹,任意兩點是不是就只有一條路徑了,我們就可以求出要求的兩點的\(LCA\),然後從兩個點往上查詢,直到找到他們的\(LCA\),取最小值,最後輸出即可。

當然,可以優化的,具體應該是在\(tarjan\)過程中就求出他們的路徑和最小值,但是我太菜了死活沒想出來,只能想到這裡了。

程式碼:

#include <bits/stdc++.h>
using namespace std;
struct node{
	int l , r , w;
};	//存邊
node ed[300010];
int n , m , q , now;
int fa[300010] , vis[300010] , lca[300010] , rf[300010]/*存往上走的路徑*/ , wrf[300010]/*存LCA往上走時的邊權*/ , qu1[300010] , qu2[300010]/*存問題的節點*/;
vector<pair<int , int> > e[300010];	//跑完最大生成樹後再建邊 
vector<pair<int , int> > ques[300010];	//存問題 
int find(int x){
	if(fa[x] == x) return x;
	return fa[x] = find(fa[x]);
}
bool cmp(node x , node y){	//最大生成樹 
	return x.w > y.w;
}
void trajan(int x){
	vis[x] = 2;	//走過 
	for(int i = 0; i < e[x].size(); i++){
		int nx = e[x][i].first;
		if(vis[nx]) continue;
		trajan(nx);
		rf[nx] = x;	//存往上走的路徑 
		wrf[nx] = e[x][i].second;	//存路徑值 
		fa[nx] = x;
	}
	for(int i = 0; i < ques[x].size(); i++)	//存LCA 
		if(vis[ques[x][i].first] == 1) lca[ques[x][i].second] = find(ques[x][i].first);
	vis[x] = 1;	//標記走過 
}
int dfs(int x , int need){	//找路徑最小值 
	if(x == need) return 0x3fffff;	//為本身 
	if(rf[x] == need) return wrf[x];	//父親是LCA就停止 
	return min(wrf[x] , dfs(rf[x] , need));	//取min 
}
int main(){
	cin >> n >> m;
	for(int i = 1; i <= m; i++) cin >> ed[i].l >> ed[i].r >> ed[i].w;
	for(int i = 1; i <= n; i++) fa[i] = i;	//最大生成樹並查集初始化 
	sort(ed + 1 , ed + m + 1 , cmp);
	for(int i = 1; i <= m; i++){
		int fx = find(ed[i].l) , fy = find(ed[i].r);
		if(fx == fy) continue;
		fa[fx] = fy;
		e[ed[i].l].push_back(make_pair(ed[i].r , ed[i].w));	//建圖 
		e[ed[i].r].push_back(make_pair(ed[i].l , ed[i].w));
		now++;
		if(now == n - 1) break;
	}
	cin >> q;
	for(int i = 1; i <= n; i++) fa[i] = i;
	for(int i = 1; i <= q; i++){
		int x , y;
		cin >> x >> y;
		qu1[i] = x , qu2[i] = y;
		ques[x].push_back(make_pair(y , i));	//存問題 
		ques[y].push_back(make_pair(x , i));
	}
	for(int i = 1; i <= n; i++)
		if(!vis[i]) trajan(i);
	for(int i = 1; i <= q; i++){
		if(find(qu1[i]) != find(qu2[i])){	//不是同一個聯通快 
			cout << -1 << endl;
			continue;
		}
		int min1 = dfs(qu1[i] , lca[i]) , min2 = dfs(qu2[i] , lca[i]);
		if(qu1[i] == qu2[i]) cout << 0 << endl;	//如果兩個相等 
		else cout << min(min1 , min2) << endl;	//取min 
	}
	return 0;
}

廢話結束,正片開始,學\(Treap\)去了,把以前坑填了