【问题标题】:Finding overlapping intervals when overlaps are rare当重叠很少时找到重叠间隔
【发布时间】:2012-12-22 03:57:12
【问题描述】:

我有一个巨大的数据库表,其中有 n 个整数间隔(例如 {1-5}、{4-16}、{6434-114343}),我需要找出 哪个间隔相互重叠。有很多similar questions on SO,但不同的是我需要分别为每个区间返回重叠区间的集合。

      ------------------ A -------------------
    ------ B -------               ----- D -----
          --------- C --------- 

对于本例,输出为A:{B,C,D} B:{A,C} C:{A,B} D:{A}

最坏的情况是,所有区间可能相互重叠,产生大小为 O(n2) 的输出。这并不比简单的解决方案更好(比较每对间隔)。然而,在实践中,我知道我的间隔很少会与其他间隔重叠,而且当它们重叠时,最多只有 5 个其他间隔。

鉴于此信息,我应该如何解决问题? (最理想的情况是,我想要一个 SQL 查询解决方案,因为数据在数据库中,但我认为只有常规的算法解决方案是可能的。)

【问题讨论】:

  • 也许您应该在这种情况下指定“巨大”的含义。数千?百万?数十亿?如果确实存在纯 SQL 解决方案(我有疑问),您可能想告诉我们数据是如何存储在数据库中的,例如例如,您是否有单独的列用于范围名称/ID、间隔的开始和结束,或者将开始和结束存储为字符串值“x-y”。了解数值的范围也可能很有趣,例如预期的最小/最大间隔开始/结束是多少?
  • @Mecki:在这种情况下,“巨大”意味着 n=100,000。在数据库中,每个区间都有一个唯一的主键整数值、一个起始整数以及一个结束整数。数字范围从 0 到 4*10^9。

标签: algorithm intervals overlap


【解决方案1】:

针对您的问题的典型编程解决方案是在所有范围内构建一个interval tree,然后对每个间隔执行一次查找,从而为您提供O(log n) 时间中所有相交间隔的列表。下面是这样一个区间树的样例:

不过,在您的情况下,您也可以将主键存储在树节点中,因此给定以下日期(查找重叠日期是可以使用区间树解决的常见问题)

你的树看起来像这样

因此,如果我想知道哪些区间与 C 重叠,我会搜索 C 的起点 1843,然后树告诉我,该值仅在区间 C 内,这是我正在测试的区间,所以我可以忽略它。然后我搜索 C 的结尾,1907,树告诉我,它在区间 A、B 和 C 中,我可以再次忽略 C,因此我的结果集是 A 和 B。

我承认,在这种树中查找并不像人们想象的那么直观。我将在这里尽可能地解释它:您从顶部根节点开始,并在每个节点决定向左或向右,直到您遇到离开节点(一个不再有子节点的节点)。如果节点值大于您正在搜索的值,则向左走。如果节点值小于您要搜索的值,则向右走。如果您的节点值恰好等于您正在搜索的值怎么办?这取决于!如果你在寻找一个区间的开始,相等的值意味着你向右走,如果你在寻找一个区间的结束,相等的值意味着你向左走。这个非常重要。到达离开节点后,您就完成了,并且您在前往该离开节点的途中在 任何节点 中找到的所有间隔,包括存储在离开节点本身中的间隔(如果有)组成您的结果集,而不仅仅是存储在离开节点中的间隔。这意味着您必须收集在执行搜索时遇到的任何间隔。

现在回到您最初的问题:所有这些都可以在 SQL 中完成吗?是的,这是可以做到的。不过,我不确定你是否真的想这样做。您可以将当前的 SQL 表数据转换为表示区间树的 SQL 表,然后直接在该区间树表中执行查找。至少我找到了一个正是这样做的人。他试图找到涵盖给定日期的所有日期范围,而不必将该日期与数据库中的所有现有范围进行比较:

A Static Relational Interval Tree

他甚至使用了一个绝妙的技巧来优化查找的速度,显着降低两者的 CPU 使用率,构建查找表并执行实际的查找(这使得整个事情变得相当复杂)。

【讨论】:

  • 感谢您的精彩回答。我怀疑区间树会以某种方式参与解决方案,对我来说,您的解决方案似乎是正确的。在实现方面,它比 maxim1000 的解决方案更复杂,但是我不确定它们在实践中如何比较性能。就 SQL 而言,这听起来是一件非常复杂的事情!
  • @Gruber 好吧,老实说,我一直在想,SQL 中的蛮力解决方案一开始是否真的那么糟糕。您需要为这样的解决方案运行 100'000 个 SQL 查询,但是如果您只需要每天检查一次这些时间间隔(甚至更少),那真的不是问题。此外,如果您的 SQL 服务器具有“足够的能力”,它可能能够在一秒钟内执行超过 1000 个此类查询;-)
  • 暴力破解通常与我的审美观念背道而驰 :) 但在我看来,它可能是最不复杂且故障安全的方法。可能也很快,因为我知道重叠很少见。代码很少,易于理解;我只需要检查每个区间端点值即可找到交叉点:SELECT * FROM myTable where beginPoint <= @myValue and endPoint >= @myValue
  • 我已经实现了蛮力算法。根据我的输入,花了 1 个小时。我也实现了maxim1000的解决方案;虽然更复杂,但只需要大约 1 分钟。
  • @rookie 不是m*O(log n),其实是O(log m+n)(n是区间数,m是报告结果数,见tinyurl.com/pc5zmsx),不过那和@基本一样987654331@ 当您在粗略的上下文中查看它时。此外,big-O 表示法试图忽略依赖于数据 种类 的因素,big-O 试图表达关于数据 数量 的复杂性(条目数在列表/树中,而不是其中有多少可能重叠)。 O(1)O(1) 而不是 O(100),即使某些类型的数据的操作可能比其他类型的数据长 100 倍
【解决方案2】:

构建一个区间开始和结束的排序序列,然后遍历它,每次更新当前区间列表,报告任何新发现的交叉点。

类似这样的:

std::vector<TBorder> borders;
for(auto i=intervals.begin();i!=intervals.end();++i)
{
    borders.push_back(TBorder(i.Start(),Start));
    borders.push_back(TBorder(i.End(),End));
}
std::sort(borders.begin(),borders.end());
std::set<int> currentIntervals;
for(auto b=borders.begin();b!=borders.end();++b)
{
    if(b.IsEnd())
        currentIntervals.erase(b.IntervalIndex());
    else
    {
        currentIntervals.insert(b.IntervalIndex());
        if(currentIntervals.size()>1)
            ReportIntersection(currentIntervals);
    }
}

通常是 O(n*log n) (假设交叉点的数量是 O(1) )。

但是,如果您已经有按例如排序的间隔开始,可能的排序可以在 O(n) 中完成(再次假设交叉点的数量是 O(1))。

【讨论】:

  • 我已经在数据库存储过程中实现了您的解决方案,使用临时表和游标进行循环。该算法运行得非常快,n=100,000 时只需 50 秒。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-06-09
  • 2019-04-12
  • 2019-12-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多