tarjan算法 java_离线LCA(Tarjan)算法详解

本文详细介绍了如何使用Tarjan算法离线解决最近公共祖先(LCA)问题,通过实例和代码解析算法过程,最终达到$O(n+q)$的时间复杂度。
摘要由CSDN通过智能技术生成

关于离线算法

(下面内容可以略过。)

离线算法其实就是将多个询问一次性解决。离线算法往往是与在线算法相对的。例如求LCA的算法中,树上倍增属于在线算法,在对树进行$O(n)$预处理后,每个询问用$O(log_2n)$复杂度回答。而离线的Tarjan算法则是用$O(n+q)$时间将询问一次性全部回答。

详解

下面是一棵树,我们将以这棵树为例子讲解Tarjan算法,其中0号点为根。

4f4bb90181d28d216fc0b803fbfd78e7.png

假设对于这棵树的询问有4个,分别询问:

$LCA(2,8)$

$LCA(5,6)$

$LCA(2,5)$

$LCA(4,9)$

首先我们将这四个询问顺序调转,再复制四份,现在就有8个询问:

$LCA(2,8)$

$LCA(5,6)$

$LCA(2,5)$

$LCA(4,9)$

$LCA(8,2)$

$LCA(6,5)$

$LCA(5,2)$

$LCA(9,4)$

这一步是必须的,后面将会说明它。

然后对于每个节点u,给它开一个链表,找到所有的询问 $LCA(u,v)$ ,把v插入到u的链表后,同时把询问编号插入,以便按照输入顺序输出答案。

于是询问就被离线了。

那么到底怎么求LCA呢?我们对带着询问树进行一次dfs。如图:

第1步,0号点被遍历:

d38060d4d7b4f86961842f5f7e622fd0.png

没有与0相关的询问,继续dfs。

第2步,1号点被遍历:

d9ebe7cf42d52a913a0cbaae0ece9e23.png

没有与1相关的询问,继续dfs。

第3步,2号点被遍历:

0a385c34873017d039d2a003163e071d.png

2号点没有儿子了,与2相关的询问有 $LCA(2,5)$ 和 $LCA(2,8)$ 。

但是5号点和8号点都还没有遍历过,我们什么也不知道,因此这两个询问不理它。

第4步,2号点回溯(遍历完毕并回溯的点标为蓝色):

3058751e047f1f1372eca4a6faa31363.png

第5步,3号点被遍历:

1fab2101d684119cb13efc116b779e87.png

ef3b30796ea03697cac0c7a9eaf89cdc.png

没3号点的事,继续dfs。

第6步,4号点被遍历:

42313ea6257a8aa4024fd24420ffea39.png

关于4号点的询问我们也是一无所知,回溯。

第7步,4号点回溯:

84d3b0e9e160f591ad6e45a525fc14e8.png

第8步,5号点被遍历:

ef3b30796ea03697cac0c7a9eaf89cdc.png

关于5的询问有 $LCA(5,6)$ 和 $LCA(5,2)$ 。

6号点的信息我们还不知道,但是2号点,我们已经知道它已经被访问且回溯了。

5的祖先一定在当前正在访问的节点中(也就是访问了还没回溯的点),那么

$LCA(5,2)$ 其实也就是在图上红色的节点里找出满足如下两个条件的点:

1.它是2的祖先。

2.它深度最大。

很容易发现这个点就是1,于是这里就可以记录下来 $LCA(5,2)=1$ 。

第9步,5号点回溯:

b4974e94f757296af1cff0861bb24282.png

第10步,3号点回溯:

6d240e76c3642a2a900ba94c48b05561.png

第11步,6号点被遍历:

6a44ec6231633964fe4344b794caee85.png

还是跟之前一样,对于跟6号点有关的询问 $LCA(6,5)$ ,去找红色点里深度最大的5的祖先,显然就是1,记下 $LCA(6,5)=1$。

第12步,6号点回溯。

333ad3993ed7cfa6b7ed4a00c985599d.png

第13步,1号点回溯:

9c28293df7df944cc348c846b5926865.png

第14步,7号点被遍历:

c15e6c7e91c4811ec727bfb9872edd8b.png

第15步,8号点被遍历:

cf193c95a36785bc0aa65a47d40c682a.png

按照之前做法,在红色节点里找出深度最大的2的祖先,可以求出 $LCA(8,2)=0$ 。

第16步,8号点回溯:

6f8767307abf35cc2b6454c793dbfce7.png

第17步,9号点被遍历:

8481751e0c75ac523d6ebf7617f46cb9.png

显然了,$LCA(9,4)=0$ 。

后面的过程就略过,因为至此我们已经求出了四个询问的答案。

$LCA(2,8)=0$

$LCA(5,6)=1$

$LCA(2,5)=1$

$LCA(4,9)=0$

也许你已经明白了,为什么要把$LCA(u,v)$复制一份$LCA(v,u)$,因为在上面过程中,我们不能保证遍历u时v已经回溯,因此需要复制一个询问。

上面的过程已经可以离线求出LCA了,但复杂度不是最优的,问题就出在上面找“红色节点中u的深度最大的祖先”,如果从u点一步步向上跳,复杂度为$O(nq)$。

假如对于一个询问$LCA(u,v)$,u已经被遍历过,此时遍历到v。容易发现$LCA(u,v)$一定是红色的(也就是访问了还未回溯)。那么如果我们在dfs的过程中,在节点u的儿子遍历完毕回溯时,将儿子的fa指向点u,那么对于询问$LCA(u,v)$,只需要从u开始,不断往u的父亲跳,跳到的深度最小一个节点,就是$LCA(u,v)$。

怎么去证明呢?首先其必是u的祖先,这个不用说。但为什么是深度最小的那一个呢?不是要求深度最大的吗?因为我们是在回溯时将u的fa指向它的父亲的,如果深度不是最小,则u的这个祖先的子树里肯定没有v。如果有v的话,其必然是深度最小的那一个。由于u已访问完毕,而v还在访问中,因此u的父亲里不会有比$LCA(u,v)$ 深度更大的点,此时就能保证u的fa里深度最小的那个就是$LCA(u,v)$

“将儿子的父亲指向点u”这个操作用并查集完成,可以保证在常数复杂度。因此对树进行遍历需要$O(n)$复杂度,而总共有q个询问,每个询问可以$O(1)$回答,复杂度为$O(n+q)$。

看不懂没关系,结合代码来理解:

参考题目洛谷P3379【模板】最近公共祖先

#include

#include

using namespace std;

inline int read()

{

int x = 0, f = 0;

char c = getchar();

for (; c < '0' || c > '9'; c = getchar()) if (c == '-') f = 1;

for (; c >= '0' && c <= '9'; c = getchar()) x = (x << 1) + (x << 3) + (c ^ '0');

return f ? -x : x;

}

const int N = 5e5 + 7;

int n, m, u, v, s;

int tot = 0, st[N], to[N << 1], nx[N << 1], fa[N], ans[N], vis[N];

struct note { int node, id; }; //询问以结构体形式保存

vector ques[N];

inline void add(int u, int v) { to[++tot] = v, nx[tot] = st[u], st[u] = tot; }

inline int getfa(int x) { return fa[x] == x ? x : fa[x] = getfa(fa[x]); } //并查集的getfa操作,路径压缩

void dfs(int u, int from)

{

for (int i = st[u]; i; i = nx[i]) if (to[i] != from) dfs(to[i], u), fa[to[i]] = u; //将u的儿子合并到u

int len = ques[u].size(); //处理与u有关的询问

for (int i = 0; i < len; i++) if (vis[ques[u][i].node]) ans[ques[u][i].id] = getfa(ques[u][i].node); //对应的v已经访问并回溯时,LCA(u,v)就是v的fa里深度最小的一个也就是getfa(v)

vis[u] = 1; //访问完毕回溯

}

int main()

{

n = read(), m = read(), s = read();

for (int i = 1; i < n; i++) u = read(), v = read(), add(u, v), add(v, u);

for (int i = 1; i <= m; i++) u = read(), v = read(), ques[u].push_back((note){v, i}), ques[v].push_back((note){u, i}); //记下询问编号便于输出

for (int i = 1; i <= n; i++) fa[i] = i;

dfs(s, 0);

for (int i = 1; i <= m; i++) printf("%d\n", ans[i]); //输出答案

return 0;

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值