从非唯一的物品列表中获取唯一组合,更快的方法?

4

首先,我能够做到这一点,但我对速度不满意。

我的问题是,有没有更好、更快的方法来完成这个任务?

我有一个类似于下面的项目列表:

[(1,2), (1,2), (4,3), (7,8)]

我需要获取所有唯一的组合。例如,2个物品的唯一组合将是:

[(1,2), (1,2)], [(1,2), (4,3)], [(1,2), (7,8)], [(4,3), (7,8)]

使用itertools.combinations后,由于存在重复项,我得到的结果比期望的要多。例如,每个包含(1,2)的列表都会出现两次。如果我创建这些组合的集合,就能获得唯一的组合。

问题在于,当原始列表有80个元组,我想要其中包含6个项目的组合时,获取该集合需要超过30秒的时间。如果可以减少这个时间,我会非常高兴。

我知道组合的数量是庞大的,这就是为什么创建集合需要耗费时间的原因。但我仍然希望有一个库可以优化这个过程,以加快速度。

值得注意的是,对于所有找到的组合,我只测试前10000个左右。因为在某些情况下,所有组合可能太多而无法处理,所以我不想花费太多时间在它们上面,因为还有其他测试需要进行。

这是我现在拥有的样本:

from itertools import combinations

ls = [list of random NON-unique sets (x,y)]
# ls = [(1,2), (1,2), (4,3), (7,8)]  # example
# in the second code snipped it is shown how I generate ls for testing

all_combos = combinations(ls, 6)
all_combos_set = set(all_combos)

for combo in all_combos_set:
  do_some_test_on(combo)

如果您想测试它,这是我用于测试不同方法速度的内容:

def main3():
    tries = 4
    elements_in_combo = 6
    rng = 90
    data = [0]*rng
    for tr in range(tries):
        for n in range(1, rng):
            quantity = 0
            name = (0,0)
            ls = []
            for i in range(n):
                if quantity == 0:
                    quantity = int(abs(gauss(0, 4)))
                    if quantity != 0:
                        quantity -= 1
                    name = (randint(1000,7000), randint(1000,7000))
                    ls.append(name)
                else:
                    quantity -= 1
                    ls.append(name)

            start_time = time.time()
            all_combos = combinations(ls, elements_in_combo)
            all_combos = set(all_combos)

            duration = time.time() - start_time
            data[n] += duration
            print(n, "random files take", duration, "seconds.")

            if duration > 30:
                break

    for i in range(rng):
        print("average duration for", i, "is", (data[i]/tries), "seconds.")

https://dev59.com/V3M_5IYBdhLWcg3w1G6N - John Zwinck
我也尝试过在谷歌上搜索“numpy组合”。 - P_Rein
@P_Rein 请查看我的更新答案,并在底部的链接中检查另一个生成函数,该函数在有许多重复项的情况下速度很快,在只有少量重复项的情况下速度较慢。 - Claudio
@P_Rein 顺便说一下:你可以为任何你想看到好答案的问题提供赏金,所以请随意在这个问题https://dev59.com/8aDia4cB1Zd3GeqPILhI上提供赏金,以便得到你想要的(那里已经有一个有用的答案,但仍然不是最好的答案)。 - Claudio
顺便说一下:我关于组合的问题已经获得了比这个问题更多的访问量 - 如果你想引起注意,赏金做得很好... :D。 - Claudio
显示剩余4条评论
2个回答

5
最初提出的问题“有没有更好、更快的方法来做这件事?”实际上包含了两个问题:
- 有没有一种更快的方法? - 有没有一种更好的方法?
我想把答案缩小到第一个问题:“有没有一种更快的方法来从列表中去除重复项,而不是使用以下方式: lstWithUniqueElements = list(set(lstWithDuplicates))?”
据我所知,没有更快的方法...
现在让我们更加关注第二部分问题(“有没有更好的方法?”)。回答这种类型的问题通常非常困难,需要进行大量讨论,但本例并非如此,因为什么是更好的方法已经被问问题的作者明确表述过了(引用):
“我想使用生成器函数。itertools组合()本身是一个可迭代对象,而不是列表或集合,因此如果我能想出如何产生唯一的组合,那就太棒了。”
所以这里就是:
def uniqueCombinations(lstList, comboSize): 
    from itertools import combinations
    lstList.sort()
    allCombos = combinations(lstList, comboSize)
    setUniqueCombos = set()
    for comboCandidate in allCombos:
        if comboCandidate in setUniqueCombos:
            continue
        yield comboCandidate
        setUniqueCombos.add(comboCandidate)

这就是全部内容了...


还有一件事或许值得在此提一下。在生成组合的列表中,如果不仅包含独特的元素,而且还有多个具有相同值的元素,则问题作者选择的获取唯一组合的方法在某些特殊情况下可能无法正常工作,比如这种情况:

set(combinations(['a','a','b','a'], 2)) gives: {('a', 'b'), ('b', 'a'), ('a', 'a')}
uniqueCombinations(['a','a','b','a'],2) gives: {('a', 'b'), ('a', 'a')}

这里有一个在stackoverflow上提供的纯Python函数,与上面提供的函数相比,它既更快又更慢。为什么可以同时更快和更慢?请参见此处了解详情。


我删除了我的评论,这样如果有人有真正的反馈,他可以使用评论。谢谢! - MSeifert
我不知道那是什么黑魔法,但uniqueCombinations的效果非常好。 它看起来与以下代码完全相同: all_combos = combinations(ls, elements_in_combo) bs = set() enough = 0 for combo in all_combos: if combo in bs: continue bs.add(combo) if enough > 10000: break 但其中一个性能比另一个要好得多。 - P_Rein
黑魔法的部分是我太蠢了,错过了“足够”的增量。我一定是被绝望冲昏了头脑:D 再次感谢您花费的所有时间。 - P_Rein

1
我猜这个答案可能迟了,但是我遇到了同样的问题,我想分享我的解决方案。我希望不在内存中存储任何组合,因为很容易出错。
首先,this link提供了一个非常清晰的解释,关于如何计算重复元素时不同组合的数量。策略是创建具有替换的组合,然后丢弃不允许的组合。
例如,如果集合是(A,A,B,B),并且您想要所有3个元素的组合,则不允许组合(A,A,A)和(B,B,B)。因此,思路是从原始集合的唯一元素列表中创建所有可能的替代组合,然后丢弃那些无效的组合。这不会占用任何查找的内存,并且很容易编写。
然而,当我们有许多唯一元素的集合时,这种策略是浪费的。将这个问题推向极端,从集合(A,B,C)中仅有的3个元素组合显然是(A,B,C),但这种策略将产生(A,A,A),(A,A,B)等。为了缓解这个问题,可以注意到在有效组合中唯一元素只能出现一次:对于唯一元素,标准的itertools.combinations()将起作用。
因此,如果我们有唯一和重复元素的混合,最终的组合可以分成两部分:一部分是使用itertools.combinations()生成的唯一元素,另一部分是使用itertools.combinations_with_replacement()生成的重复元素。
总之,这就是代码。它的运行速度取决于原始集合中重复的数量。最坏的情况是没有重复的情况:
import itertools
from collections import Counter

#Check if an element is repeated more times than allowed.
def comb_check(comb, original_dic):
    trouble = False
    if not comb:
        return(not trouble)
    comb_unique = set(comb)
    ratio = len(comb_unique)/len(comb)
    if ratio < 1:
       comb = Counter(comb)
       ks = (v for v in comb_unique)
       complete = False
       while (not trouble) and (not complete):
           try:
               k = next(ks)
               if comb[k] > 1:
                   if original_dic[k] < comb[k]: trouble = True
           except StopIteration:
               complete = True
    return(not trouble)

def generate_comb(elements,k):
    elements = Counter(elements)
    elements_unique = [k for k,v in elements.items() if v == 1]
    elements_other = [k for k, v in elements.items() if k not in elements_unique]
    max_repetition = sum([elements[k] for k in elements_other ])
    for n in range(0, min(k+1,len(elements_unique)+1)):
        if (n + max_repetition)>= k:
            for i in itertools.combinations(elements_unique, n):
                for j in itertools.combinations_with_replacement(elements_other, k-n):
                    if comb_check(j, elements):
                        (yield  j)

#All unique elements is the worst case when it comes to time
lst = [a for a in range(80)]
for k in generate_comb(lst, 6):
    pass
#It took my machine ~ 264 sec to run this

#Slightly better
lst = [a for a in range(40)] + [a for a in range(40)]
for k in generate_comb(lst, 6):
    pass
#It took my machine ~ 32 sec to run this

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接