【问题标题】:Longest common prefix in binary representation二进制表示中的最长公共前缀
【发布时间】:2022-01-08 16:47:59
【问题描述】:

我们得到了一个无向树,其节点为 N (1 to N),其根节点为节点 1。每个节点都有一个分配的值, 由数组表示 - A[i] 其中i:[1:N]

我们需要回答 Q 类型的查询: -> V X : 值V 和节点 X 的任何祖先之间的公共前缀的最长长度,包括 X , 以 62 位长度的二进制表示。

两个数字之间的共同前缀定义为:

例子:

4: 0..................0100 (62-bit binary representation)
6: 0..................0110 
Considering both as 62-bit in it's binary representation. 
Longest length of the common prefix is: 60 (as 60 left most bits are same.)

现在我们得到 N(节点数)、、节点值(A[i])和查询,我们需要在最佳时间回答每个查询。

约束:

N <= 10^5, number of nodes 
A[i] <= 10^9, value of each node
Q <= 10^5 ,number of queries
Edge[i] = (i, j) <= N

方法:

  1. 创建树并跟踪每个节点的直接父节点。
  2. 对于每个查询:[V, X],遍历每个节点 n(在从 X 到根的路径中)并将每个节点的值与 V 进行异或运算,并找到最重要的设置位每个 XOR 操作,并从所有这些操作中选择最小的一个。
  3. 所以查询的结果:[V, X]:62 -(1 + Step-2 结果)。

有没有其他有效的方法来解决这个问题?由于上述方法在最坏的情况下需要O(n^2) 时间。

【问题讨论】:

  • 你有关于树的形状的任何信息吗?
  • X和X的祖先的值有什么关系吗?
  • @user3386109,X 和 X 的祖先的值之间没有关系,它只是一个无向树,有 N 个节点(每个节点可以有 1 到 10^9 之间的任何值)有1 作为根节点。
  • 一种方法是从左侧 (MSB) 查找设置位。对于两个数字,如果它们属于 [2^(n-1), 2^n) 范围,那么它们将从 MSB 设置相同的位,因此至少公共前缀是直到该位,然后很少额外检查后续位。如果它们不属于同一范围,则返回较大数字的设置 MSB 位位置 - 1 作为公共前缀。如果你已经知道树(不是动态创建的),你可以预先计算这些东西,并在查询时快速返回结果。
  • 维护 N 位的映射和范围 [2^(n-1), 2^n) 的数字列表,并在创建树时(从输入)添加值相应的映射键列表。

标签: java c++ algorithm math data-structures


【解决方案1】:

您可以使用 fully persistent binary search trees 在 O((N+Q) log N) 时间内解决此问题。

“持久”数据结构是在修改时保留先前版本的数据结构。 “完全持久”意味着以前的版本也可以修改。通常,完全持久的数据结构被实现为purely functional data structures

你需要一个二叉搜索树。经典示例是 Okasaki's red-black trees,但您可以从任何纯函数式语言移植任何 BST 实现。

有了这种数据结构,你的问题就很容易解决了。

  1. 为仅包含根值的根创建一个单例树。
  2. 对于每个子项,通过添加子项的值从其父项创建一个新版本。
  3. 继续按 BFS 或 DFS 顺序进行操作,直到每个节点都有一个包含其所有祖先值的树版本。这将需要 O(N log N) 的空间和时间。
  4. 对于每个查询[v,x],然后,获取节点x 的树并找到最大值&lt;= x 和最小值&gt;= x。这需要 O(log N) 时间。
  5. 具有最长公共前缀的祖先将是您找到的值之一。通过使用v 对它们进行异或并选择最小的结果来检查它们。然后二进制搜索(或一些更快的 bit-hack 方法)找到最左边的位的位置。

注意:上述讨论假设您说“我们需要在最佳时间回答每个查询”时是认真的。

如果您可以无序处理查询,那么您就不需要持久树。您可以只使用您在语言库中找到的单个常规 BST,因为您不需要同时使用所有树。

按顺序浏览图表,在找到每个节点时调整树,然后处理专门针对该节点的所有查询。

【讨论】:

  • 到目前为止,我对持久性 BST 知之甚少,尽管实际上并没有在任何地方提到它以在线方式回答查询,所以我想我们可以按任何顺序回答它,然后在返回 result[] 数组时重新排序。
  • 好的,所以对于查询(V,X),我们需要在我们正在创建的节点的 BST 中找到最大值 = V使用前序遍历。
  • 是的。当你进入一个节点时,你将它的值添加到树中,当你离开一个节点时,你删除它的值。
【解决方案2】:

Java:使用numberOfLeadingZeros。 Long 和 Integer 类有一组很好的实用函数。

long commonPrefix(long x, long y) {
    return Long.numberOfLeadingZeros(x ^ y);
}

关于任何算法改进:不诚实 在这里提供一个极好的解决方案。而且更好 你用笔和纸做一些事情,数学方面令人费解。 事实上,我可以看到一个微小的方式。也许你可以做得更好。

【讨论】:

  • 我试图解决它,但不知道如何使用数学来降低这里的时间复杂度,您能否描述一下您的方法或者提示如何继续。
  • 对不起,我根本没时间。我也需要对其进行编程;只讲不如教坏。考虑具有 2-4-8-16 子树的数字中的一般位模式,依此类推。如果您以二进制数打印,您将看到该模式并可能会利用它。但首先要制定一个缓慢的解决方案。
【解决方案3】:

我想到了一个可能有用的方法。您可以通过为 MSB 位集添加附加值来修改节点中的数据。例如,如果数字为 8,则 MSB 位设置,考虑到 MSB 在左侧,是 62 - 4 = 从左侧算起的第 58 位。此外,您可以将位存储在 bitset 中。因此,您的树节点结构可能会被修改为(在 C++ 中):

struct TreeNode
{
    int node_value;
    int set_msb_bit_from_left;
    bitset<62> b;
    TreeNode* left;
    TreeNode* right;
};

要点:

  1. 如果两个数字 x 和 y 在相同的范围 [2^(n-1), 2^n) 内,则它们具有相同的 set_msb_bit_from_left。因此可以将这些数字添加到一组中,例如Gi,其中iset_msb_bit_from_left 的值。
  2. 如果x属于Gi,y属于Gi+1,则最长公共前缀为i
  3. 如果x属于Gi,y属于Gji &lt; j,那么最长公共前缀是i
  4. 如果 x 和 y 都属于同一个组,则需要从 MSB 到 LSB 进行逐位检查,直到发现不匹配,或者使用一些其他有效的位操作技巧,如 XOR 操作。

算法:

  1. 在创建树的节点期间,为node_value 计算set_msb_bit_from_left
  2. 根据msb_bit_from_left创建一个用于存储组的数据结构。此数据结构可以是简单的映射或不相交的集合并集。
  3. 对于给定的两个节点 V(第 n 级子节点)和 X(祖先),将所有节点从 X 到 V 放入一个集合中,并根据增加的 set_msb_bit_from_left 或组 ID 进行比较。
  4. 从集合中的最后一个(最高组 id)到第一个元素(最低组 id)并计算 x 和 y 之间的最长公共前缀:
  • 获取 x 和 y 的组。
  • 如果相同,则从 MSB 处理到 LSB,直到发现不匹配或对属于同一组的所有内容进行异或并返回输出。
  • 否则,返回min(grp_id_x, grp_id_y) - 1,其中grp_id_xgrp_id_y 是各自的set_msb_bit_from_left 值。 -1 因为我们不想计算设置的 MSB 位。

注意:如果最后有两个或多个元素在同一个组中,那么你只需要找到它们之间的最长公共前缀(不需要处理来自其他组的元素,最长公共前缀会更小)。

【讨论】:

    【解决方案4】:

    求两个数字的二进制表示的长度,如果不相同则不能有任何匹配位,因此只有前导零是公共前缀。否则我们可以通过异或值来找到匹配的前缀位数。

    这是一个巧妙的技巧,因为 XOR 操作会将所有匹配位变为零,直到最左边不匹配的位。因此,公共前缀的长度将是任一数字的二进制表示的总长度(因为它们的长度相同)与 XOR 值的二进制表示的长度之间的差。

    最后,由于您希望它为 62 位,因此您添加了前导零,前导零的数量是 62 减去任一数字的二进制表示的长度。

    所以, 62 - 任一数字的长度 + 公共前缀。 进一步简化意味着答案是 62 - xor 值的长度;

    这里唯一棘手的情况是,如果数字相同,您可以预先处理。

    int commonPrefix(int x, int y)
        if(x == y)
            return 62;
    
        int xLength = Math.floor(Math.log(x) + 1);
        int yLength = Math.floor(Math.log(x) + 1);
        
        if(xLength != yLength)
            return 62 - Math.max(xLength, yLength);
    
        int xor = x ^ y;
        int xorLength = Math.floor(Math.log(xor) + 1);
    
        return 62 - xorLength;
    

    一旦您了解了这一点,一个简单的 DFS 应该会为您提供您正在寻找的节点的祖先,并且您可以通过将给定值与每个祖先进行比较来找到最长的公共前缀。

    【讨论】:

    • 这种方法在最坏的情况下是O(n^2),我在这里寻找更有效的方法,因为蛮力很慢。
    猜你喜欢
    • 2021-09-11
    • 1970-01-01
    • 2022-11-22
    • 2021-10-12
    • 1970-01-01
    • 1970-01-01
    • 2020-07-05
    • 2018-09-30
    • 2013-04-14
    相关资源
    最近更新 更多