【问题标题】:How to divide a set of overlapping ranges into non-overlapping ranges?如何将一组重叠范围划分为非重叠范围?
【发布时间】:2010-10-12 08:17:57
【问题描述】:

假设您有一组范围:

  • 0 - 100: 'a'
  • 0 - 75: 'b'
  • 95 - 150: 'c'
  • 120 - 130:'d'

显然,这些范围在某些点重叠。您将如何剖析这些范围以生成不重叠范围的列表,同时保留与其原始范围相关的信息(在本例中为范围后面的字母)?

例如上面运行算法后的结果是:

  • 0 - 75: 'a', 'b'
  • 76 - 94: 'a'
  • 95 - 100: 'a', 'c'
  • 101 - 119:'c'
  • 120 - 130: 'c', 'd'
  • 131 - 150: 'c'

【问题讨论】:

标签: python algorithm math range rectangles


【解决方案1】:

对 Edmunds 的类似回答,经过测试,包括对 (1,1) 等区间的支持:

class MultiSet(object):
    def __init__(self, intervals):
        self.intervals = intervals
        self.events = None

    def split_ranges(self):
        self.events = []
        for start, stop, symbol in self.intervals:
            self.events.append((start, True, stop, symbol))
            self.events.append((stop, False, start, symbol))

        def event_key(event):
            key_endpoint, key_is_start, key_other, _ = event
            key_order = 0 if key_is_start else 1
            return key_endpoint, key_order, key_other

        self.events.sort(key=event_key)

        current_set = set()
        ranges = []
        current_start = -1

        for endpoint, is_start, other, symbol in self.events:
            if is_start:
                if current_start != -1 and endpoint != current_start and \
                       endpoint - 1 >= current_start and current_set:
                    ranges.append((current_start, endpoint - 1, current_set.copy()))
                current_start = endpoint
                current_set.add(symbol)
            else:
                if current_start != -1 and endpoint >= current_start and current_set:
                    ranges.append((current_start, endpoint, current_set.copy()))
                current_set.remove(symbol)
                current_start = endpoint + 1

        return ranges


if __name__ == '__main__':
    intervals = [
        (0, 100, 'a'), (0, 75, 'b'), (75, 80, 'd'), (95, 150, 'c'), 
        (120, 130, 'd'), (160, 175, 'e'), (165, 180, 'a')
    ]
    multiset = MultiSet(intervals)
    pprint.pprint(multiset.split_ranges())


[(0, 74, {'b', 'a'}),
 (75, 75, {'d', 'b', 'a'}),
 (76, 80, {'d', 'a'}),
 (81, 94, {'a'}),
 (95, 100, {'c', 'a'}),
 (101, 119, {'c'}),
 (120, 130, {'d', 'c'}),
 (131, 150, {'c'}),
 (160, 164, {'e'}),
 (165, 175, {'e', 'a'}),
 (176, 180, {'a'})]

【讨论】:

    【解决方案2】:

    你描述的是集合论的一个例子。有关计算集合的并集、交集和差集的通用算法,请参见:

    www.gvu.gatech.edu/~jarek/graphics/papers/04PolygonBooleansMargalit.pdf

    虽然本文针对的是图形,但它也适用于一般集合论。不完全是轻量级的阅读材料。

    【讨论】:

      【解决方案3】:

      在编写混合(部分重叠)音频样本的程序时,我遇到了同样的问题。

      我所做的是将“开始事件”和“停止事件”(针对每个项目)添加到列表中,按时间点对列表进行排序,然后按顺序处理。您可以这样做,除了使用整数点而不是时间,而不是混合声音,您将向与范围相对应的集合中添加符号。是生成空范围还是忽略它们都是可选的。

      Edit也许是一些代码...

      # input = list of (start, stop, symbol) tuples
      points = [] # list of (offset, plus/minus, symbol) tuples
      for start,stop,symbol in input:
          points.append((start,'+',symbol))
          points.append((stop,'-',symbol))
      points.sort()
      
      ranges = [] # output list of (start, stop, symbol_set) tuples
      current_set = set()
      last_start = None
      for offset,pm,symbol in points:
          if pm == '+':
               if last_start is not None:
                   #TODO avoid outputting empty or trivial ranges
                   ranges.append((last_start,offset-1,current_set))
               current_set.add(symbol)
               last_start = offset
          elif pm == '-':
               # Getting a minus without a last_start is unpossible here, so not handled
               ranges.append((last_start,offset-1,current_set))
               current_set.remove(symbol)
               last_start = offset
      
      # Finish off
      if last_start is not None:
          ranges.append((last_start,offset-1,current_set))
      

      显然,完全未经测试。

      【讨论】:

      • 那绝对是完美的,非常感谢!但是,必须解决一件事:在 range.append 行上,current_set 必须是 current_set.copy(),否则您最终只会为每个添加对 current_set 的引用(因此每个范围的最后一个空集结束)。谢谢!
      • 要记住的是,当范围标签是唯一的,但不能正确处理重叠范围内的重复标签(即 0-10 = A,5-10 = A , 10-20 = B)。否则,我很高兴看到有人和我有同样的想法,而且我也不是完全疯了。谢谢!
      【解决方案4】:

      伪代码:

      unusedRanges = [ (each of your ranges) ]
      rangesInUse = []
      usedRanges = []
      beginningBoundary = nil
      
      boundaries = [ list of all your ranges' start and end values, sorted ]
      resultRanges = []
      
      for (boundary in boundaries) {
          rangesStarting = []
          rangesEnding = []
      
          // determine which ranges begin at this boundary
          for (range in unusedRanges) {
              if (range.begin == boundary) {
                  rangesStarting.add(range)
              }
          }
      
          // if there are any new ones, start a new range
          if (rangesStarting isn't empty) {
              if (beginningBoundary isn't nil) {
                  // add the range we just passed
                  resultRanges.add(beginningBoundary, boundary - 1, [collected values from rangesInUse])
              }
      
              // note that we are starting a new range
              beginningBoundary = boundary
      
              for (range in rangesStarting) {
                  rangesInUse.add(range)
                  unusedRanges.remove(range)
              }
          }
      
          // determine which ranges end at this boundary
          for (range in rangesInUse) {
              if (range.end == boundary) {
                  rangesEnding.add(range)
              }
          }
      
          // if any boundaries are ending, stop the range
          if (rangesEnding isn't empty) {
              // add the range up to this boundary
              resultRanges.add(beginningBoundary, boundary, [collected values from rangesInUse]
      
              for (range in rangesEnding) {
                  usedRanges.add(range)
                  rangesInUse.remove(range)
              }
      
              if (rangesInUse isn't empty) {
                  // some ranges didn't end; note that we are starting a new range
                  beginningBoundary = boundary + 1
              }
              else {
                  beginningBoundary = nil
              }
          }
      }
      

      单元测试:

      最后,resultRanges 应该有你正在寻找的结果,unusedRanges 和 rangeInUse 应该是空的,beginningBoundary 应该是 nil,并且 usedRanges 应该包含未使用的Ranges 曾经包含的内容(但按 range.end 排序)。

      【讨论】:

        【解决方案5】:

        我会说创建一个端点列表并对其进行排序,还可以通过起点和终点索引范围列表。然后遍历已排序的端点列表,并为每个端点检查范围以查看哪些端点在该点开始/停止。

        这可能在代码中更好地表示...如果您的范围由元组表示:

        ranges = [(0,100,'a'),(0,75,'b'),(95,150,'c'),(120,130,'d')]
        endpoints = sorted(list(set([r[0] for r in ranges] + [r[1] for r in ranges])))
        start = {}
        end = {}
        for e in endpoints:
            start[e] = set()
            end[e] = set()
        for r in ranges:
            start[r[0]].add(r[2])
            end[r[1]].add(r[2])
        current_ranges = set()
        for e1, e2 in zip(endpoints[:-1], endpoints[1:]):
            current_ranges.difference_update(end[e1])
            current_ranges.update(start[e1])
            print '%d - %d: %s' % (e1, e2, ','.join(current_ranges))
        

        尽管回想起来,如果没有更有效(或至少看起来更干净)的方法,我会感到惊讶。

        【讨论】:

          猜你喜欢
          • 2013-10-28
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-06-03
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2018-05-02
          相关资源
          最近更新 更多