3

关于离线算法

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

详解

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

假设对于这棵树的询问有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号点被遍历:
图片描述

没有与0相关的询问,继续dfs。
第2步,1号点被遍历:
图片描述

没有与1相关的询问,继续dfs。
第3步,2号点被遍历:
图片描述

2号点没有儿子了,与2相关的询问有 $LCA(2,5)$ 和 $LCA(2,8)$ 。
但是5号点和8号点都还没有遍历过,我们什么也不知道,因此这两个询问不理它。
第4步,2号点回溯(遍历完毕并回溯的点标为蓝色):
图片描述

第5步,3号点被遍历:
图片描述

图片描述
没3号点的事,继续dfs。
第6步,4号点被遍历:
图片描述

关于4号点的询问我们也是一无所知,回溯。
第7步,4号点回溯:
图片描述

第8步,5号点被遍历:
图片描述

关于5的询问有 $LCA(5,6)$ 和 $LCA(5,2)$ 。
6号点的信息我们还不知道,但是2号点,我们已经知道它已经被访问且回溯了。
5的祖先一定在当前正在访问的节点中(也就是访问了还没回溯的点),那么
$LCA(5,2)$ 其实也就是在图上红色的节点里找出满足如下两个条件的点:
1.它是2的祖先。
2.它深度最大。
很容易发现这个点就是1,于是这里就可以记录下来 $LCA(5,2)=1$ 。
第9步,5号点回溯:
图片描述

第10步,3号点回溯:
图片描述

第11步,6号点被遍历:
图片描述

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

第13步,1号点回溯:
图片描述

第14步,7号点被遍历:
图片描述

第15步,8号点被遍历:
图片描述

按照之前做法,在红色节点里找出深度最大的2的祖先,可以求出 $LCA(8,2)=0$ 。
第16步,8号点回溯:
图片描述

第17步,9号点被遍历:
图片描述

显然了,$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 <vector>
#include <cstdio>
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<note> 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;
}

ZJL_OIJR
4 声望0 粉丝

Power