004.最近公共祖先

最近公共祖先(LCA)

倍增算法

建立两个数组:

\(dep[u]\)存储\(u\)点的深度;

\(fa[u][i]\)存储\(u\)点向上\(2^i\)层的祖先节点\((i = 0,1,2···)\).

1.DFS树,创建ST表

倍增递推:\(fa[u][i] = fa[fa[u][i-1][i-1]]\)

2.利用ST表求LCA

\((1)\)\(x,y\)不在同一层,则让\(x,y\)先跳到同一层

\((2)\)到达同一层后,将\(x,y\)一起跳到LCA的下一层

时间复杂度:\(O((n+m)\log\ n)\)

[P3379 【模板】最近公共祖先(LCA)](P3379 【模板】最近公共祖先(LCA) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn))
#include<iostream>
#include<vector>
using namespace std;
const int N = 5e5 + 5;
vector<int> e[N];
int n, m, a, b, s;
int dep[N], fa[N][20];

inline void add(int u, int v) {
	e[u].push_back(v);
	e[v].push_back(u);
}

void dfs(int u, int father) {
	dep[u] = dep[father] + 1;
	fa[u][0] = father;
	for (int i = 1; i <= 19; i++)
		fa[u][i] = fa[fa[u][i - 1]][i - 1];
	for (int v : e[u]) {
		if (v != father) dfs(v, u);
	}
}

int lca(int u, int v) {
    if (dep[u] < dep[v]) swap(u, v);
    //先跳到同一层
    for (int i = 19; i >= 0; i--)
        if (dep[fa[u][i]] >= dep[v])
            u = fa[u][i];
    if (u == v) return v;
    //一起跳到LCA的下一层
    for (int i = 19; i >= 0; i--)
        if (fa[u][i] != fa[v][i])
            u = fa[u][i], v = fa[v][i];
    return fa[u][0];
}



int main() {
    cin >> n >> m >> s;
    while (n > 1) {
        n--;
        int x, y;
        cin >> x >> y;
        add(x, y);
    }
    dfs(s, 0);
    while (m--) {
        cin >> a >> b;
        cout << lca(a,b) << endl;
    }

	return 0;
}



Tarjan算法

Tarjan是一种离线算法,利用并查集维护祖先节点

\(e[u]\)存树边;

\(query[u]\)存查询;

\(fa[u]\)存父节点;

\(vis[u]\)打标记;

\(ans[i]\)存查询结果.

查询过程:

\((1)\)从根开始DFS,进入节点\(u\)后对其打上标记\((vis[u]=true)\)

\((2)\)枚举节点\(u\)的儿子节点\(v\),遍历\(v\)的子树,返回\(u\)时,把\(v\)指向\(u(fa[v]=u)\)

\((3)\)遍历完\(u\)的儿子们,离开\(u\)返回上一层前,枚举以\(u\)为起点的查询,若终点\(v\)被搜索过\(vis[v]=true\),则查找\(v\)的根\(find[v]\),即\(u,v\)的LCA,答案计入\(ans[]\);

\((4)\)递归遍历完整棵树,得到全部查询答案.

时间复杂度:\(O(n+m)\)

[P3379 【模板】最近公共祖先(LCA)](P3379 【模板】最近公共祖先(LCA) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn))

#include<iostream>
#include<vector>
using namespace std;
const int N = 5e5 + 5;
vector<int> e[N];
int n, m, a, b, s;
int fa[N],vis[N],ans[N];
vector< pair<int,int> > query[N];//query[u]表示以u为起点的查询,.first表示终点,.second表示编号

inline void add(int u, int v) {
	e[u].push_back(v);
	e[v].push_back(u);
}

int find(int u){//带状态压缩的并查集查找
    if(u == fa[u]) return u;
    return fa[u] = find(fa[u]);
}

void tarjan(int u){
    vis[u] = true;//关键点1 进入u节点:打上标记
    for(auto v : e[u]){
        if(!vis[v]){
            tarjan(v);//递归遍历,进入儿子节点
            fa[v] = u;//关键点2 从每个儿子节点回到u时:v的父节点标记为u
        }
    }
    //关键点3 离开u进入其父节点时 枚举以u为起点的查询,找到LCA
    for(auto q : query[u]){
        int v = q.first,i = q.second;
        if(vis[v]) ans[i] = find(v);//时间戳中包含v才能进行查找,这也是为什么双向建立查询
    }
}

int main(){
    for(int i = 1 ; i <= N ; i ++) fa[i] = i;
    cin >> n >> m >> s;
    for(int i = 1 ; i < n ; i ++){
        cin >> a >> b;
        add(a,b);
    }
    
    for(int i = 1 ; i <= m ; i ++){
        cin >> a >> b;
        query[a].push_back({b,i});
        query[b].push_back({a,i});
    }   
    
    tarjan(s);
    for(int i = 1 ; i <= m ; i ++){
        cout << ans[i] << endl;
    }
    return 0;
}
posted @ 2025-06-16 20:11  _P_D_X  阅读(21)  评论(0)    收藏  举报