合并两个区间列表,优先级为一个列表

时间:2017-05-24 10:27:30

标签: python algorithm

我目前遇到了一个算法问题,我想在其中优化复杂性。

我有两个间隔S = [[s1, s2], [s3, s4], ..., [sn-1, sn]]W = [[w1, w2], [w3, w4], ..., [wm-1, wm]]的列表,我希望合并尊重顺序,而 S的间隔优先于W 。 (S表示强,W表示弱) 例如,该优先级意味着:

  • S = [[5,8]]W = [[1, 5], [7, 10]]将提供:res = [[1, 4, W], [5, 8, S], [9, 10, W]]。这里,来自W的间隔优先于S
  • 的间隔进行裁剪
  • S = [[5, 8]]W = [[2, 10]]将提供:res = [[2, 4, W], [5, 8, S], [9, 10, W]]。这里W的间隔分为两部分,因为S具有优先权。

在合并这些列表的同时,我需要通过在每个区间旁边写第三个元素来跟踪这些区间的强弱特性,我们可以将其称为符号。这就是为什么结果如下:[[1,4,W],[5,8,S],[9,10,W]]。 最后,由于所有区间的并集不覆盖某个范围内的所有整数,我们有第三个符号,假设B为空白填充缺失区间:[[1, 2, W], [5, 8, S], [9, 10, W], [16, 20, S]]将填充成为:[1, 2, W], [3, 4, B], [5, 8, S], [9, 10, W], [11, 15, B], [16, 20, S]] }

我的第一次尝试非常幼稚和懒惰(因为我首先想让它起作用): 如果这两个间隔列表所覆盖的最大整数是M,那么我创建了一个大小为M的列表,其中填充了B符号:res = [B]*M = [B, B, B ..., B] 然后我首先从W中逐个取出间隔,并在此间隔中重写索引res的元素,将其符号更改为W.接下来,我对S的间隔执行相同的操作,并且优先级得到尊重,因为我用符号S覆盖在最后一步。 它给出了类似的东西:

  1. [B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B, B]
  2. [B, B, B, W, W, W, W, B, W, W, W, W, B, W, W, B, B]
  3. [B, B, S, S, W, W, W, B, S, S, W, W, B, S, W, B, B]
  4. 最后,我最后一次通过大列表来分解并重新创建具有相应符号的区间。上一个例子给出: [[1, 2, B], [3, 4, S], [5, 7, W], [8, 8, B], [9, 10, S], [11, 12, W], [13, 13, B], [14, 14, S], [15, 15, W], [16, 17, B]]

    不幸的是,但可以预见的是,这个算法在实践中不可用:在我的应用程序中M大约是1000000,如果我不是,那么这个算法是 O (n 2 )错了。

    所以,我想要一些建议和方向来解决这个算法的复杂性问题。我确信这个问题看起来很像一个众所周知的算法问题,但我不知道该去哪里。

    我现在提出改进的几点想法可以用来优化算法,但实现起来相当复杂,所以我认为有更好的想法。但他们在这里:

    • 执行相同类型的覆盖过程以尊重优先级:在列表W中,在必要时插入S的间隔以覆盖优先级。然后填写此列表以插入带有B符号的缺失间隔。但是由于案件数量很多,我们会大量使用 if 来比较间隔。
    • 逐步浏览S和W时构建新列表。在这个想法中,我们将有一个光标列表从一个间隔到一个间隔,直到两个列表之一的结束。我们再次使用了很多 if 和我们在新列表中根据优先级插入间隔的情况。但它在大量案件中引发了同样复杂的问题。

    我希望自己明确表示,如果不是,我可以用其他方式解释。 请教我经验和聪明:)

    由于

    编辑:这是我的“天真”算法代码:

    def f(W, S, size):
    
      #We first write one symbol per sample
      int_result = ['B'] * size
      for interval in W:
          for i in range(interval[0], interval[1]+1):
              int_result[i] = 'W'
      for interval in S:
          for i in range(interval[0], interval[1]+1):
              int_result[i] = 'S'
    
      #we then factorize: we store one symbol for an interval of the same    symbol.    
      symbols_intervals = []
      sym = int_result[0]
      start = 0
      for j in range(len(int_result)):
          if int_result[j] != sym:
              symbols_intervals.append([start, j-1, sym])
              sym = all_symbols[j]
              start = j
          if j == len(int_result)-1:
              symbols_intervals.append([start, j-1, sym])
    
      return symbols_intervals
    

2 个答案:

答案 0 :(得分:0)

你天真的方法听起来很合理;我认为它的时间复杂度是 O(NM),其中 N 是您尝试解析的间隔数, M 是你试图解决它们的范围。您可能遇到的困难是您还有 O(M)的空间复杂性,这可能会占用相当多的内存。

这是一种合并方法,无需构建“主列表”,这可能更快;因为它将区间视为对象,所以复杂性不再与 M 相关联。

我将间隔(或间隔列表)表示为一组元组(a,b,p),每个元组都指示从ab的时间点,包括整数优先级pW可以是1S可以是2)。在每个区间中,必须是a< b。优先考虑更高的优先级。

我们需要一个谓词来定义两个区间之间的重叠:

def has_overlap(i1, i2):
    '''Returns True if the intervals overlap each other.'''
    (a1, b1, p1) = i1
    (a2, b2, p2) = i2
    A = (a1 - a2)
    B = (b2 - a1)
    C = (b2 - b1)
    D = (b1 - a2)
    return max(A * B, D * C, -A * D, B * -C) >= 0

当我们发现重叠时,我们需要解决它们。这种方法考虑到了这一点,尊重优先级:

def join_intervals(i1, i2):
    '''
    Joins two intervals, fusing them if they are of the same priority,
    and trimming the lower priority one if not.

    Invariant: the interval(s) returned by this function will not
    overlap each other.

    >>> join_intervals((1,5,2), (4,8,2))
    {(1, 8, 2)}
    >>> join_intervals((1,5,2), (4,8,1))
    {(1, 5, 2), (6, 8, 1)}
    >>> join_intervals((1,3,2), (4,8,2))
    {(1, 3, 2), (4, 8, 2)}
    '''
    if has_overlap(i1, i2):
        (a1, b1, p1) = i1
        (a2, b2, p2) = i2
        if p1 == p2:
            # UNION
            return set([(min(a1, a2), max(b1, b2), p1)])
        # DIFFERENCE
        if p2 < p1:
            (a1, b1, p1) = i2
            (a2, b2, p2) = i1
        retval = set([(a2, b2, p2)])
        if a1 < a2 - 1:
            retval.add((a1, a2 - 1, p1))
        if b1 > b2 + 1:
            retval.add((b2 + 1, b1, p1))
        return retval
    else:
        return set([i1, i2])

最后,merge_intervals采用可迭代的间隔并将它们连接在一起,直到不再有重叠:

import itertools

def merge_intervals(intervals):
    '''Resolve overlaps in an iterable of interval tuples.'''
    # invariant: retval contains no mutually overlapping intervals
    retval = set()
    for i in intervals:
        # filter out the set of intervals in retval that overlap the
        # new interval to add O(N)
        overlaps = set([i2 for i2 in retval if has_overlap(i, i2)])
        retval -= overlaps
        overlaps.add(i)
        # members of overlaps can potentially all overlap each other;
        # loop until all overlaps are resolved O(N^3)
        while True:
            # find elements of overlaps which overlap each other O(N^2)
            found = False
            for i1, i2 in itertools.combinations(overlaps, 2):
                if has_overlap(i1, i2):
                    found = True
                    break
            if not found:
                break
            overlaps.remove(i1)
            overlaps.remove(i2)
            overlaps.update(join_intervals(i1, i2))
        retval.update(overlaps)
    return retval

我认为这是 O(N ^ 4)的最坏情况时间复杂度,尽管平均情况应该很快。在任何情况下,您可能希望针对更简单的方法计算此解决方案,以查看哪种方法更适合您的问题。

据我所知,我的merge_intervals适用于您的示例:

# example 1
assert (merge_intervals({(5, 8, 2), (1, 5, 1), (7, 10, 1)}) ==
        {(1, 4, 1), (5, 8, 2), (9, 10, 1)})

# example 2
assert (merge_intervals({(5, 8, 2), (2, 10, 1)}) ==
        {(2, 4, 1), (5, 8, 2), (9, 10, 1)})

要以空白(B)间隔覆盖案例,只需添加另一个覆盖整个范围的间隔元组,优先级为0(1, M, 0)

# example 3 (with B)
assert (merge_intervals([(1, 2, 1), (5, 8, 2), (9, 10, 1),
                         (16, 20, 2), (1, 20, 0)]) ==
        {(1, 2, 1), (3, 4, 0), (5, 8, 2),
         (9, 10, 1), (11, 15, 0), (16, 20, 2)})

答案 1 :(得分:0)

以下解决方案具有O(n + m)复杂度,其中n和m是S和W列表的长度。它假定S和W在内部排序。

def combine(S, W):
    s, w = 0, 0 # indices of S and W
    common = []
    while s < len(S) or w < len(W):
        # only weak intervals remain, so append them to common
        if s == len(S):
            common.append((W[w][0], W[w][1], 'W'))
            w += 1
        # only strong intervals remain, so append them to common
        elif w == len(W):
            common.append((S[s][0], S[s][1], 'S'))
            s += 1
        # assume that the strong interval starts first
        elif S[s][0] <= W[w][0]:
            W[w][0] = max(W[w][0], S[s][1]+1)
            if W[w][0] > W[w][1]: # drop the weak interval
                w += 1
            common.append((S[s][0], S[s][1], 'S'))
            s += 1
        # assume that the weak interval starts first
        elif S[s][0] > W[w][0]:
            # end point of weak interval before the start of the strong
            if W[w][1] < S[s][0]:
                common.append(W[w][0], W[w][1], 'W')
                w += 1
            # end point of the weak interval between a strong interval
            elif S[s][0] <= W[w][1] <= S[s][1]:
                W[w][1] = S[s][0] - 1
                common.append((W[w][0], W[w][1], 'W'))
                w += 1
            # end point of the weak interval after the end point of the strong
            elif W[w][1] > S[s][1]:
                common.append((W[w][0], S[s][0]-1, 'W'))
                W[w][0] = S[s][1] + 1
    return common


print combine(S=[[5,8]], W=[[1, 5],[7, 10]])
print combine(S=[[5,8]], W=[[2,10]])