关于离线算法
(下面内容可以略过。)
离线算法其实就是将多个询问一次性解决。离线算法往往是与在线算法相对的。例如求LCA的算法中,树上倍增属于在线算法,在对树进行
O
(
n
)
O(n)
O(n)预处理后,每个询问用
O
(
l
o
g
2
n
)
O(log_2n)
O(log2n)复杂度回答。而离线的Tarjan算法则是用
O
(
n
+
q
)
O(n+q)
O(n+q)时间将询问一次性全部回答。
详解
下面是一棵树,我们将以这棵树为例子讲解Tarjan算法,其中0号点为根。
假设对于这棵树的询问有4个,分别询问:
L
C
A
(
2
,
8
)
LCA(2,8)
LCA(2,8)
L
C
A
(
5
,
6
)
LCA(5,6)
LCA(5,6)
L
C
A
(
2
,
5
)
LCA(2,5)
LCA(2,5)
L
C
A
(
4
,
9
)
LCA(4,9)
LCA(4,9)
首先我们将这四个询问顺序调转,再复制四份,现在就有8个询问:
L
C
A
(
2
,
8
)
LCA(2,8)
LCA(2,8)
L
C
A
(
5
,
6
)
LCA(5,6)
LCA(5,6)
L
C
A
(
2
,
5
)
LCA(2,5)
LCA(2,5)
L
C
A
(
4
,
9
)
LCA(4,9)
LCA(4,9)
L
C
A
(
8
,
2
)
LCA(8,2)
LCA(8,2)
L
C
A
(
6
,
5
)
LCA(6,5)
LCA(6,5)
L
C
A
(
5
,
2
)
LCA(5,2)
LCA(5,2)
L
C
A
(
9
,
4
)
LCA(9,4)
LCA(9,4)
这一步是必须的,后面将会说明它。
然后对于每个节点u,给它开一个链表,找到所有的询问
L
C
A
(
u
,
v
)
LCA(u,v)
LCA(u,v) ,把v插入到u的链表后,同时把询问编号插入,以便按照输入顺序输出答案。
于是询问就被离线了。
那么到底怎么求LCA呢?我们对带着询问树进行一次dfs。如图:
第1步,0号点被遍历:
没有与0相关的询问,继续dfs。
第2步,1号点被遍历:
没有与1相关的询问,继续dfs。
第3步,2号点被遍历:
2号点没有儿子了,与2相关的询问有
L
C
A
(
2
,
5
)
LCA(2,5)
LCA(2,5) 和
L
C
A
(
2
,
8
)
LCA(2,8)
LCA(2,8) 。
但是5号点和8号点都还没有遍历过,我们什么也不知道,因此这两个询问不理它。
第4步,2号点回溯(遍历完毕并回溯的点标为蓝色):
第5步,3号点被遍历:
没3号点的事,继续dfs。
第6步,4号点被遍历:
关于4号点的询问我们也是一无所知,回溯。
第7步,4号点回溯:
第8步,5号点被遍历:
关于5的询问有
L
C
A
(
5
,
6
)
LCA(5,6)
LCA(5,6) 和
L
C
A
(
5
,
2
)
LCA(5,2)
LCA(5,2) 。
6号点的信息我们还不知道,但是2号点,我们已经知道它已经被访问且回溯了。
5的祖先一定在当前正在访问的节点中(也就是访问了还没回溯的点),那么
L
C
A
(
5
,
2
)
LCA(5,2)
LCA(5,2) 其实也就是在图上红色的节点里找出满足如下两个条件的点:
1.它是2的祖先。
2.它深度最大。
很容易发现这个点就是1,于是这里就可以记录下来
L
C
A
(
5
,
2
)
=
1
LCA(5,2)=1
LCA(5,2)=1 。
第9步,5号点回溯:
第10步,3号点回溯:
第11步,6号点被遍历:
还是跟之前一样,对于跟6号点有关的询问
L
C
A
(
6
,
5
)
LCA(6,5)
LCA(6,5) ,去找红色点里深度最大的5的祖先,显然就是1,记下
L
C
A
(
6
,
5
)
=
1
LCA(6,5)=1
LCA(6,5)=1。
第12步,6号点回溯。
第13步,1号点回溯:
第14步,7号点被遍历:
第15步,8号点被遍历:
按照之前做法,在红色节点里找出深度最大的2的祖先,可以求出
L
C
A
(
8
,
2
)
=
0
LCA(8,2)=0
LCA(8,2)=0 。
第16步,8号点回溯:
第17步,9号点被遍历:
显然了,
L
C
A
(
9
,
4
)
=
0
LCA(9,4)=0
LCA(9,4)=0 。
后面的过程就略过,因为至此我们已经求出了四个询问的答案。
L
C
A
(
2
,
8
)
=
0
LCA(2,8)=0
LCA(2,8)=0
L
C
A
(
5
,
6
)
=
1
LCA(5,6)=1
LCA(5,6)=1
L
C
A
(
2
,
5
)
=
1
LCA(2,5)=1
LCA(2,5)=1
L
C
A
(
4
,
9
)
=
0
LCA(4,9)=0
LCA(4,9)=0
也许你已经明白了,为什么要把
L
C
A
(
u
,
v
)
LCA(u,v)
LCA(u,v)复制一份
L
C
A
(
v
,
u
)
LCA(v,u)
LCA(v,u),因为在上面过程中,我们不能保证遍历u时v已经回溯,因此需要复制一个询问。
上面的过程已经可以离线求出LCA了,但复杂度不是最优的,问题就出在上面找“红色节点中u的深度最大的祖先”,如果从u点一步步向上跳,复杂度为
O
(
n
q
)
O(nq)
O(nq)。
假如对于一个询问
L
C
A
(
u
,
v
)
LCA(u,v)
LCA(u,v),u已经被遍历过,此时遍历到v。容易发现
L
C
A
(
u
,
v
)
LCA(u,v)
LCA(u,v)一定是红色的(也就是访问了还未回溯)。那么如果我们在dfs的过程中,在节点u的儿子遍历完毕回溯时,将儿子的fa指向点u,那么对于询问
L
C
A
(
u
,
v
)
LCA(u,v)
LCA(u,v),只需要从u开始,不断往u的父亲跳,跳到的深度最小一个节点,就是
L
C
A
(
u
,
v
)
LCA(u,v)
LCA(u,v)。
怎么去证明呢?首先其必是u的祖先,这个不用说。但为什么是深度最小的那一个呢?不是要求深度最大的吗?因为我们是在回溯时将u的fa指向它的父亲的,如果深度不是最小,则u的这个祖先的子树里肯定没有v。如果有v的话,其必然是深度最小的那一个。由于u已访问完毕,而v还在访问中,因此u的父亲里不会有比
L
C
A
(
u
,
v
)
LCA(u,v)
LCA(u,v) 深度更大的点,此时就能保证u的fa里深度最小的那个就是
L
C
A
(
u
,
v
)
LCA(u,v)
LCA(u,v)
“将儿子的父亲指向点u”这个操作用并查集完成,可以保证在常数复杂度。因此对树进行遍历需要
O
(
n
)
O(n)
O(n)复杂度,而总共有q个询问,每个询问可以
O
(
1
)
O(1)
O(1)回答,复杂度为
O
(
n
+
q
)
O(n+q)
O(n+q)。
在此奉上代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+20;
int ver[N],Next[N],head[N],tot,vis[N],f[N],ans[N],n,m,root;
struct aaa{
int id,y;
};
vector < aaa > q[N];
void add(int x,int y)
{
ver[++tot]=y,Next[tot]=head[x],head[x]=tot;
}
int get(int x)
{
if(x==f[x]) return x;
return f[x]=get(f[x]);
}
void trajan(int x)
{
vis[x]=1;
for(int i=head[x];i;i=Next[i])
{
int y=ver[i];
if(vis[y]) continue;
trajan(y);
f[y]=x;
}
for(int i=0;i<q[x].size();i++)
{
int id=q[x][i].id,y=q[x][i].y;
if(vis[y]==2) ans[id]=get(y);
}
vis[x]=2;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n>>m>>root;
for(int i=1;i<=n;i++) f[i]=i;
int x,y;
for(int i=1;i<n;i++) cin>>x>>y,add(x,y),add(y,x);
for(int i=1;i<=m;i++)
{
cin>>x>>y;
if(x==y)
{
ans[i]=x;
continue;
}
q[x].push_back({i,y}),q[y].push_back({i,x});
}
trajan(root);
for(int i=1;i<=m;i++)
cout<<ans[i]<<'\n';
return 0;
}