LCA (最近公共祖先) Tarjan & 倍增

总结

1)求树上两点距离

            dis (a , b)  =  depth[a] + depth[b] - 2 * depth[ LCA (a, b) ] 

2)求线段重合距离

             A到C和B到C的重合长度 = (dis_AC + dis_BC - dis_AB)/  2

LCA Tarjan:

实现原理

理解:离线算法,建好树后再查询,一次DFS 吧所有查询解决完。

时间复杂度:O(n+q);

n个点 q次询问

补一下:链式向前星并查集 ,Tarjan

代码

#include<iostream>
#include<math.h>
#include<stdio.h>
#include<algorithm>
#include<cstring>
using namespace std;
const int MAXN = 5e5+ 10;
int fa[MAXN], head[MAXN], head_ask[MAXN], cnt, cnt_ask, ans[MAXN];
bool vis[MAXN];
int n, m, s;
struct Edge{
	int to, dis, next;
	int num;
}edge[MAXN << 1], ask[MAXN << 1]; 
void add_edge(int u, int v, int dis) {
	edge[++cnt].to = v;
	edge[cnt].next = head[u];
	head[u] = cnt;
}
void add_ask(int x, int y, int num) { //num 第几个查询
	ask[++cnt_ask].to = y;
	ask[cnt_ask].num = num;	//第几个查询 
	ask[cnt_ask].next = head_ask[x];
	head_ask[x] = cnt_ask;
}
int find(int x) {	//并查集 
	return fa[x] == x ? x : fa[x] = find(fa[x]);
}
void init() {
	cnt = 1;
	cnt_ask = 1;
	memset(vis, 0, sizeof(vis));
	fa[n] = n;
}
void Tarjan(int x) {
	vis[x] = true;
	for(int i = head[x]; i ; i = edge[i].next) {
		int to = edge[i].to;
		if( !vis[to] ) {
			Tarjan(to);
			fa[to] = x;
		}
	}
	for(int i = head_ask[x]; i; i = ask[i].next) {
		int to = ask[i].to;
		if( vis[to] ){ 
			ans[ask[i].num] = find(to);
		}
	}
}
int main () {
	ios::sync_with_stdio(0);
    cin.tie(0);
	cin >> n >> m >> s;
	int x, y;
	init();
	for(int i = 1; i < n; ++i) {
		fa[i] = i;
		cin >> x >> y;
		add_edge(x, y, 0);
		add_edge(y, x, 0);
	}
	for(int i = 1; i <= m; ++i) {
		cin >> x >> y;
		add_ask(x, y, i);
		add_ask(y, x, i);
	} 
	Tarjan(s);	 
	for(int i = 1; i <= m; ++i) {
		cout << ans[i] << endl;
	}
} 

练习题:

洛谷 P3379

LCA 倍增:

补习顺序:

        1)白话理解

        2)初始代码

        3)优化:

                1.链式向前星存图

                2. 把a, b 两个节点抬升到同一高度,用二进制思想。

其他 :链式向前星

我对倍增的理解:

    1)初次了解LCA(最近公共祖先)我只能想到在树上的两点,同时一步一步往上跳,直到相遇就是两个点的LCA。

    2)然后学习了倍增优化,每次跳  2^{i} 步,不再一步一步跳,这样我们可以节省大量的时间 ( 树越深体现越深 ) 

          1. fa [ i ][ j ] 表示 结点 i 向上 2^{j} 层的祖先

          2. fa [ i ][ 0 ] 表示 结点 i 向上 2^{0}  = 1 层的祖先,即 i 的父亲节点, (fa[ i ][ 0 ] 就是递推的基础条件)

          3. fa[ i ][ j ] =  fa[ fa [ i ][ j - 1] ] [j -1] 对这个式子的理解 :fa[ i ][j - 1]是结点 i 往上跳2^{j-1}层的祖先,那我们就在跳到这个结点的基础上,再向上跳2^{j-1}层,这样就相当于从结点 i,先跳2^{j-1}层,再跳2^{j-1}层,最后还是到达了 2^{j} 层.

         4. 如果用链式向前星存图时,在对树的深搜时我们就可以保存每个结点的 第 2^{i} 步跳到的父亲结点。

//更新每个节点的深度, 在搜索过程中 
void DFS (int now, int pre) {	 
	depth[now] = depth[pre] + 1;
	fa[now][0] = pre;
	for (int i = 1; (1 << i) <= depth[now]; ++i) {
		fa[now][i] = fa[fa[now][i - 1]][i - 1];
	}
	for(int i = head[now]; i; i = edge[i].next) {
		if(edge[i].to != pre) {
			DFS(edge[i].to, now);
		}
	}
}

LCA部分

int LCA(int a, int b) {
	//让 a 处于更低的深度 
	if (depth[a] < depth[b]) swap(a, b);
	//然后让 a 向上爬,爬到与b相同深度
	int dep = depth[a] - depth[b];
	for (int i = 0; (1 << i) <= dep; ++i) {
		if ((1 << i) & dep) {
			a = fa[a][i];
		}
	}
	//如果 b 爬到与 a 同一深度时 a == b 则直接返回该节点(该节点就是a b的LCA)  
	if(a == b) return a;
	//否者 a b 同时向上倍增  从最远的开始试
	for (int i = log2(depth[a]); i >= 0; --i) {
			//如果父亲不同就向上跳, 如果父亲相同就减小距离再比较,直到不相同在跳。 
		if (fa[a][i] != fa[b][i]) {
			a = fa[a][i];
			b = fa[b][i];	
		}
	} 
	return fa[a][0];
	//跳到最后 a 和 b 距离最近的LCA只差一步 所以返回 dp[a][0] 即可 
}

整体模板

const int MAXN = ;
int head[MAXN], cnt;
int fa[MAXN][20], depth[MAXN];
struct Edge{
	int to, dis, next;
}edge[MAXN << 1];
void add_edge(int u, int v, int dis) {
	edge[++cnt].to = v;
	edge[cnt].dis = 0;
	edge[cnt].next = head[u];
	head[u] = cnt;
}
void init() {
	cnt = 1;
	memset(head, 0, sizeof(head));
	memset(depth, 0 , sizeof(depth));
	memset(fa, 0, sizeof(fa));
}
//更新每个节点的深度, 在搜索过程中 
void DFS (int now, int pre) {	 
	depth[now] = depth[pre] + 1;
	fa[now][0] = pre;
	for (int i = 1; (1 << i) <= depth[now]; ++i) {
		fa[now][i] = fa[fa[now][i - 1]][i - 1];
	}
	for(int i = head[now]; i; i = edge[i].next) {
		if(edge[i].to != pre) {
			DFS(edge[i].to, now);
		}
	}
}
int LCA(int a, int b) {
	//让 a 处于更低的深度 
	if (depth[a] < depth[b]) swap(a, b);
	//然后让 a 向上爬,爬到与b相同深度
	int dep = depth[a] - depth[b];
	for (int i = 0; (1 << i) <= dep; ++i) {
		if ((1 << i) & dep) {
			a = fa[a][i];
		}
	}
	//如果 b 爬到与 a 同一深度时 a == b 则直接返回该节点(该节点就是a b的LCA)  
	if(a == b) return a;
	//否者 a b 同时向上倍增  从最远的开始试
	for (int i = log2(depth[a]); i >= 0; --i) {
			//如果父亲不同就向上跳, 如果父亲相同就减小距离再比较,直到不相同在跳。 
		if (fa[a][i] != fa[b][i]) {
			a = fa[a][i];
			b = fa[b][i];	
		}
	} 
	return fa[a][0];
	//跳到最后 a 和 b 距离最近的LCA只差一步 所以返回 dp[a][0] 即可 
}

DFS(S, 0);
LCA(x, y);

 

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
最近公共祖先(Lowest Common Ancestor, LCA问题是指在给定一棵树中找到两个节点的最短路径上的共同祖先。Tarjan算法通常用于解决这个问题,但它的主要目的是为了发现图中的强连通分量(Strongly Connected Components, SCC),而不是直接计算LCA。不过,由于这两种问题都涉及到深度优先搜索和拓扑排序的思想,所以我们可以借助Tarjan算法的思路来理解LCATarjan算法是基于深度优先搜索和一种称为“DFS树”的数据结构。在寻找LCA的过程中,如果能找到两个节点在同一棵DFS树或它们的DFS祖先相同,那么这两个节点就是最近公共祖先。这里的关键在于维护节点的前驱(pred)和后继(succ)指针,以及一个秩(rank)数组来判断边的方向,以确定节点是否构成一个回路。 下面是 Tarjan 算法的主要步骤: 1. 初始化:对于每个未访问的节点 u,设置其秩 rank[u] = 次序号(u 的编号),低link[u] = u(表示 u 的父节点),深度 depth[u] = 0,访问次数和栈顶指针为 null。 2. DFS 递归过程:从根节点开始遍历,对每个子节点 v,执行以下操作: a. 如果 v 没有被访问过,则进行一次深度优先搜索,更新深度、秩和低link信息。 b. 记录 v 的秩 rank[v] 和当前的深度 depth[v],并将 v 加入到相应的 DFS 树。 c. 更新 v 的所有前驱和后继指针。 d. 如果 v 是回路的一部分,将 v 设置为它自身的低link,这会导致算法在下一个阶段检测到回路。 3. 完成搜索后,对每个节点 u,检查 lowlink[u] 是否等于 u,如果是,说明 u 和 u 是同一个强连通分量内的节点。同时,这也帮助我们找到 u 和其他节点的最近公共祖先,因为他们在同一棵树上。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值