Lowest Common Ancestors
flyhite - minami
美波美波,没有你我可怎么活啊
问题描述
对于有根树 T 的两个结点 u、v,最近公共祖先 LCA(T, u, v) 表示一个结点x,满足 x 是 u 和 v 的祖先且 x 离根尽可能的远。在这里,一个节点也可以是它自己的祖先。
数据格式参考 [洛谷 P3379]
搜索(暴力解)
这里将 LCA 的实例方法私有化了,别问,问就是面向对象。
import java.util.*;
public class Main {
public static void main(String[] args) { new Main().run(); }
public void run() {
LCA lca = LCA.of(N, root, edges);
}
public static class LCA {
public static class Edge {
int v, w;
Edge(int v, int w) {
this.v = v;
this.w = w;
}
}
public static LCA of(int N, int root, Collection<Edge> edges) { return new LCA(N, root, edges); }
int root;
List<Integer>[] graph;
LCA(int N, int root, Collection<Edge> edges) {
this.root = root;
graph = new List[N + 1];
for (int i = 0; i <= N; i++)
graph[i] = new ArrayList(3);
for (Edge edge : edges) {
graph[edge.v].add(edge.w);
graph[edge.w].add(edge.v);
}
}
public int query(int u, int v) { return dfs(root, -1, u, v) - 3; }
public int dfs(int root, int fa, int u, int v) {
int ans = root == u || root == v ? 1 : 0;
for (int w : graph[root]) {
if (w == fa) continue;
ans += dfs(w, root, u, v);
if (ans > 2)
return ans;
if (ans == 2)
return root + 3;
}
return ans;
}
}
}
我们把每次调用 dfs 的返回值定义为 状态D,如果遍历到了 u、v 其中的一个,我们将 D 设为 1。对于每一个 D 我们都使其累加上它的子节点的 D,因此当 D = 2 时,当前节点就是 LCA(u, v) 的结果,这时我们将 D + 节点序号 + 1(节点序号可能为0),并规定当 D 大于 2 时直接返回 D。这样,当最后一个方法出栈,我们就拿到了 LCA(u, v)。
太暴力了,期望的复杂度不谈
树链剖分
树链剖分是一种树的划分算法,通过节点的轻重边将树剖分成多条链,并保证每一个节点属于且仅属于一条链。
可能这个算法不太常见,于是这里我给出这个名词的定义:
重儿子:子节点中最大的节点
轻儿子:子节点中除重儿子以外所有节点
重边:父节点和重儿子构成的边
轻边:除去重边以外的所有边
重链:由连通的重边构成的链
轻链:由连通的轻边构成的链
为了实现树的剖分,我们一般需要两遍 dfs 遍历:
第一遍 找出出全部重儿子,并记录每个节点的深度。
第二遍 链起所有重边构成重链。
由于从根节点到某一节,不会有超过 logn 条轻边和 logn 条重链,因此树链剖分算法的复杂程度得以了保证。
import java.util.*;
public class Main {
public static void main(String[] args) { new Main().run(); }
public void run() {
LCA lca = LCA.of(N, root, edges);
}
public static class LCA {
public static class Edge {
int v, w;
Edge(int v, int w) {
this.v = v;
this.w = w;
}
}
public static LCA of(int N, int root, Collection<Edge> edges) { return new LCA(N, root, edges); }
int[] linked, father, depth;
LCA(int N, int root, Collection<Edge> edges) {
List<Integer>[] graph = new List[N + 1];
depth = new int[N + 1];
linked = new int[N + 1];
father = new int[N + 1];
for (int i = 0; i <= N; i++)
graph[i] = new ArrayList(3);
for (Edge edge : edges) {
graph[edge.v].add(edge.w);
graph[edge.w].add(edge.v);
}
int[] weight = new int[N + 1];
dfs(root, -1, 0, weight, graph);
linked[root] = root;
dfs(root, - 1, weight, graph);
father[root] = root;
}
public int query(int u, int v) {
return linked[u] == linked[v] ?
(depth[u] < depth[v] ? u : v) :
(depth[linked[u]] < depth[linked[v]] ? query(u, father[linked[v]]): query(father[linked[u]], v));
}
public int dfs(int v, int fa, int depth, int[] weight, List<Integer>[] graph) {
int size = 1, max = 0, now;
this.depth[v] = depth;
this.father[v] = fa;
for (int w : graph[v]) {
if (w == fa) continue;
size += now = dfs(w, v, depth + 1, weight, graph);
if (now > max) {
weight[v] = w;
max = now;
}
}
return size;
}
public void dfs(int v, int fa, int[] weight, List<Integer>[] graph) {
for (int w : graph[v]) {
if (w == fa) continue;
if (weight[v] != w) linked[w] = w;
else linked[w] = linked[v];
dfs(w, v, weight, graph);
}
}
}
}
当我们将树完全剖分后,这时我们求 LCA(u, v),
如 u、v 在一条链上,那么 LCA(u, v) 为 u、v 中深度较小的节点。
如 u、v 不在一条链上,我们将在深度较大的链的节点跳至链头,再从链头跳至父节点,重复此过程,直至 u、v 在一条链上。
期望复杂程度 <O(n),O(logn)>
dfs序 & 欧拉序
下列算法都依赖标题的应用,这里简单提一下
有如下树状图:
可以说 dfs序 等于 先序遍历的结果。
于是有 dfs序:{ 7,3, 1, 2, 5, 4, 6, 8, 9 }
过。
欧拉序则不单是在第一次访问时记录,回溯时也会将经过的节点一并保存下来。
于是有 欧拉序:{ 7, 3, 1, 3, 2, 3, 5, 4, 5, 3, 7, 6, 7, 8, 9, 8, 7 }
对于一个有 n 个节点的树状图,它的 欧拉序 序列的长度为 2n - 1。
我们将第一次访问时的记录标记出来。
{ 7, 3, 1, 3, 2, 3, 5, 4, 5, 3, 7, 6, 7, 8, 9, 8, 7 }
不难发现在任意两个标记括起的子序列中包含着它们的 LCA,且 LCA 所在的深度最小,剩下的就在下面说吧。
朴素算法
其实还是暴力
public class Main {
public static void main(String[] args) { new Main().run(); }
public void run() {
LCA lca = LCA.of(N, root, edges);
}
public static class LCA {
public static class Edge {
int v, w;
Edge(int v, int w) {
this.v = v;
this.w = w;
}
}
public static LCA of(int N, int root, Collection<Edge> edges) { return new LCA(N, root, edges); }
int clock;
int[] stamp, father;
LCA(int N, int root, Collection<Edge> edges) {
List<Integer>[] graph = new List[N + 1];
father = new int[N + 1];
stamp = new int[N + 1];
for (int i = 0; i <= N; i++)
graph[i] = new ArrayList(3);
for (Edge edge : edges) {
graph[edge.v].add(edge.w);
graph[edge.w].add(edge.v);
}
dfs(graph, root, -1);
}
public int query(int u, int v) {
if (stamp[u] == stamp[v]) return u;
return stamp[u] > stamp[v] ?
query(father[u], v) : query(u, father[v]);
}
private void dfs(List<Integer>[] graph, int v, int fa) {
father[v] = fa;
stamp[v] = clock++;
for (int w : graph[v]) {
if (w == fa) continue;
dfs(graph, w, v);
}
}
}
}
我们先按 dfs序 预处理出每个节点出现的时间戳,同时记录每个节点的父节点序号。
对于每次查询,
如果查询节点的时间戳相等,也就意味着它们是相同的节点,等价于 LCA 为自身。
如果查询节点的时间戳不想等,我们将时间戳大的一方溯回到其父节点,重复此过程,直至如果查询节点的时间戳相等。
最坏情况就是树退化成链表,那就是 GG 了,
期望就是取随机树高
期望时间复杂度 <O(n),O(logn)>
倍增算法
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class Main {
public static void main(String[] args) { new Main().run(); }
public void run() {
Tree tree = new Tree(N);
tree.link(x, y);
tree.setRoot(S);
tree.LCA(u, v);
}
public class Tree {
int N;
int[][] ST;
int[] depth;
List<Integer>[] V;
Tree(int v) {
this.V = new List[N = v];
ST = new int[N][log2(N) + 1];
depth = new int[N];
while (v-- > 0)
V[v] = new ArrayList(3);
}
public void setRoot(int v) {
Arrays.fill(depth, 0);
for (int w : V[v])
dfs(w, v);
}
private void dfs(int now, int fa) {
depth[now] = depth[fa] + 1;
ST[now][0] = fa;
for (int i = 1; 1 << i <= depth[now]; i++)
ST[now][i] = ST[ST[now][i - 1]][i - 1];
for (int v : V[now])
if (v != fa) dfs(v, now);
}
public void link(int v, int w) {
V[v].add(w);
V[w].add(v);
}
public int LCA(int u, int v) {
if (depth[u] < depth[v]) return LCA(v, u);
while (depth[u] > depth[v])
u = ST[u][log2(depth[u] - depth[v])];
if (u == v) return u;
for (int k = log2(depth[u]); k >= 0; k--) {
if (ST[u][k] != ST[v][k]) {
u = ST[u][k];
v = ST[v][k];
}
}
return ST[u][0];
}
private int log2(int a) { return (int)(Math.log(a) / Math.log(2)); }
}
}
用 O(nlogn) 的处理时间换来了稳定的 O(logn) 的查询时间复杂度
算得上值当
Tarjan 算法
Tarjan算法是一种离线算法,它仅需一遍 dfs,在回溯过程中就能完成所有的 LCA 查找操作。
当我们搜索到某个节点的时候,我们会认为当前节点是自身的父节点,也就是以当前节点为根。当搜索完该节点的所有子节点后,我们再将这个节点的父节点设置为这个节点的上级节点。
这里以 LCA(u, v) 来举两个例子来解释一下此举的作用:
若 LCA(u, v) = u,我们搜索到 u 时,把 u 的父节点设为自己,搜索到 v 时,发现 u 已经被搜索过,记录下 u 的父节点,也就是 LCA(u, v) = u, LCA(u, v) = v 同理。
若 LCA(u, v) = a,若 u, v 同在 a 的一个子树里,转换为上一个例子。若不在则有,搜索到 v 时,发现 u 已经被搜索过了,记录下 u 的父节点,也就是 LCA(u, v) = a,若搜索 v 发生在搜索 u 之前,则恰反。
因此在回溯时,查并集也有了一展拳脚的地方
值得注意的是在 Tarjan算法 里每个节点只会被访问一次,所以 并查集 find() 方法均摊复杂度在O(1)。
对于整个初始化过程,能在 O(n + m) 的复杂度下完成
import java.util.*;
public class Main {
public static void main(String[] args) { new Main().run(); }
public void run() {
answer = LCA.of(N, S, edges, query);
}
public static class LCA {
public static class Edge {
int v, w;
Edge(int v, int w) {
this.v = v;
this.w = w;
}
int other(int v) { return v == this.v ? this.w : this.v; }
}
private static class Query {
int index;
Edge edge;
Query(int index, Edge edge) {
this.index = index;
this.edge = edge;
}
}
public static int[] of(int N, int root, Collection<Edge> edges, List<Edge> queryList) {
int M = queryList.size(), idx = 0;
int[] answer = new int[M];
List<Query>[] query = new List[N + 1];
List<Integer>[] graph = new List[N + 1];
for (int i = 0; i <= N; i++) {
query[i] = new ArrayList();
graph[i] = new ArrayList();
}
for (Edge edge : edges) {
graph[edge.v].add(edge.w);
graph[edge.w].add(edge.v);
}
for (Edge que : queryList) {
Query quer = new Query(idx++, que);
query[que.v].add(quer);
query[que.w].add(quer);
}
tarjan(graph, query, answer, new int[N + 1], new boolean[N + 1], root);
return answer;
}
private static void tarjan(List<Integer>[] graph, List<Query>[] query, int[] answer, int[] linked, boolean[] marked, int v) {
linked[v] = v;
marked[v] = true;
for (int w : graph[v]) {
if (marked[w]) continue;
tarjan(graph, query, answer, linked, marked, w);
linked[w] = v;
}
for (Query que : query[v])
if (marked[que.edge.other(v)])
answer[que.index] = find(linked, que.edge.other(v));
}
private static int find(int[] linked, int v) { return linked[v] == v ? v : (linked[v] = find(linked, linked[v])); }
}
}
用欧拉序规约成约束RMQ问题
对于问题 LCA(i, j)
,不难发现其结果为由i
、j
为端点组成的连续欧拉序子序列中,深度最小的节点。
基于这一点,我们可以将 LCA 问题规约成 约束(±1)RMQ 问题。其瓶颈在于RMQ,如果我们能在线性时间内求解 RMQ,那么也就意味我们能在线性时间内求解 LCA。
并且这是一个在线算法。
Range Minimum/Maximum Query 指路
import java.util.*;
public class Main {
public static void main(String[] args) { new Main().run(); }
public void run() {
LCA lca = LCA.of(N, root, edges);
}
public static class LCA {
private int clock;
private PlusOrMinusOneRMQ rmq;
private int[] table, euler, stamp;
public static class Edge {
int v, w;
Edge(int v, int w) {
this.v = v;
this.w = w;
}
}
public static LCA of(int N, int root, Collection<Edge> edges) { return new LCA(N, root, edges); }
LCA(int N, int root, Collection<Edge> edges) {
int M = edges.size();
table = new int[N + 1];
euler = new int[(M + 1 << 1) - 1];
stamp = new int[(M + 1 << 1) - 1];
List<Integer>[] graph = new List[N + 1];
for (int i = 0; i <= N; i++)
graph[i] = new ArrayList();
for (Edge edge : edges) {
graph[edge.v].add(edge.w);
graph[edge.w].add(edge.v);
}
clock = 0;
dfs(graph, root, root, 0);
rmq = new PlusOrMinusOneRMQ(stamp);
}
public int query(int i, int j) { return euler[rmq.query(table[i], table[j])]; }
void dfs(List<Integer>[] graph, int v, int fa, int depth) {
table[v] = clock;
euler[clock] = v;
stamp[clock++] = depth;
for (int w : graph[v]) {
if (w == fa) continue;
dfs(graph, w, v, depth + 1);
euler[clock] = v;
stamp[clock++] = depth;
}
}
private class PlusOrMinusOneRMQ {
int N, M, K;
int[][] ST;
int[][][] F;
int[] A, block;
PlusOrMinusOneRMQ(int[] A) { this(A, 0.5); }
PlusOrMinusOneRMQ(int[] A, double xK) {
this.N = A.length;
this.K = max(1, (int)(floorLog2(N) * xK));
this.M = N / K;
if (N % K > 0) M++;
block = new int[M];
F = new int[1 << K - 1][K][K];
ST = new int[M][floorLog2(M) + 1];
this.A = A;
init();
}
public int query(int i, int j) {
if (i > j) return query(j, i);
int B1 = i / K, B2 = j / K;
int O1 = i % K, O2 = j % K;
if (B1 == B2)
return B1 * K + F[block[B1]][O1][O2];
int LM = F[block[B1]][O1][K - 1] + B1 * K,
RM = F[block[B2]][0][O2] + B2 * K;
int res = A[LM] < A[RM] ? LM : RM;
if (B2 - B1 > 1) {
int k = floorLog2(B2 - B1 - 1);
int MLM = ST[B1 + 1][k], MRM = ST[B2 - (1 << k)][k];
int ans = A[MLM] < A[MRM] ? MLM : MRM;
if (A[ans] < A[res]) res = ans;
}
return res;
}
void init() {
int min, now, block, offset;
for (int i = 0; i < M; i++) {
min = offset = i * K;
block = 0;
for (int k = 1; k < K && offset + k < N; k++)
if (A[offset + k] < A[offset + k - 1]) {
block |= 1 << k - 1;
if (A[offset + k] < A[min])
min = offset + k;
}
this.block[i] = block;
this.ST[i][0] = min;
}
for (int k = 1; k <= floorLog2(M); k++)
for (int i = 0; i + (1 << k - 1) < M; i++)
ST[i][k] = A[ST[i][k - 1]] < A[ST[i + (1 << k - 1)][k - 1]] ? ST[i][k - 1] : ST[i + (1 << k - 1)][k - 1];
for (int k = 0, m = 1 << K - 1; k < m; k++)
for (int i = 0; i < K; i++) {
F[k][i][i] = i;
now = 0;
min = 0;
for (int j = i + 1; j < K; j++) {
F[k][i][j] = F[k][i][j - 1];
if ((k & (1 << j - 1)) == 0) now++;
else if (min > --now) {
F[k][i][j] = j;
min = now;
}
}
}
}
}
}
}
< O ( n ) , O ( 1 ) > <O(n),O(1)> <O(n),O(1)>
虽然能在线性规模的时间内完成初始化,能在常数意义下完成单次查询,但该算法不论时间空间常数都极大,不推荐使用。