【问题标题】:Obtaining all sequences of tuples satisfying position criteria by combining subsets of tuples in a list通过组合列表中的元组子集获得满足位置标准的所有元组序列
【发布时间】:2021-12-08 20:45:16
【问题描述】:

我有一个元组列表,每个元组都携带以下信息:

(start_position,end_position,list of strings)

示例列表如下:

aList = [(6, 9, ['ataH']), 
(4, 9, ['svataH']), 
(0, 9, ['vEvasvataH']), 
(2, 5, ['vasu', 'vasU']), 
(1, 3, ['Eva', 'eva']), 
(0, 1, ['vA', 'vE'])]

我需要找到所有的元组序列,使得每个序列必须覆盖从start_positionend_position 的所有位置,在本例中是从09。在一个序列中,比如a,相邻的元组需要满足a[i+1][0] - a[i][1] <= 1 的约束。

总结一下,输出应该如下:

[[(0, 1, ['vA', 'vE']), (2,5,['vasu', 'vasU']), (6, 9, ['ataH'])  ],
 [(0, 1, ['vA', 'vE']), (1, 3, ['Eva', 'eva']), (4, 9, ['svataH'])],
 [(0, 9, ['vEvasvataH'], [7])]]

我已经使用以下代码来实现相同的效果。

maxVal = max(aList,key=lambda item:item[1])[1]
diffSets = list()

for i,item in enumerate(aList):
    if maxVal == item[1]:  
        _temp = [item]        
        currStart = item[0]
        for j,stuff in enumerate(aList):
            if i != j:
                if currStart == stuff[1] or currStart == stuff[1]+1:                    
                    _temp.append(stuff)
                    currStart = stuff[0]        
        diffSets.append(_temp) #the output needs to be reversed to get the sequence in the order as shown above

有没有更有效和更快的方法来实现同样的目标,比如使用itertools

【问题讨论】:

  • 你需要找到所有可能的集合吗?还是任何组合都可以?
  • 所有可能的组合@flakes
  • 在这种情况下你会怎么做:[(0, 6, []), (5, 10, [])]?
  • 你的意思是a[i][1] - a[i+1][0] <= 1
  • 此处无法进行线性搜索。找到重叠段后,您需要能够回溯。所以基本上,这是图遍历。

标签: python itertools


【解决方案1】:

这就是我的做法(并不是说它一定会更快)。首先,您可以先根据开始和结束对数据进行排序。这意味着当我们稍后查看组合时,我们不必回溯到结果中的其他值(我们将知道entry[i] 的开头必须小于或等于entry[i+1] 的开头)

import itertools
import operator

data = [
    (6, 9, ['ataH']),
    (4, 9, ['svataH']),
    (0, 9, ['vEvasvataH']),
    (2, 5, ['vasu', 'vasU']),
    (1, 3, ['Eva', 'eva']),
    (0, 1, ['vA', 'vE'])
]

data = sorted(data, key=operator.itemgetter(0, 1))
start = min(data, key=operator.itemgetter(0))[0]
end = max(data, key=operator.itemgetter(1))[1]

现在我们已经对数据进行了排序,并且知道了我们的开始值和结束值。

要找到任何大小的子集的所有数据组合,我们可以使用这个技巧:https://stackoverflow.com/a/5898031/3280538

def all_combinations(data):
    for i in range(len(data)):
        yield from itertools.combinations(data, r=i+1)

这里我们使用yield 来避免创建昂贵的容器。现在我们编写另一个使用这些组合的方法,并为每个方法检查其有效性:

def valid_combinations(data):
    for comb in all_combinations(data):
        if comb[0][0] != start or comb[-1][1] != end:
            continue
    
        for i in range(len(comb) - 1):
            if not 0 <= comb[i+1][0] - comb[i][1] <= 1:
                break
        else:
            yield comb

在这里,我使用了一个巧妙的 for 循环技巧。循环中的 else 块只有在 for 循环自然完成并且没有中断的情况下才会执行,如果我们不中断,那么我们知道每个项目都是有效的。

大家一起:

import itertools
import operator


def all_combinations(data):
    for i in range(len(data)):
        yield from itertools.combinations(data, r=i+1)


def valid_combinations(data):
    data = sorted(data, key=operator.itemgetter(0, 1))
    start = min(data, key=operator.itemgetter(0))[0]
    end = max(data, key=operator.itemgetter(1))[1]

    for comb in all_combinations(data):
        if comb[0][0] != start or comb[-1][1] != end:
            continue

        for i in range(len(comb) - 1):
            if not 0 <= comb[i+1][0] - comb[i][1] <= 1:
                break
        else:
            yield comb

得到结果:

from pprint import pprint
pprint(list(valid_combinations(
    [
        (6, 9, ['ataH']),
        (4, 9, ['svataH']),
        (0, 9, ['vEvasvataH']),
        (2, 5, ['vasu', 'vasU']),
        (1, 3, ['Eva', 'eva']),
        (0, 1, ['vA', 'vE'])
    ]
)))
[((0, 9, ['vEvasvataH']),),
 ((0, 1, ['vA', 'vE']), (1, 3, ['Eva', 'eva']), (4, 9, ['svataH'])),
 ((0, 1, ['vA', 'vE']), (2, 5, ['vasu', 'vasU']), (6, 9, ['ataH']))]

【讨论】:

    【解决方案2】:

    假设您的任何节点都不允许完全重叠。您可以设置节点之间的连接图,并将其传递给 networkx 之类的东西,它实现了您感兴趣的公认的最佳搜索算法。

    您有一个 DAG,其连接由一个节点的结尾和下一个节点的开头之间的重叠表示。以下是填充图表的方法:

    import networkx as nx
    
    g = nx.DiGraph()
    for i, n in enumerate(aList):
        g.add_node(i, start=n[0], end=n[1], data=n[2])
    

    节点是aList 中数据的索引,数据存储为属性。这是您可以添加边缘的地方。这是O(n<sup>2</sup>),但如果您对数据进行适当的预排序,则可以简化为O(n log n)

    for i, m in enumerate(aList):
        for j, n in enumerate(aList):
            if i != j and m[0] < n[0] and n[0] - m[1] <= 1 and m[1] < n[1]:
                g.add_edge(i, j)
    

    现在您可以找到跨越整个图表的路径。首先确定起点:

    from operator import itemgetter
    
    minValue = min(aList, key=itemgetter(0))[0]
    maxValue = max(aList, key=itemgetter(1))[1]
    
    start = [i for i, n in enumerate(aList) if n[0] == minValue]
    end = [i for i, n in enumerate(aList) if n[1] == maxValue]
    

    可以使用nx.all_simple_paths 等方式查找路径:

    paths = []
    for node in start:
        if node in end:
            paths.append([node])
        else:
            paths.extend(nx.all_simple_paths(g, node, end))
    

    现在你有一个这样的列表:

    [[2], [5, 3, 0], [5, 3, 1], [5, 4, 1], [5, 4, 3, 0], [5, 4, 3, 1]]
    

    您可以使用结果中的索引对原始列表进行采样,或使用存储在图表中的元数据,具体取决于您的偏好。这是前一种方法:

    result = [[aList[item] for item in path] for path in paths]
    

    最终的结果是这样的:

    [[(0, 9, ['vEvasvataH'])],
     [(0, 1, ['vA', 'vE']), (2, 5, ['vasu', 'vasU']), (6, 9, ['ataH'])],
     [(0, 1, ['vA', 'vE']), (2, 5, ['vasu', 'vasU']), (4, 9, ['svataH'])],  # Do you want this (5 > 4)?
     [(0, 1, ['vA', 'vE']), (1, 3, ['Eva', 'eva']), (4, 9, ['svataH'])],
     [(0, 1, ['vA', 'vE']), (1, 3, ['Eva', 'eva']), (2, 5, ['vasu', 'vasU']), (6, 9, ['ataH'])], # Do you want this (3 > 2)?
     [(0, 1, ['vA', 'vE']), (1, 3, ['Eva', 'eva']), (2, 5, ['vasu', 'vasU']), (4, 9, ['svataH'])]] # Do you want this (3 > 2), (5 > 4)?
    

    由于路径是有序的,因此很容易立即看出它们是如何跨越区间的。

    【讨论】:

    • 非常感谢您的解决方案。在其当前形式中,它过度生成了一些无效的解决方案(您使用 cmets 标记的解决方案)。但是,您已经说明了整个解决方案,我已经清楚地根据我的要求对其进行了修改。
    • @AmrithKrishna。您可以将n[0] - m[1] &gt;= 0 添加到条件中,并可能删除约束m[0] &lt; n[0]。无论如何,预先排序并使用递归 DFS 算法可能会快得多。
    【解决方案3】:

    这是一个替代解决方案,它使用带有start_position 的字典作为关键字来加快搜索速度。

    import operator as op
    import itertools as it
    from collections import defaultdict
    
    # These two accessor functions make it more legible to work with the tuples
    start_position = op.itemgetter(0)
    end_position = op.itemgetter(1)
    
    def find_all_paths(list_of_tuples):
        last_position = end_position(max(list_of_tuples, key=end_position))
    
        tuples_dict = defaultdict(list)
        for t in list_of_tuples:
            tuples_dict[start_position(t)].append(t)
    
        def all_paths_starting_from(next_value):
            candidates = it.chain(tuples_dict[next_value], tuples_dict[next_value+1])
    
            paths = []
            for candidate in candidates:
                if end_position(candidate) == last_position:
                    paths.append([candidate])
                else:
                    for branch in all_paths_starting_from(end_position(candidate)):
                        paths.append([candidate] + branch)
            return paths
        return all_paths_starting_from(-1)
    

    这个想法是,在一个大图中,最大的时间消费者将搜索给定节点的下一个可能节点。 tuples_dict 将这个时间减少到基本上 O(1)。此外,递归的最大深度为len(tuples_dict)

    如果输入集很大并且图形有很多分支和合并,则可以记住 all_paths_starting_from 函数以提高性能(但代码变得不太清晰)。记忆的解决方案是:

    def find_all_paths(list_of_tuples):
        last_position = end_position(max(list_of_tuples, key=end_position))
    
        tuples_dict = defaultdict(list)
        for t in list_of_tuples:
            tuples_dict[start_position(t)].append(t)
    
        known_paths = {}  # Dict from next_value to the possible paths
        def all_paths_starting_from(next_value):
            if next_value in known_paths:
                return known_paths[next_value]
            candidates = it.chain(tuples_dict[next_value], tuples_dict[next_value+1])
    
            paths = []
            for candidate in candidates:
                if end_position(candidate) == last_position:
                    paths.append([candidate])
                else:
                    for branch in all_paths_starting_from(end_position(candidate)):
                        paths.append([candidate] + branch)
            known_paths[next_value] = paths
            return paths
        return all_paths_starting_from(-1)
    

    这是一个图表中的示例,其中包含更多的分支和合并(以及死胡同):

    test_input = [(0, 2, ['1']),
                  (0, 3, ['2']),
                  (0, 5, ['3']),
                  (2, 7, ['4']),
                  (3, 8, ['5']),
                  (4, 9, ['6']),
                  (8, 13, ['7']),
                  (9, 14, ['8']),
                  (10, 11, ['9']),
                  (14, 15, ['10']),
                  ]
    
    from pprint import pprint
    pprint(find_all_paths(test_input))
    

    结果是:

    [(0, 2, ['1']), (2, 7, ['4']), (8, 13, ['7']), (14, 15, ['10'])],
     [(0, 2, ['1']), (3, 8, ['5']), (8, 13, ['7']), (14, 15, ['10'])],
     [(0, 2, ['1']), (3, 8, ['5']), (9, 14, ['8']), (14, 15, ['10'])],
     [(0, 3, ['2']), (3, 8, ['5']), (8, 13, ['7']), (14, 15, ['10'])],
     [(0, 3, ['2']), (3, 8, ['5']), (9, 14, ['8']), (14, 15, ['10'])],
     [(0, 3, ['2']), (4, 9, ['6']), (9, 14, ['8']), (14, 15, ['10'])]]
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-04-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-10-02
      • 2016-12-19
      相关资源
      最近更新 更多