LCA 最近公共祖先


flyhite - minami
美波美波,没有你我可怎么活啊


问题描述


对于有根树 T 的两个结点 u、v,最近公共祖先 LCA(T, u, v) 表示一个结点x,满足 xuv 的祖先且 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。

我们将第一次访问时的记录标记出来。

{ 73132354537678987 }

不难发现在任意两个标记括起的子序列中包含着它们的 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),不难发现其结果为由ij为端点组成的连续欧拉序子序列中,深度最小的节点。

基于这一点,我们可以将 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)>

虽然能在线性规模的时间内完成初始化,能在常数意义下完成单次查询,但该算法不论时间空间常数都极大,不推荐使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值