如何检查重叠区间
How to check for overlapping intervals

原始链接: https://zayenz.se/blog/post/how-to-check-for-overlapping-intervals/

## 处理区间与重叠检测 本文探讨了确定区间重叠的有效方法——两个点之间的范围,适用于时间、数字或日期。一个关键原则是,识别*非重叠*通常比直接检测重叠更简单。区间通常表示为对,如[开始,结束],经常使用半开表示法(结束值不包含在内)。 最初的重叠检测方法涉及分析所有可能的重叠场景,导致复杂的布尔条件。然而,通过关注*非重叠*可以显著简化这一点:如果一个区间完全在另一个区间之前,则两个区间不重叠。这转化为一个简洁的条件:`self.start < other.end and other.start < self.end`。 这种“反向思考”技术扩展到二维场景,如盒子。虽然对盒子重叠进行完整案例分析会产生 16 种可能性,但检查*非重叠*(盒子完全在左边、右边、上面或下面)要简单得多。由此产生的重叠条件与区间情况相同——如果两个维度都存在重叠,则存在重叠。 最终,利用否定和几何直觉可以产生更简洁、更易于理解的重叠检测代码,避免过于复杂的逐案分析。

## 重叠区间检查:一则黑客新闻讨论总结 一则黑客新闻讨论串探讨了检查重叠区间的方法,源于zayenz.se的一篇文章。对话强调了这个看似简单的任务的复杂性,超越了基本的重叠检测。 关键点包括:将概念扩展到多个维度(使用AND逻辑处理轴对齐边界框,常见于游戏碰撞检测),以及排序算法的效率(对于动态对象使用冒泡排序,对于静态集合使用R-Tree等技术实现O(N log N))。 许多评论者指出明确定义区间类型(闭区间 vs. 开区间)和处理边缘情况的重要性,尤其是在处理浮点数时。 讨论还延伸到相关问题,例如查找连续区间、合并区间,以及使用诸如区间树和范围树等数据结构。分享了诸如艾伦区间代数、区域连接演算以及现有代码示例(包括Stack Overflow和GitHub链接)等资源。最终,该讨论强调需要强大的测试——单元测试和属性测试——以确保实现的正确性。
相关文章

原文

Working with intervals is a common task in programming, whether you’re dealing with time ranges, scheduling problems, or geometric computations. A key insight when working with intervals is that checking for the absence of an overlap is often much simpler than checking for all the ways an overlap can occur.

This post will first cover how to represent intervals in code, then dive into the logic for detecting overlaps.

An interval represents a range between two points, and could be in a continuous or discrete domain. A common way to write an interval is a pair of values [start, end] where start ≤ end. This would be a so-called closed interval, where the end-value is included in the interval. An alternative is [start, end), which denotes a half-open interval where the end value is not included in the interval.1 Half-open intervals are very common in programming languages.

Some examples of intervals are

  • Time intervals: [9:00, 17:00] (a work day)
  • Numeric ranges: [1, 10) (the digit 1, 2, 3, …, 9)
  • Date ranges: [2025-01-01, 2025-12-31] (all the days in the year 2025)
  • Temperature range: [20°C, 25°C]

We will use Python as an example language, and will use plain integers as the underlying type. All the examples will use open intervals, as that is very common.

"""Interval from start to end (exclusive)"""

For the purposes of this post, we will only consider integer valued intervals for simplicity. In addition we require that intervals are non-empty. In many real cases, real valued intervals approximated by floating points are needed as is the handling of empty intervals.

One of the most common questions when working with intervals is: “Do these two intervals overlap?” Lets implement a method for this

def overlaps(self, other: Interval) -> bool:

""" Return true iff the two intervals overlap. """

The straight-forward approach#

Whenever one needs to make a Boolean condition, the easiest way is often to consider all the cases. So, let’s consider all the ways that two intervals may overlap. There are four distinct cases.

Case 1: self starts first, other overlaps end of self

Case 2: other starts first, self overlaps end of other

Case 3: self completely contains other

Case 4: other completely contains self

In all these cases, we can see that the intervals share at least one point in common. The challenge is to write a condition that captures all four cases correctly. Let’s translate it into code straight from the case analysis.

def overlaps(self, other: Interval) -> bool:

""" Return true iff the two intervals overlap. """

self.start <= other.start < self.end or

self.start < other.end <= self.end or

self.start < other.end <= other.end < self.end or

other.start <= self.start <= self.end < other.end

This is a straightforward translation of the cases, where the most tricky thing is to make sure that the handling of the half-open property is correct. One thing to note though, is that by checking the endpoints of the blue other interval in Case 1 and 2, then we have also covered Case 3. This means that due to short circuiting, we will never trigger Case 3. In addition Case 4 is overly specific. We only need to care about checking if the start-point of the green self interval is in the blue other interval.

Taking these observations into account, we can simplify the above condition to the following cases.

def overlaps(self, other: Interval) -> bool:

""" Return true iff the two intervals overlap. """

self.start <= other.start < self.end or

self.start < other.end <= self.end or

other.start <= self.start < other.end

With a case-analysis and some careful considerations, we have a nice expression that captures overlapping intervals. But checking overlap feels like something that should be obvious, and this condition is not that obvious. Checking for overlap of two intervals also feels like it shouldn’t reduce down to three cases. From that argument, we can intuit that it should be possible to simplify this even more.

Can we make a simpler condition, that is also easy to find?

Flipping the script#

The core insight is that sometimes it is much easier to check for the negation of a property than the original property. For overlap, the negation is checking if two intervals are not overlapping. Let’s look at the cases we have now instead.

Case 1: self is before other

Case 2: other is before self

There are only two ways the two intervals are not overlapping, and this is much easier to translate into code.

def overlaps(self, other: Interval) -> bool:

""" Return true iff the two intervals overlap. """

Using De Morgan’s law (¬(AB)¬A¬B)\left( \lnot (A \lor B) \large\vdash \lnot A \land \lnot B \right)

def overlaps(self, other: Interval) -> bool:

""" Return true iff the two intervals overlap. """

This is finally a nice and simple expression that checks just two properties. By changing our viewpoint from checking for an overlap to checking against one, we deduced the correct expression much more easily.

Understanding the new condition#

The new condition works, and we could derive it from a case analysis and using Boolean simplification. But there is also a meaning to the expression that we can interpret geometrically.

Case 1: start of other is before end of self

Case 2: start of self is before end of other

The first case checks that it is not the case that other starts after self ends. The second case checks that it is not the case that self starts after other ends. These two conditions together gives us the result that there is some part that is overlapping.

It is of course possible to figure out this condition directly, but making this intuitive leap is in my view harder than following the case analysis from the negative case.2

An interval is a one-dimensional property. A very natural extension is to instead consider two-dimensional properties. Representing a box is very similar to an interval, here we go from top and left (inclusive) to bottom and right (exclusive).

"""Box from (left, top) to (right, bottom) (exclusive)"""

Case analysis for overlap#

Checking for overlap is equally important for boxes as it is for intervals. Let’s start by making a full case analysis of all the variants for how two boxes can overlap.

Case 1: other left+below, ends inside

Case 2: other left, ends inside; contained vertically

Case 3: other left+above, ends inside

Case 4: other left, ends inside; contains vertically

Case 5: other contained horizontally; starts below, ends inside

Case 6: other fully contained in self

Case 7: other contained horizontally; starts inside, ends above

Case 8: other contains self horizontally; overlaps vertically

Case 9: other right+below, starts inside

Case 10: other right, starts inside; contained vertically

Case 11: other right+above, starts inside

Case 12: other right, starts inside; contains vertically

Case 13: other contains horizontally; starts below, ends inside

Case 14: other contains horizontally; contained vertically

Case 15: other contains horizontally; starts inside, ends above

Case 16: other completely contains self

This does not inspire confidence in getting understandable code, let’s not even attempt it.

Negation to the rescue again#

Using the same idea as for intervals, let’s look at the cases where two boxes do not overlap.

Case 1: other is to the left of self

Case 2: other is to the right of self

Case 3: other is above self

Case 4: other is below self

This is quite easy, the other box can be to the left, right, above, or below. If it is to the left, it does not matter how much (or at all) the box overlaps in the vertical direction. Translating this into straightforward code as before.

def overlaps(self, other: Box) -> bool:

""" Return true iff the two boxes overlap. """

Using the same analysis as before, we can use De Morgan’s laws to change the implementation to not start with a not.3

def overlaps(self, other: Box) -> bool:

""" Return true iff the two boxes overlap. """

This new expression can be interpreted as a two dimensional variant of the interval overlap check. Two boxes overlap if there is overlap in both the horizontal and the vertical direction.

Figuring out the best way to express a property can be tricky. For intervals, while one can sit down and think thoroughly to find the best way to express the overlaps property, it is not easy. Using case analysis is the straight-forward way to create these but it can easily lead to an overly complicated and error-prone expression. This is especially clear for the box case, where there are 16 different cases to handle.

Using negation as a trick to simplify the case analysis works really well here. Naturally, it doesn’t always work out this well, but when it does it is a technique that is worth keeping in mind.

联系我们 contact @ memedata.com