📜  Sqrt(或平方根)分解|集合2(O(sqrt(height))时间中树的LCA)(1)

📅  最后修改于: 2023-12-03 15:20:19.937000             🧑  作者: Mango

Sqrt(或平方根)分解|集合2(O(sqrt(height))时间中树的LCA)

介绍

LCA(Lowest Common Ancestor)问题是在树中查找两个节点的最近公共祖先的问题。在一个树中,任意两个节点都有唯一的最近公共祖先。LCA问题是许多树问题的基本原型,如计算树的直径或者解决动态规划问题等。

Sqrt分解是一种常用的算法思想,通过将一个数组划分为多个块,每个块的大小为sqrt(n)或者log(n)可以在O(sqrt(n))或者O(log(n))的时间内完成各种复杂操作,如查询序列中的区间最值、快速计算区间和等等。Sqrt分解的思想可以用来解决树的LCA问题。

思路

我们将树按照深度划分为sqrt(h)个块,其中h为树的高度,每个块包含所有深度在一个区间内的节点。我们将每个块内的节点提前计算出它们的LCA,保存在一个二维数组中。我们可以在O(sqrt(h))的时间内预处理出每个块的LCA,预处理时间为O(n)。

对于查询,我们需要分为三种情况:

  • u和v在同一个块中
  • u和v在不同的块中,但是它们的LCA恰好就是块中的一个节点
  • u和v在不同的块中,且它们的LCA不是块中的一个节点

对于第一种情况,我们可以在O(sqrt(h))的时间内通过当前块内的LCA数组完成查询。

对于第二种情况,我们可以在O(sqrt(h))的时间内查询出这两个块中离它们的LCA最近的节点,然后再在它们的LCA节点内O(sqrt(h))的时间内完成查询。

对于第三种情况,我们可以在O(sqrt(h))的时间内从u开始往上跳sqrt(h)步,找到它所在的块,然后再在O(sqrt(h))的时间内一直往上跳,直到找到LCA;同时,我们需要从v往下跳sqrt(h)步,找到它所在的块,然后再在O(sqrt(h))的时间内一直往下跳,直到找到LCA。这样总共需要进行3次跳跃,每次跳跃的距离都不超过sqrt(h)。

以上操作的总时间复杂度为O(sqrt(h))。

实现

下面是使用python语言实现的代码片段:

import math

class SqrtLCA:
    def __init__(self, graph, root=1):
        self._graph = graph
        self._n = len(graph)
        self._log_n = int(math.ceil(math.log2(self._n)))
        self._depth = [0] * self._n
        self._parent = [[0] * self._log_n for _ in range(self._n)]
        self._bucket_size = int(math.sqrt(math.sqrt(self._n)))
        self._bucket_id = [-1] * self._n
        self._bucket_lca = [[-1] * self._log_n for _ in range(self._n // self._bucket_size + 1)]
        self._bucket_lca_depth = [-1] * (self._n // self._bucket_size + 1)
        self._dfs(root, -1, 0)
        for i in range(self._n):
            if self._bucket_id[i] < 0:
                self._bucket_id[i] = i // self._bucket_size
        self._preprocess()

    def _dfs(self, u, p, d):
        self._depth[u] = d
        self._parent[u][0] = p
        for k in range(1, self._log_n):
            if self._parent[u][k-1] < 0:
                self._parent[u][k] = -1
            else:
                self._parent[u][k] = self._parent[self._parent[u][k-1]][k-1]
        for v in self._graph[u]:
            if v != p:
                self._dfs(v, u, d+1)

    def _preprocess(self):
        for i in range(0, self._n, self._bucket_size):
            j = min(i + self._bucket_size, self._n) - 1
            lca = self._preprocess_bucket(i, j)
            b = i // self._bucket_size
            self._bucket_lca[b] = lca
            self._bucket_lca_depth[b] = self._depth[lca]

    def _preprocess_bucket(self, l, r):
        if l > r:
            return None
        if l == r:
            return l
        m = (l + r) // 2
        lv = self._preprocess_bucket(l, m)
        rv = self._preprocess_bucket(m+1, r)
        return self._lca(lv, rv)

    def _lca(self, u, v):
        if self._depth[u] < self._depth[v]:
            u, v = v, u
        for k in range(self._log_n-1, -1, -1):
            if self._parent[u][k] >= 0 and self._depth[self._parent[u][k]] >= self._depth[v]:
                u = self._parent[u][k]
        if u == v:
            return u
        for k in range(self._log_n-1, -1, -1):
            if self._parent[u][k] != self._parent[v][k]:
                u = self._parent[u][k]
                v = self._parent[v][k]
        return self._parent[u][0]

    def lca(self, u, v):
        if self._depth[u] < self._depth[v]:
            u, v = v, u
        if self._bucket_id[u] == self._bucket_id[v]:
            return self._bucket_lca[self._bucket_id[u]][0]
        if self._depth[self._bucket_lca[self._bucket_id[u]]] >= self._depth[self._bucket_lca[self._bucket_id[v]]]:
            return self._lca(u, self._bucket_lca[self._bucket_id[u]])
        else:
            return self._lca(self._bucket_lca[self._bucket_id[v]], v)
总结

通过上面的讲解,我们可以看到Sqrt分解的算法思想可以用来解决树的LCA问题,时间复杂度为O(sqrt(h))。在实际应用中,我们需要根据具体情况来选择分块的大小和LCA计算方法,同时还需要合理地处理边界情况和各种特殊情况。