title: 最近公共祖先
date: 2018-09-11 16:57:01
tags: lca
categorithms: algorithms
对于有根树上的两个节点u, v, 最近公共祖先lca(u, v) = x
,x是u, v的祖先并且深度尽可能大
对于这棵树来说lca(9, 10) = 7
, lac(6, 10) = 4
, lac(3, 6) = 1
求lac的算法比较通用的有三种:
tarjan离线算法
RMQ在线算法
倍增lac
tarjan离线
tarjan是一种基于深度优先搜索的求强连通分量的算法, 这里用来求lca的算法本质上是深度优先搜索加并查集
并查集可以看看这篇并查集
tarjan求强连通分量可以看看这篇文章有向图强连通分量的Tarjan算法
先看一下伪代码描述
lca(cur):
ancestor[u] = u//先把祖先标记为自己
visited[u] = true
for v in son[cur]:
lca(v)//访问儿子节点
ancestor[v] = cur
for (cur, v) in query:
if visited[v]://注意是如果v被访问了
lca(cur, v) = ancestor[v]//cur与v的lca就是v的祖先
我们以下图为例分析一下:
假设现在访问到节点9
了, 此时1, 2, 4, 6, 7
节点都已经访问过了, 其中6
已经出栈.
就是说现在已经调用了lca(1), lca(2), lca(4), lca(6)
, 由于6
没有子节点所以lca(6)
已经出栈了, 然后在lca(4)
中继续调用lca(7)
, 再在lca(7)
中调用lca(9)
.
所以现在的ancestor[6] = 4
. 其他还在栈中的节点ancestor[i] = i
, 比如ancestor[4] = 4
, 因为要这个节点出栈后它才会指向它的父节点.
现在在lca(9)
中
这里假设我们查询了lca(6, 9)
, 6
已经访问过了, 所以lca(6, 9) = find(6) = 4
, 即6, 9
的lca是4
, 如果查询lca(2, 9)
, 2
还在栈中, find(2) = 2
, 所以lca(2, 9) = 2
.
tarjan算法是在递归调用过程中实时记录最近的祖先, 要注意对递归的理解才能很好地理解这个算法.
之所以叫离线算法是因为我们需要知道提前查询了哪些节点的lca.
代码实现
好久没有写过java了, 感觉写得巨丑
/**
* lowest common ancestor
*/
import java.util.ArrayList;
import java.util.Scanner;
public class LCA {
int[] ancestor;
boolean[] visited;
ArrayList<ArrayList<Integer>> tree;
ArrayList<ArrayList<Integer>> query;
public LCA(int n) {
ancestor = new int[n + 1];
visited = new boolean[n + 1];
tree = new ArrayList<>(n + 1);
query = new ArrayList<>(n + 1);
for (int i = 0; i <= n; i++) {
tree.add(new ArrayList<>());
query.add(new ArrayList<>());
}
}
public void lca() {
lca(1);
}
private void lca(int cur) {
ancestor[cur] = cur;
visited[cur] = true;
for (int v : tree.get(cur)) {
lca(v);
ancestor[v] = cur;
}
for (int v : query.get(cur))
if (visited[v])//这里偷懒直接输出了
System.out.printf("lca(%d, %d) = %d\n", cur, v, find(v));
}
private int find(int v) {
while (v != ancestor[v])
v = ancestor[v];
return v;
}
public void addEgde(int u, int v) {
tree.get(u).add(v);
}
public void addQuery(int u, int v) {
query.get(u).add(v);
query.get(v).add(u);
}
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n = in.nextInt(), m = in.nextInt();//n 节点数, m查询数
LCA lca = new LCA(n);
for (int i = 0; i < n - 1; i++)//读入树
lca.addEgde(in.nextInt(), in.nextInt());
for (int i = 0; i < m; i++)
lca.addQuery(in.nextInt(), in.nextInt());
lca.lca();
}
}
/*
输入
10 5
1 2
2 4
4 6
4 7
7 9
7 10
2 5
5 8
1 3
6 9
2 9
9 10
8 9
9 3
输出
lca(9, 6) = 4
lca(9, 2) = 2
lca(10, 9) = 7
lca(8, 9) = 2
lca(2, 9) = 2
lca(3, 9) = 1
*/
RMQ在线
RMQ算法
RMQ(Range MInimum/Maximum Query), 指区间最值查询. RMQ有各种算法, 比如线段树, 树状数组. 这里我们用一种叫ST(Sparse Table)的算法. ST算法需要预处理, 时间复杂度为O(nlogn), 查询的复杂度为O(1).
定义dp[i][j]
表示i
到i + 2 ^ j - 1
的闭区间, 即[i, i + 2 ^ j - 1]
之间的最小值(最大值同理). 容易得到状态转移的公式为dp[i][j] = min(dp[i][j - 1], dp[i + 2 ^ (j - 1)][j - 1])
.
比如dp[0][3]
表示[0, 7]
之间的最小值, 它就等于[0, 3] , [4, 7]
两个区间最小值中的较小的, 即dp[0][3] = min(dp[0][2], dp[4][2])
.
预处理
预处理时用动态规划得到各个区间的最小值.
int[][] dp;
public RMQ(int[] a) {
int k = (int) (Math.log(a.length) / Math.log(2.0));
dp = new int[a.length][k + 1];
for (int i = 0; i < a.length; i++)
dp[i][0] = a[i];//[i, i]之间的最小值
for (int j = 1; j <= k; j++)
for (int i = 0; i + (1 << j) - 1 < a.length; i++)//1<<j 表示 2^j
dp[i][j] = Math.min(dp[i][j - 1], dp[i + (1 << (j - 1))][j - 1]);
}
比如对于一个[0, 8]
的区间, 第一层应该是dp[0][0]
...dp[8][0]
, 第二层就应该是dp[0][1]
...dp[7][1]
, 因为dp[8][1]
已经越界. 那么最后一层就该是dp[0][3]
和dp[1][3]
.
查询
public int query(int lo, int hi) {
int k = (int) (Math.log(hi - lo + 1) / Math.log(2.0));
return Math.min(dp[lo][k], dp[hi - (1 << k) + 1][k]);
}
比如查询[0, 8]
中[2, 7]
的最小值, k就等于2, 返回的是min(dp[2][2], dp[4][2])
, 就是区间[2, 5]
与[4][7]
中的最小值. 如果查询区间[lo, hi]
长度恰好是2的幂, 那么返回的是两个相同区间的最小值.
完整代码
dp[i][j]
中i
是区间开始的位置, 而j
表示区间长度, 长度为j ^ 2
.
class RMQ {
int[][] dp;
public RMQ(int[] a) {
int k = (int) (Math.log(a.length) / Math.log(2.0));
dp = new int[a.length][k + 1];
for (int i = 0; i < a.length; i++)
dp[i][0] = a[i];
for (int j = 1; j <= k; j++)
for (int i = 0; i + (1 << j) - 1 < a.length; i++) {
dp[i][j] = Math.min(dp[i][j - 1], dp[i + (1 << (j - 1))][j - 1]);
}
}
public int query(int lo, int hi) {
int k = (int) (Math.log(hi - lo + 1) / Math.log(2.0));
return Math.min(dp[lo][k], dp[hi - (1 << k) + 1][k]);
}
}
RMQ在线求LCA
深度优先遍历这棵树可以得到下表:
每个节点第一次被访问时的次序:
深度遍历中两个节点的最近父节点必定会在两节点被访问的中间被访问, 并且是中间被访问节点中深度最浅的那一个.
例如对节点D, F来说, 第一次访问D后会依次访问B, E, F. 而B, E中, B的深度更浅, 所以D, F 的最近公共祖先就是B.
import java.util.ArrayList;
import java.util.Scanner;
public class LCA {
private ArrayList<ArrayList<Integer>> tree;
private int[] visit;
private int[] depth;
private int[] first;
private int order;
int[][] dp;
private void rmq() {
int k = (int) (Math.log(order) / Math.log(2.0));
dp = new int[order][k + 1];
for (int i = 0; i < order; i++)
dp[i][0] = i;
for (int j = 1; j <= k; j++)
for (int i = 0; i + (1 << j) - 1 < order; i++) {
int a = dp[i][j - 1], b = dp[i + (1 << (j - 1))][j - 1];
dp[i][j] = depth[a] < depth[b] ? a : b;
}
}
private int query(int lo, int hi) {
int k = (int) (Math.log(hi - lo + 1) / Math.log(2.0));
int a = dp[lo][k], b = dp[hi - (1 << k) + 1][k];
return visit[depth[a] < depth[b] ? a : b];
}
public LCA(int n) {
order = 0;
visit = new int[n * 2 + 2];
depth = new int[n * 2 + 2];
first = new int[n + 1];
tree = new ArrayList<>(n + 1);
for (int i = 0; i <= n; i++)
tree.add(new ArrayList<>());
}
public int lcaOf(int a, int b) {
int firstA = first[a], firstB = first[b];
if (firstA > firstB) {
int t = firstA;
firstA = firstB;
firstB = firstA;
}
return query(firstA, firstB);
}
public void run() {
dfs(1, 0);
order++;
rmq();
}
public void addEgde(int u, int v) {
tree.get(u).add(v);
}
private void dfs(int cur, int dp) {
order++;
visit[order] = cur;
depth[order] = dp;
first[cur] = order;
for (int i : tree.get(cur)) {
dfs(i, dp + 1);
order++;
visit[order] = cur;
depth[order] = dp;
}
}
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int n =in.nextInt();
LCA lca = new LCA(n);
for (int i = 0; i < n-1; i++) {
lca.addEgde(in.nextInt(), in.nextInt());
}
lca.run();
while (in.hasNext()){
System.out.println(lca.lcaOf(in.nextInt(), in.nextInt()));
}
}
}
/*
7
1 2
2 4
2 5
5 6
5 7
1 3
4 6
2
4 7
2
4 5
2
4 3
1
1 3
1
2 3
1
*/
倍增算法求lac
//待更