【问题标题】:Lowest Common Ancestor implementations - what's the difference?Lowest Common Ancestor 实现 - 有什么区别?
【发布时间】:2011-12-03 13:35:22
【问题描述】:

我一直在阅读有关 the Lowest Common Ancestor algorithm on top coder 的文章,但我不明白为什么涉及 RMQ 算法 - 那里列出的解决方案非常复杂,并且具有以下属性:

  • O(sqrt(n)) 搜索时间复杂度,O(n) 预计算时间复杂度
  • 存储每个节点的父节点的空间复杂度为 O(n)
  • O(n) 空间复杂度,用于存储每个节点的预计算

我的解决方案:给定 2 个整数值,通过简单的前序遍历找到节点。取其中一个节点并上树并将路径存储到一个集合中。取另一个节点并沿着树向上走,并在我向上时检查每个节点:如果该节点在 Set 中,则停止并返回 LCA。 Full implementation.

  • 在给定值的情况下,查找 2 个节点中的每一个的 O(n) 时间复杂度(因为它是常规树,而不是 BST -
  • 将路径存储到集合中的空间复杂度为 O(log n)
  • 使用第二个节点上树的 O(log n) 时间复杂度

那么鉴于这两个选择,Top Coder 上的算法是否更好,如果是,为什么?这是我无法理解的。我认为 O(log n) 比 O(sqrt(n)) 好。

public class LCA {

    private class Node {

        int data;
        Node[] children = new Node[0];
        Node parent;

        public Node() {
        }

        public Node(int v) {
            data = v;
        }

        @Override
        public boolean equals(Object other) {
            if (this.data == ((Node) other).data) {
                return true;
            }
            return false;
        }
    }
    private Node root;

    public LCA() {
        root = new Node(3);

        root.children = new Node[4];
        root.children[0] = new Node(15);
        root.children[0].parent = root;
        root.children[1] = new Node(40);
        root.children[1].parent = root;
        root.children[2] = new Node(100);
        root.children[2].parent = root;
        root.children[3] = new Node(10);
        root.children[3].parent = root;

        root.children[0].children = new Node[3];
        root.children[0].children[0] = new Node(22);
        root.children[0].children[0].parent = root.children[0];
        root.children[0].children[1] = new Node(11);
        root.children[0].children[1].parent = root.children[0];
        root.children[0].children[2] = new Node(99);
        root.children[0].children[2].parent = root.children[0];

        root.children[2].children = new Node[2];
        root.children[2].children[0] = new Node(120);
        root.children[2].children[0].parent = root.children[2];
        root.children[2].children[1] = new Node(33);
        root.children[2].children[1].parent = root.children[2];

        root.children[3].children = new Node[4];
        root.children[3].children[0] = new Node(51);
        root.children[3].children[0].parent = root.children[3];
        root.children[3].children[1] = new Node(52);
        root.children[3].children[1].parent = root.children[3];
        root.children[3].children[2] = new Node(53);
        root.children[3].children[2].parent = root.children[3];
        root.children[3].children[3] = new Node(54);
        root.children[3].children[3].parent = root.children[3];

        root.children[3].children[0].children = new Node[2];
        root.children[3].children[0].children[0] = new Node(25);
        root.children[3].children[0].children[0].parent = root.children[3].children[0];
        root.children[3].children[0].children[1] = new Node(26);
        root.children[3].children[0].children[1].parent = root.children[3].children[0];

        root.children[3].children[3].children = new Node[1];
        root.children[3].children[3].children[0] = new Node(27);
        root.children[3].children[3].children[0].parent = root.children[3].children[3];
    }

    private Node findNode(Node root, int value) {
        if (root == null) {
            return null;
        }
        if (root.data == value) {
            return root;
        }
        for (int i = 0; i < root.children.length; i++) {
            Node found = findNode(root.children[i], value);
            if (found != null) {
                return found;
            }
        }
        return null;
    }

    public void LCA(int node1, int node2) {
        Node n1 = findNode(root, node1);
        Node n2 = findNode(root, node2);
        Set<Node> ancestors = new HashSet<Node>();
        while (n1 != null) {
            ancestors.add(n1);
            n1 = n1.parent;
        }
        while (n2 != null) {
            if (ancestors.contains(n2)) {
                System.out.println("Found common ancestor between " + node1 + " and " + node2 + ": node " + n2.data);
                return;
            }
            n2 = n2.parent;
        }
    }

    public static void main(String[] args) {
        LCA tree = new LCA();
        tree.LCA(33, 27);
    }
}

【问题讨论】:

  • 是的,或者对于不需要额外存储的实现,只需从一个节点到另一个节点交替遍历树。
  • @Martin:对于任何 e>0,O(log(n)) 都比 O(n^e) 好。
  • 仅供参考,也有比这两种算法都快的算法,具有 O(n) 预处理和 O(1) 查找(例如 Schieber Vishkin 1988)

标签: algorithm tree ancestor


【解决方案1】:

LCA 算法适用于任何树(不一定是二叉树,也不一定是平衡的)。您的“简单算法”分析失败了,因为跟踪到根节点的路径实际上是 O(N) 时间和空间而不是 O(log N)

【讨论】:

    【解决方案2】:

    只是想指出问题在于有根树而不是二叉搜索树。所以,在你的算法中

    在给定值的情况下,查找 2 个节点中的每一个的 O(n) 时间复杂度 将路径存储到集合中的 O(n) 空间复杂度 O(sqrt(n)) 时间复杂度,用第二个节点向上树并在第一个 n 存储元素中搜索。

    当我们从第二个节点上升时检查每个节点需要 O(n),因此对于 n 个节点将需要 O(sqrt(n))。

    【讨论】:

    • 我认为你在树中找到节点是正确的......因为它是一棵常规树而不是 BST,所以查找时间将是 O(n) 最坏的情况。这使得它等于那里描述的算法。至于另一部分:当我们从第二个节点上升时检查每个节点,not 需要 O(n),因为我们有一个到父节点的链接。所以总体来说是 O(log n)
    • @user361676 如 missingno 所述,将路径追踪回根需要 O(n) 时间和空间复杂度。
    【解决方案3】:

    Harel 和 Tarjan LCA 算法(您提供的链接中的参考)使用复杂度为 O(n) 的预计算,之后查找为 O(1)(不是您声称的 O(sqrt(n)) .

    【讨论】:

    • 它说解决方案是“一个 解决方案”。我错过了什么吗?
    • epubs.siam.org/sicomp/resource/1/smjcat/v13/i2/… 转到您为“受限” RMQ 算法引用的页面底部,该算法与 LCA 具有相同的复杂性。 "限制 RMQ 的一个 算法"
    猜你喜欢
    • 2023-02-08
    • 1970-01-01
    • 2016-02-18
    • 2016-12-19
    • 2013-05-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-01-06
    相关资源
    最近更新 更多