2
$\begingroup$

I'm looking for an efficient algorithm to merge a list of overlapping intervals (each of which has data associated) into non-overlapping intervals. In case two or more intervals overlap, the latter one wins (e.g. the later intervals shadow the earlier ones).

In my case, the intervals actually come pre-sorted (by starting point) but of course I can't do any further sorts because the intervals that come later in the input list dominate the previous ones.

Other facts:

  • the intervals may overlap but don't have to
  • there may be gaps not covered by any intervals

This feels like a common problem but I haven't found a common algorithm quite yet.

Algorithm ideas

The naive algorithm (O(n2)) is just to intersect each interval with every interval that follows, keeping only the 'non-occluded' pieces. But that's inefficient.

I've also thought about using a variation of the regular overlapping intervals algorithm: Basically, building a stack and always intersecting the next element with the top of the stack followed by pushing the non-overlapping part of the old element and the overlapping part of the new element. But of course that potentially leaves a left-over piece from the old element. (Essentially ((1, 10), "1") and ((2, 5), "2") becomes ((1, 2), "1"), ((2, 5), "2") with a left-over of ((5, 10), "1"). But that left over is now unsorted with respect to the elements that follow and I can't sort it into the input list either because other intervals need to dominate this one.

Or maybe I should use an interval intersection algorithm to find each of the segments and then use a scan line algorithm to find the original interval that dominates each of the segments?

Examples:

Example 1

  • input: [((1, 10), "green"), ((5, 8), "red")]
  • output: [((1, 5), "green"), ((5, 8), "red"), ((8, 10), "green")]

Example 2:

  • input: [((1,10), "1"), ((2, 4), "2"), ((3, 8), "3"), ((4, 7), "4"), ((5, 6), "5"), ((6, 9), "6")]
  • output: [((1, 2), "1"), ((2, 3), "2"), ((3, 4), "3"), ((4, 5), "4"), ((5, 6), "5"), ((6, 9), "6")]
  • visualised:
((1,10), "1") 1111111111
((2, 4), "2") |222     |
((3, 8), "3") ||333333 |
((4, 7), "4") |||4444  |
((5, 6), "5") ||||55   |
((6, 9), "6") |||||6666|
              ||||||||||
              vvvvvvvvvv
========================
merged        1234566661


```
$\endgroup$

2 Answers 2

1
$\begingroup$

I found an answer that works well. The basic concept is to iterate a sorted list of all 'points of interest' (start and end location of intervals). Iterating that (scan line) allows us to maintain a stack of 'active intervals' (push when interval starts; pop when interval ends) where the top one is the dominating interval. That will essentially give us a list of non-overlapping segments associated with the dominating interval. That is exactly the goal.

visualisation of the algorithm

In slightly more detail (hope I got the text description about right):

  1. Create an array of all intervals which are already sorted by start point in $O(n)$.
  2. Initialize:
    1. An empty hash set of 'dead intervals' in $O(1)$.
    2. An empty stack of 'active intervals' in $O(1)$.
    3. An empty array of 'output intervals' in $O(1)$.
  3. Create a 'points of interest' array of all start and end points of all the intervals alongside the kind (start/end) and index (into the array of all intervals) in $O(n)$.
  4. Sort the 'points of interest' array by position/index/kind in $O(n \log n)$.
  5. Run a scan line from front to back: Iterate through the 'points of interest' from front to back in $O(n)$. For each 'point of interest' do:
    1. If it's a start point:
      1. Add the interval (looked up by index) to the stack of 'active intervals' in $O(1)$.
      2. If we have previously remembered an 'open interval', close it and append it to the array of 'output intervals'. Also clear the remembered 'open interval' in $O(1)$.
      3. Remember the current position as 'open interval' in $O(1)$.
    2. If it's an end point:
      1. Add the interval (looked up by index) to the set of 'dead intervals' in $O(1)$.
      2. While the interval at the top of the 'active intervals' is contained in the set of 'dead intervals' in amortized $O(1)$ (this can be $O(n)$ but each interval can only be in the dead intervals once):
        1. Remove from 'dead intervals' in $O(1)$.
        2. Pop the stack in $O(1)$.
      3. If we have previously remembered an 'open interval', close it and append it to the array of 'output intervals'. Also clear the remembered 'open interval' in $O(1)$.
      4. If the stack of 'active intervals' is non-empty, remember the current position as an 'open interval' in $O(1)$.

Overall, the algorithm runs in $O(n \log n)$.

$\endgroup$
3
  • 1
    $\begingroup$ Thank you for this useful self-answer. I have edited the formatting and inaccuracies, see if it's okay with you. Kindly also provide source if you encountered this answer elsewhere. $\endgroup$ Feb 18 at 13:29
  • 1
    $\begingroup$ Thank you! Didn't encounter it elsewhere. $\endgroup$ Feb 18 at 19:56
  • $\begingroup$ You're welcome, you may accept the edit if you find it accurate. $\endgroup$ Feb 19 at 5:55
0
$\begingroup$

One conceptually simple approach is to sort the intervals by which is latest. Then create an empty binary tree for the intervals you keep. Iterate through the intervals, from latest on to earliest, and for each interval, check if it overlaps any interval in the tree; if not, add it to the tree. If you use a self-balancing tree, each tree operation takes $O(\log n)$ time, so the total running time is $O(n \log n)$.

$\endgroup$
1
  • $\begingroup$ Thanks for your reply. I'm not sure if I follow completely on how this would handle partially overlapping intervals. I need to keep the un-overlapped parts of intervals that have been partially overlapped by others. For example in my first example [((1, 10), "green"), ((5, 8), "red")], I don't just want to keep [((5, 8), "red")], I want to get [((1, 5), "green"), ((5, 8), "red"), ((8, 10), "green")]. So the 'red' part dominates in the region of the 'red' interval but then 'green' interval is un-occluded left and right of the 'red' interval. So two intervals may result in three intervals. $\endgroup$ Feb 15 at 8:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.