最近被人问到这样一个问题的解决方案:在一个餐馆的预定系统中,接受用户在未来任意一段时间内的预订用餐,用户在预订的时候需要提供用餐的开始时间和结束,餐馆的餐桌是用限的,问题是,系统要在最快的时间段计算出在该用户预定的时间段内是否还有可用的餐桌?其实类似的问题我们在做系统时经常碰到,比如在一个“任务管理”系统中,我们要知道某个任务的执行时间段是否跟已知的时间段有重叠,揭开这些特定需求的外表,本质的问题可以这样描述:在一个线性的空间中,已存在很多区间段分布在该线性空间中,现给出一个指定的区间段,求出空间中所有和该区间段有重叠的空间段集合。

定义区间重叠

怎样定义“两个区间重叠”?大家都能立刻判断出这个结果,但是我们要用语言定义出来,或者用数学公式表达出来才能建立解决模型。先看下面一张图:



我们把上面的区间叫做t1,下面的区间叫做t2,根据上图可以看出,区间t2和区间t1有重叠的话,必然要满足下列三种情况之一:

  1. t2的开始时间落在t1区间段内
  2. t2的结束时间落在t1区间段内
  3. t2直接包含了整个t1区间

如果我们用数学公式表达的话,就是:

\begin{equation}
t2_{starttime} = t1_{starttime}
\end{equation}

穷举法

根据上面的公式,穷举所有区间集合中的元素,逐个计算,两两比较,返回所有满足要求的区间元素。时间计算复杂度是 \(\theta(N)\)

交集

根据上面的公式1,可以构建两个有序集合,分别存放所有区间段的开始时间和结束时间,假设两个集合分别是 S 和 E,则查询和指定区间(s,e)重叠的所有区间可以这样计算:先计算集合S中所有小于e的元素,再计算出集合E中所有大于s的元素,计算出这两个结果的交集,则为最终结果。用公式表达就是:

\begin{equation}
\{x|x \in S \land x \leq e \} \cap \{y|y \in E \land y \geq s \}
\end{equation}

在具体系统开发中,实现方式有多种。如果基于数据库,如MySQL,可以直接通过`Merge Index`利用两个索引字段。Redis中也有集合的交集运算实现`ZINTERSTORE`。这种方式从直观感觉上比穷举法好像快很多。我们可以大概计算评估下:第一步是要从两个集合中范围查找子集,采用一般的`树结构`,都能做到 $\theta(\log{N})$,第二步要做两个子集的交集运算,复杂度又回到了 $\theta(N)$。这其实和上面的穷举法感觉没有什么区别。

初识IntervalTree算法

其实各种各样的树结构,都是利用二分原理快速找到需要的数据,其复杂度都是 \(\theta(\log{N})\)级。IntervalTree也是利用这一特性,把每个区间二分对折,淘汰掉另外一半来快速找到所要区间数据。

构建

构建一个IntervalTree很简单,每次添加一个区间元素t时,先比较区间t是否覆盖x_center(x_center就是当前整个区间的中间点,从算法效率上来讲,不应该是区间起点和终点的平均值,而应该是落中这个区间内所有元素的中位值)值,如果覆盖则把区间的开始值和结束值分别存放在该节点的两个有序集合中,分别是所有覆盖区间的开始时间集合和结束时间集合。如果区间t在x_center之后,则放到右子节点上,处理方式一样(递归处理);如果区间t在x_center之前,则放到左子点上,也是递归处理。这样每个节点的数据结构大概这样:

  1. class Node(object):
  2. def __init__(self, boundary):
  3. # 区间范围
  4. self.boundary = boundary
  5. # 中间值
  6. self.x_center = (boundary[1] - boundary[0]) / 2 + boundary[0]
  7. # 左子节点,该节点下的所有区间都小于x_center
  8. self.left = None
  9. # 右子节点,该节点下的所有区间都大于x_center
  10. self.right = None
  11. # 覆盖x_center的所有节点的开始时间集合
  12. self.begins = []
  13. # 覆盖x_center的所有节点的结束时间集合
  14. self.ends = []
  15. def add_overlap_interval(self, start_point, end_point):
  16. self.begins.append(start_point)
  17. self.begins = sorted(self.begins)
  18. self.ends.append(end_point)
  19. self.ends = sorted(self.ends)

boundary参数表左该节点所能影响到整个区间范围,包含了一个起点和终点。这里简单的把x_center值取成范围的中间值。left 和 right 分别为左子节点和右子节点。begins为有序集合,里面的元素为所有满足特定条件(覆盖x_center)的区间的开始值。同begins一样,ends存放的是所有覆盖x_center的区间的结束值的有序集合。方法add_overlap_interval的作用就是添加能覆盖x_center的间到此节点中。

有了上面描述的节点定义,IntervalTree就是由上述节点组成的,即然是树结构,所以就有根节点的概念。每个IntervalTree有一个根节点。

  1. class IntervalTree(object):
  2. def __init__(self, min_point, max_point):
  3. self.min_point = min_point
  4. self.max_point = max_point
  5. self.root = Node((min_point, max_point))
  6. def add(self, start_point, end_point):
  7. node = self.root
  8. while end_point < node.x_center or start_point > node.x_center:
  9. # 如果区间没有覆盖x_center,则添加到子节点中去
  10. if end_point < node.x_center:
  11. # 添加到左子节点
  12. if not node.left:
  13. node.left = Node((node.boundary[0], node.x_center))
  14. node = node.left
  15. else:
  16. # 添加到右子节点
  17. if not node.right:
  18. node.right = Node((node.x_center, node.boundary[1]))
  19. node = node.right
  20. else:
  21. # 区间覆盖x_center,则添加到此节点
  22. node.add_overlap_interval(start_point, end_point)

查询

对于一个区间集合 S,对于给定的区间 q,现要查询出所有和区间 q 有重叠的区间子集合,怎样做呢?根据前面的区间重叠定义中说的,如果一个区间的开始时间或者结束时间落在了另外一个区间内,或者完全包含这个区间,则是重叠的。所以我们按照这个思路分别求解。

先查出所有点(无论开始时间或结束时间点)落在查询区间 q 段内的数据。这点很好做,可以把所有开始时间和结束时间放在一个排序的数据结构中(如红黑树),这样求解就转换成了在一个树中求范围数据,其复杂度是 \(\theta(\log{N})\)。

再找出那些区间完全包含了查询区间q的数据。这里有个技巧可以利用,在区间q中随便取一个点p,我们可以有如下结论推理:凡是区间能覆盖到点p的,则肯定和区间q有重叠。这个用数学公式很好推理出来。所以现在的问题就是在一个IntervalTree树中查出给定一个点的所有覆盖区间子集合。这个问题的求解和构建树结构一致。从根节点开始查询,查询此节点中所有可覆盖的区间。然后根据指定点落在左,或右子节点上来2分查找,直到没有没有子节点时退出。这里要注意一点:如果指定点刚好等于x_center点,则立即停止查找子节点,并返回当前节点所包含的所有区间数据。查找算法如下:

  1. def search_intervals(self, point):
  2.   # 从根节点开始查找
  3. node = self.root
  4. result = []
  5. while point != node.x_center:
  6. # 如果查找点没有和x_center相同
  7. if point < node.x_center:
  8. # 如果查找点在x_center前边,则该节点内所有的区间中,开始时间早于或者等于point的区间都是覆盖point的
  9. result += [s for s in node.begins if s <= point]
  10. node = node.left
  11. else:
  12. # 如果查找点在x_center后边,则该节点内所有的区间中,结束时间晚于或者等于point的区间都是覆盖point的
  13. result += [s for s in node.ends if s >= point]
  14. node = node.right
  15. if not node:
  16. break
  17. else:
  18. result += node.begins
  19. return result

至此,整个IntervalTree的大概思路表述完了。上面的代码其实更多的是讲述思路,细节没有注意,比如Node结构中begins和ends用LinkedList还是RBTree更合适。还有其它一些思考,比如区间的删除,以及具体数据业务场景中,选择什么样的x_center的取值方式使树更平衡些。留言下说你的思考,谢谢!

参考:wiki_IntervalTree

区间重叠计算及IntervalTree初识的更多相关文章

  1. 数学之路-python计算实战(5)-初识numpy以及pypy下执行numpy

    N .有用的线性代数.傅里叶变换和随机数生成函数.numpy和稀疏矩阵运算包scipy配合使用更加方便.NumPy(Numeric Python)提供了很多高级的数值编程工具,如:矩阵数据类型.矢量处 ...

  2. #419 Div2 Problem B Karen and Coffee (统计区间重叠部分 && 前缀和)

    题目链接 :http://codeforces.com/contest/816/problem/B 题意 :给出 n 表示区间个数,限定值 k 以及问询次数 q,当一个数被大于或等于 k 个区间重复覆 ...

  3. Expm 7_2区间调度问题

    [问题描述] 给定n个活动,其中的每个活动ai包含一个起始时间si与结束时间fi.设计与实现算法从n个活动中找出一个最大的相互兼容的活动子集S. 要求:分别设计动态规划与贪心算法求解该问题.其中,对贪 ...

  4. POJ 1328 Radar Installation【贪心 区间问题】

    题目链接: http://poj.org/problem?id=1328 题意: 在x轴上有若干雷达,可以覆盖距离d以内的岛屿. 给定岛屿坐标,问至少需要多少个雷达才能将岛屿全部包含. 分析: 对于每 ...

  5. 洛谷P2434 [SDOI2005]区间

    题目描述 现给定n个闭区间[ai, bi],1<=i<=n.这些区间的并可以表示为一些不相交的闭区间的并.你的任务就是在这些表示方式中找出包含最少区间的方案.你的输出应该按照区间的升序排列 ...

  6. HDU 4578 Transformation (线段树区间多种更新)

    http://acm.hdu.edu.cn/showproblem.php?pid=4578 题目大意:对于一个给定序列,序列内所有数的初始值为0,有4种操作.1:区间(x, y)内的所有数字全部加上 ...

  7. 区间dp笔记√

    区间DP是一类在区间上进行dp的最优问题,一般是根据问题设出一个表示状态的dp,可以是二维的也可以是三维的,一般情况下为二维. 然后将问题划分成两个子问题,也就是一段区间分成左右两个区间,然后将左右两 ...

  8. 洛谷 P1890 gcd区间

    P1890 gcd区间 题目提供者 洛谷OnlineJudge 标签 数论(数学相关) 难度 普及/提高- 题目描述 给定一行n个正整数a[1]..a[n]. m次询问,每次询问给定一个区间[L,R] ...

  9. 高效算法——E - 贪心-- 区间覆盖

    E - 贪心-- 区间覆盖 题目链接:http://acm.hust.edu.cn/vjudge/contest/view.action?cid=85904#problem/E 解题思路: 贪心思想, ...

随机推荐

  1. 算法系列:FFT 001

    转载自http://blog.csdn.net/orbit/article/details/17210461 2012年9月的时候,一个南京的大学生从电视台播放的一段记者采访360总裁周鸿祎的视频中破 ...

  2. ajax下载文件

    得到所有Post数据: var postData=Request.Form.ToString() 构建JS代码 // Ajax 文件下载jQuery.download = function(url, ...

  3. linux xorddos样本分析2

    逆向分析 之后我们通过ida对该样本进行更深入的分析样本的main函数中,一开始会调用函数dec_conf对样本中的大量加密的字符串进行解密,如下图所示.

  4. 图文:通过sql server 连接mysql

    1.在SQL SERVER服务器上安装MYSQL ODBC驱动; 驱动下载地址:http://dev.mysql.com/downloads/connector/odbc/ 2.安装好后,在管理工具- ...

  5. java抽象类实践

    package javaClassStudy; /** * * @author yuxg * 抽象类实践 */ public abstract class Person { private Strin ...

  6. solr5.5 基于内置jetty配置 Ubuntu

    下载地址:http://archive.apache.org/dist/lucene/solr/ 在你的目录下直接解压 tar -zxvf xxxxxx.tgz 现在就可以直接开启solr了bin/s ...

  7. Android MPAndroidChart RadarChart (蜘蛛网图)

    最近项目涉及到这个统计图形,经过实现,记录下,防止忘记了. 1.Github地址:MPAndroidChart 官方使用RadarChart demo:RadarChartActivitry 2.使用 ...

  8. linux系统ftp命令

    先来一段简单的ftp 下载脚本 ftp -i -n<<EOF open 14.2.33.211 user etl etl cd /etlfile/ftpfile lcd /etlfile/ ...

  9. linux 添加 service 服务并自动添加 chkconfig 启动级别

    下面以添加一个叫做watchcat的服务为例进行说明: 1.写一个提供给service命令使用的脚本 service 命令的使用方法一般如下 启动: $ service watchcat start ...

  10. Predicate<T>与Func<T, bool>泛型委托

    引用别人的: static void Main(string[] args) { List<string> l = new List<string>(); l.Add(&quo ...