【问题标题】:A range intersection algorithm better than O(n)?比 O(n) 更好的范围相交算法?
【发布时间】:2010-09-23 03:32:02
【问题描述】:

范围相交是一个简单但不平凡的问题。

已经回答过两次了:

第一个解决方案是 O(n),第二个解决方案是针对数据库(当然小于 O(n))。

我也有同样的问题,但是对于很大的 n 并且我不在数据库中。

这个问题似乎与Store 2D points for quick retrieval of those inside a rectangle 非常相似,但我看不出它是如何映射的。

那么,您会将范围集存储在哪种数据结构中,以使搜索范围的成本低于 O(n)? (使用可用于 Java 的库的额外功劳)

编辑:

我想获取所有相交范围的子集,这意味着搜索范围可以与多个范围相交。

Java中需要小于O(n)的方法是:

public class RangeSet {
    ....
    public Set<Range> intersects(Range range);
    ....
}

其中 Range 只是一个包含一对 int start 和 end 的类。

这不是一个不可能的问题,我已经有了解决方案,我只是想看看是否有更标准/更简单的方法来做到这一点

【问题讨论】:

  • 您要查找列表中的所有相交范围吗?还是只检查一个范围与范围列表的交叉点?
  • 你真的需要识别交叉口,还是仅仅检测它们?如果您需要识别所有交叉点,则无法击败 O(n),因为集合中的 所有 范围在最坏的情况下可能与给定查询相交。
  • 你如何解决小于 O(n) 但可能返回包含 n 个范围的集合?
  • 如果没有更好的方法,我会及时发布
  • Andrew,使用正确的数据结构,您不必返回范围集,而是返回范围的范围。例如。在我下面的算法中,当你假设你有排序的范围时,你可以获得在 O(log n)

标签: java algorithm search big-o interval-intersection


【解决方案1】:

我刚刚发现了嵌套包含列表source、implementation,据说它的构建和查询速度比区间树快一个数量级,并且内存消耗更少。

【讨论】:

    【解决方案2】:

    标准方法是使用interval tree。

    在计算机科学中,区间树是一种保存区间的树数据结构。具体来说,它允许人们有效地找到与任何给定区间或点重叠的所有区间。它通常用于窗口查询,例如,在矩形视口内查找计算机地图上的所有道路,或查找 3D 场景内的所有可见元素。类似的数据结构是段树。

    简单的解决方案是访问每个区间并测试它是否与给定的点或区间相交,这需要 O(n) 时间,其中 n 是集合中的区间数。由于查询可能返回所有区间,例如,如果查询是与集合中所有区间相交的大区间,则这是渐近最优的;然而,我们可以通过考虑输出敏感算法做得更好,其中运行时间用 m 表示,m 是查询产生的间隔数。区间树的查询时间为 O(log n + m),初始创建时间为 O(n log n),同时将内存消耗限制为 O(n)。创建后,区间树可能是动态的,允许在 O(log n) 中有效地插入和删除一个区间。如果区间的端点在一个小的整数范围内(例如,在 [1,...,O(n)] 范围内),则存在更快的数据结构[1],预处理时间为 O(n),查询时间为 O( 1+m) 用于报告包含给定查询点的 m 个区间。

    【讨论】:

      【解决方案3】:

      如果范围重叠,并且想要检索 所有与给定目标范围重叠(或包含)的范围,则上述大多数解决方案似乎都不起作用。

      正如一些人指出的那样,如果(最坏情况)所有范围恰好与目标范围相交(例如,如果目标范围是 {0..MAXINT} 或类似的),那么当然返回 n 个范围需要 O(n)。

      但不是有趣且典型/平均的情况,即 n 个总范围中只有很小 % 与目标范围相交吗?将 do 与“m”相交的数字称为“m”——在这种情况下,可以想象你可以做得和 O(m) 一样好。如果 n=10^9 和 m=10,这就是成败的区别。

      考虑一个文本文档的简单案例,该文档具有针对其“类型”标记的多个区域——也许您想要找到包含或与给定的连续文本范围相交的所有标记单元(例如,段落)。在 HTML、XML 或类似内容中,它们只能是包含目标范围内至少一些字符的文本节点的祖先。在每个节点中具有父指针的典型表示中,这是 O(m) - 比 O(n) 好得多,尤其是因为 m(对于短或同步目标范围)仅仅是树嵌套深度,它往往甚至低于ln(n) 因为大型 XML 文档在实践中会变得更复杂而不是更深。

      有趣的情况更难:如果您的“元素”不像 XML 那样形成树,但可以像 MECS、CLIX、LMNL 和其他一些系统那样重叠,该怎么办?您仍然希望找到与您的目标重叠的所有区域/“元素”,但它们并不那么容易组织。

      另一方面,您应该能够做得很好,因为在许多应用程序中标记的范围通常都很小——一本书中的单词、句子和段落远多于章节。因此,即使可能有大量范围从目标之前开始,也有大量范围在目标之后结束,但平均而言,交叉点会非常小。

      我认为这就是最初的提问者的意思,恐怕我没有看到解决该问题的答案。如果不是原来的问题,那么我想提出一个新问题。

      【讨论】:

        【解决方案4】:

        编辑: 听起来这个解决方案或多或少是an Interval Tree。更完整的区间树实现可以在here找到。

        class TreeNode
        {
        public:
            long pivot;
            List<Range> leaves;  //Any ranges that intersect the pivot
            TreeNode left;        //Tree nodes that fall to the left of the pivot
            TreeNode right;       //Tree nodes that fall to the right of the pivot
        };
        

        准备 O(n log n):

        1. 创建范围列表
        2. 选择轴心点(可能通过使用结束日期的排序列表。)??
        3. 构建你的树。

        搜索:

        1. 使用二分搜索查找 >= TestRange.End 的第一个主元
        2. 遍历树直到枢轴> TestRange.Start

          2a。将叶子添加到您的结果中。


        例子:

        范围:

        • 0 - 2
        • 1 - 2
        • 2 - 3
        • 1 - 4
        • 2 - 4
        • 0 - 5
        • 4 - 5
        • 2 - 6
        • 3 - 7

        树:

                                     4
                       --------------+------------------
                       3             |                 7
                       |            1-4                |
                       |            2-4                |
                       |            0-5                |
                       |            4-5                |
              ---------+------                 --------+--------
              2        |    null              6        |       null
         -----+----   2-3                 ----+----   3-7
        null  |  null                   null  |  null    
             0-2                             2-6
             1-2
        

        【讨论】:

        • 图中可能有错误:我认为2-6和3-7的范围实际上应该在4以下的列表中,因为4在这些范围内。子节点应仅包含完全位于父枢轴左侧或完全右侧的范围。
        • 您知道@itowlson 实际上是对的。区间树按照他的描述工作,因此这两个范围应该属于枢轴4。您的树无效。
        【解决方案5】:

        当我遇到这个问题时,我使用了一个排序的范围数组和一个二分搜索来寻找交叉点。这是(我相信)O(log n) 性能,处理重叠范围需要一点开销。

        我认为,您的问题的答案可以从下面的代码中推导出来,但没有插入。我展示了整个代码以避免因不同的上下文而造成混淆 - 我需要将一系列 Unicode 代码点插入到代码点范围列表中。

        -- 编辑--

        调整下面的代码以确定多个范围的交集涉及从插入点开始的简单前向搜索,直到找到不再相交的范围。

        -- 结束编辑--

        Range 类包含:

        final int                               lower;                                  // lower end of range
        final int                               upper;                                  // upper end of range
        
        public int compareTo(Object obj) {
            if(obj==null) { return -1; }
        
            Range                           oth=(Range)obj;
        
            if(lower<oth.lower) { return -1; }
            if(lower>oth.lower) { return  1; }
            if(upper<oth.upper) { return -1; }
            if(upper>oth.upper) { return  1; }
            return 0;
            }
        

        范围插入:

        public Builder addRange(int fir, int las) {
            if(fir!=-1) { fir&=0x001FFFFF; }
            if(las!=-1) { las&=0x001FFFFF; }
        
            if(codepoints==null || codepoints.length==0) {
                codepoints=new Range[]{new Range(fir,las)};
                }
            else {
                int                         idx=Range.findChar(codepoints,fir);
                int                         ins=(idx<0 ? -(idx+1) : idx);
        
                if(idx<0) {
                    if     (ins>0                 && fir==(codepoints[ins-1].upper+1)) { idx=(ins-1); }  // new range adjoins the following range (can't overlap or idx would be >=0)
                    else if(ins<codepoints.length && las>=(codepoints[ins  ].lower-1)) { idx=ins;     }  // new range overlaps or adjoins the following range
                    }
        
                if(idx<0) {
                    codepoints=(Range[])Util.arrayInsert(codepoints,ins,new Range(fir,las));
                    }
                else {
                    boolean                 rmv=false;
        
                    for(int xa=(idx+1); xa<codepoints.length && codepoints[xa].lower<=las; xa++) {
                        if(las<codepoints[xa].upper) { las=codepoints[xa].upper; }
                        codepoints[xa]=null;
                        rmv=true;
                        }
                    if(codepoints[idx].lower>fir || codepoints[idx].upper<las) {
                        codepoints[idx]=new Range((codepoints[idx].lower < fir ? codepoints[idx].lower : fir),(codepoints[idx].upper>las ? codepoints[idx].upper : las));
                        }
                    if(rmv) { codepoints=Range.removeNulls(codepoints); }
                    }
                }
            return this;
            }
        

        二分搜索:

        static int findChar(Range[] arr, int val) {
            if(arr.length==1) {
                if     (val< arr[0].lower) { return -1; }                             // value too low
                else if(val<=arr[0].upper) { return  0; }                             // value found
                else                       { return -2; }                             // value too high
                }
            else {
                int                             lowidx=0;                               // low index
                int                             hghidx=(arr.length-1);                  // high index
                int                             mididx;                                 // middle index
                Range                           midval;                                 // middle value
        
                while(lowidx<=hghidx) {
                    mididx=((lowidx+hghidx)>>>1);
                    midval=arr[mididx];
                    if     (val< midval.lower) { hghidx=(mididx-1); }                   // value too low
                    else if(val<=midval.upper) { return mididx;     }                   // value found
                    else                       { lowidx=(mididx+1); }                   // value too high
                    }
                return -(lowidx+1);                                                     // value not found.
                }
            }
        

        【讨论】:

        • 我认为您的问题只有 1 个相交范围,我想要所有相交范围的子集。我更新了问题以反映这一点。
        • 是的,因为我将相交的范围折叠在一起以创建一个更大的范围;但是对于多个范围,从命中向后和向前进行简单的线性搜索将找到相邻的多个范围。
        【解决方案6】:

        非重叠范围:

        准备 O(n log n):

        1. 制作范围的数组/向量。
        2. 按范围的结尾对向量进行排序(通过按范围的开头排序来打破平局)

        搜索:

        1. 使用二分搜索查找 End 值为 >= TestRange.Start 的第一个范围
        2. 迭代器从二分查找开始,直到找到 Start > TestRange.End:

          2a。如果当前范围在 TestRange 范围内,则将其添加到您的结果中。

        【讨论】:

        • 我想你明白了,它很简单。
        • 这比我的解决方案好。
        • 这行不通,因为范围可能有非常不同的长度。一个短的可能落在查询之外并停止迭代器,而下一个长的(按结束坐标排序)仍可能落在查询内部,因此会被遗漏。
        • 等等,错过了话题。对于非重叠范围,这当然可以。
        • 但迭代阶段仍然是 O(n),因为在最坏的情况下,您的查询会与每个范围相交,因此您需要遍历所有范围。
        【解决方案7】:

        重叠范围:

        准备 O(n log n):

        1. 制作范围的数组/向量。
        2. 按范围的结尾对向量进行排序(通过按范围的开头排序来打破平局)
        3. 制作第二个整数向量。这表示您可以停止搜索的点。

          int stop[size];
          stop[size-1] = Ranges[size - 1].start;
          for (int i = size - 2; i >= 0; i--)
          {
              stop[i] = min(Ranges[i].start, stop[i+1]);
          }
          

        搜索:

        1. 使用二分搜索查找 End 值为 >= TestRange.Start 的第一个范围
        2. 迭代器从二分查找开始直到 stop[i] > TestRange.End:

          2a。如果当前范围在 TestRange 范围内,则将其添加到您的结果中。

        【讨论】:

          【解决方案8】:

          就像四叉树适用于一组二维点一样,简单的二叉树也适用于这种情况。用你的范围构建一棵树。

          进一步解释: 树中的每个节点都包含两个整数,范围的开始和结束,如果它不是叶节点,则包含两个子节点。 要查找输入范围跨越的范围,然后从树的顶部开始

            - if the node range intersects the input range:
               - if it's a leaf node, then add the range to your result list
               - if it's not a leaf node, then traverse down to the child nodes and repeat this process.
          

          应该是 O(logN)

          更多细节: 二叉树的结构类似于四叉树的一维版本。每个节点将有三个整数(对不起,我在上面说了两个,但现在我意识到你需要三个),最低的代表低于此节点的最低范围的最小值,最高的代表低于此的最高范围的最大值节点和枢轴。左孩子将从这个节点的最低点跨越到它的枢轴。右子节点将从该节点的枢轴跨越到该节点的最高节点。如果只有一个范围从“最低”到“最高”,那么您将没有枢轴,这将是一片叶子。理想情况下,您会为每个节点选择枢轴以保持树的平衡。

          【讨论】:

          • 每个范围都有 2 个维度。我不明白二叉树是如何工作的。
          • 感谢您添加更多详细信息,我不明白您的树的结构。二叉树中的父子关系是什么?
          【解决方案9】:

          这取决于您的确切问题,在链接的问题中,不同的范围,没有共同部分,并且搜索的范围可能跨越多个范围。如果你的问题是一样的,那真的很简单: 取一个范围数组,按它们的最小值对它们进行排序(因为它们不重叠,这也将与按它们的最大值排序的顺序相同)。

          现在只需对您的目标下限值(如果不准确则更小)和目标上限值(如果不准确则更大)进行 binsearch。结果索引是涵盖的范围。您必须检查索引本身的范围是否包含在内或排除在外,但这只是 2 次检查。总体复杂度 O(log n)。

          【讨论】:

          • O(log(n)) 仅当集合已经排序,否则它的排序 O(nlog(n))
          • 您完全正确,但从问题来看,范围集似乎不会有太大变化,因此只需执行一次。
          • 是的,您可以说范围集是一种数据类型,因此它按上下值排序
          【解决方案10】:

          听起来您需要一个实现 SortedSet 接口的类。 TreeSet 是核心 API 附带的实现。

          有一组按最小值排序,一组按最大值排序。

          然后您可以使用内存中的集合实现数据库算法的等效项。

          至于这是否真的比O(n)快,我不能说。

          【讨论】:

          • 我得出了同样的结论,但我想看看是否有更好的方法。这个解决方案要么是 O(log(n)) 要么是 O(log^2(n))。我很确定找到两个子集之间的交集要花多少钱。
          猜你喜欢
          • 1970-01-01
          • 2023-03-25
          • 1970-01-01
          • 1970-01-01
          • 2019-10-23
          • 1970-01-01
          • 2019-05-25
          • 2023-04-10
          • 2021-09-25
          相关资源
          最近更新 更多