在计算之前删除包含某些值的组合

Posted

技术标签:

【中文标题】在计算之前删除包含某些值的组合【英文标题】:Remove combinations that contains some values before even calculated 【发布时间】:2018-07-14 23:05:47 【问题描述】:

给定一个列表和排除元素,是否可以忽略包含这些元素的组合计算?

示例 1

给定l = [1, 2, 3, 4, 5],我想在计算之前计算size 4 的所有组合,并排除包含(1, 3) 的组合。

结果是:

    All results:            Wanted results:

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

所有包含 1 和 3 的组合均已删除。

示例 2

@Eric Duminil 建议

l = [1, 2, 3, 4, 5, 6]size 4

的结果 不包括第二列中的(1, 2, 3)

不包括第三列中的(1, 2)

All results:        Wanted results 1            Wanted results 2
                    (Excluding [1, 2, 3]):      (Excluding [1, 2])

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

所有包含 1 和 2 和 3 的组合都已从想要的结果 1 中删除

所有包含 1 和 2 的组合都已从想要的结果 2 中删除

我有更大的组合要计算,但这需要很多时间,我想使用这些排除项来减少这个时间。

尝试过的解决方案

使用方法1,仍然计算组合

使用方法2,我尝试修改combinations function,但在计算之前我找不到合适的方法来忽略我的排除列表。

            Method 1                    |               Method 2
                                        |               
def main():                             |   def combinations(iterable, r):
    l = list(range(1, 6))               |       pool = tuple(iterable)
    comb = combinations(l, 4)           |       n = len(pool)
                                        |       if r > n:
    for i in comb:                      |           return
        if set([1, 3]).issubset(i):     |       indices = list(range(r))
            continue                    |       yield tuple(pool[i] for i in indices)
        else                            |       while True:
            process()                   |           for i in reversed(range(r)):
                                        |               if indices[i] != i + n - r:
                                        |                   break
                                        |               else:
                                        |                   return
                                        |           indices[i] += 1
                                        |           for j in range(i+1, r):
                                        |               indices[j] = indices[j-1] + 1
                                        |           yield tuple(pool[i] for i in indices)

编辑:

首先,谢谢大家的帮助,我忘了提供更多关于约束的细节。

输出的顺序不相关,例如,如果结果是[1, 2, 4, 5] [2, 3, 4, 5][2, 3, 4, 5] [1, 2, 4, 5],则不重要。

组合的元素应该(如果可能的话)排序,[1, 2, 4, 5] [2, 3, 4, 5] 而不是[2, 1, 5, 4] [3, 2, 4, 5],但这并不重要,因为组合可以在之后排序。

排除列表是不应出现组合一起的所有项目的列表。例如,如果我的排除列表是 (1, 2, 3),则不应计算包含 1 和 2 和 3 的所有组合。但是,允许使用 1 和 2 而不是 3 的组合。在这种情况下,如果我排除包含 (1, 2)(1, 2, 3) 的组合,那将完全没有用,因为将由 (1, 2, 3) 过滤的所有组合都已由 (1, 2) 过滤

多个排除列表必须是可能的,因为我对我的组合使用了多个约束。

测试答案

@tobias_k 此解决方案将排除列表 (1, 2, 3) 视为 OR 排除含义,如果我理解良好,将排除 (1, 2), (2, 3) and (1, 3),这在一个案例中很有用,但不适用于我当前的问题,我修改了问题以提供更多详细信息,抱歉造成混淆。在您的回答中,我不能仅使用列表 (1, 2)(1, 3) 作为您指定的排除项。然而,这种解决方案的一大优势是允许多重排除。

@Kasramvd 和 @mikuszefski 您的解决方案非常接近我想要的,如果它确实包含多个排除列表,那就是答案。

谢谢

【问题讨论】:

您是否需要按照您在问题中描述的方式对输出元素进行排序? 你能补充一点关于排他性约束的内容吗?如果你有一组三个独占号码,这是否意味着你只能拥有这三个中的一个,或者你可以拥有两个?你能有像“只有 1 和 3 中的一个,只有 1 和 5 中的一个,但 3 和 5 还可以”这样的约束吗? l = [1, 2, 3, 4, 5, 6]size 4 和不包括(1, 2, 3) 的结果应该是什么? 【参考方案1】:

(事实证明这并不完全符合 OP 的要求。仍然将其留在这里,因为它可能对其他人有所帮助。)


要包含互斥元素,您可以将它们包装在列表中的列表中,获取其中的combinations,然后获取子列表组合的product

>>> from itertools import combinations, product
>>> l = [[1, 3], [2], [4], [5]]
>>> [c for c in combinations(l, 4)]
[([1, 3], [2], [4], [5])]
>>> [p for c in combinations(l, 4) for p in product(*c)]
[(1, 2, 4, 5), (3, 2, 4, 5)]

一个更复杂的例子:

>>> l = [[1, 3], [2, 4, 5], [6], [7]]
>>> [c for c in combinations(l, 3)]
[([1, 3], [2, 4, 5], [6]),
 ([1, 3], [2, 4, 5], [7]),
 ([1, 3], [6], [7]),
 ([2, 4, 5], [6], [7])]
>>> [p for c in combinations(l, 3) for p in product(*c)]
[(1, 2, 6),
 (1, 4, 6),
 ... 13 more ...
 (4, 6, 7),
 (5, 6, 7)]

这不会生成任何以后要过滤掉的“垃圾”组合。但是,它假定您最多需要每个“独占”组中的一个元素,例如在第二个示例中,它不仅阻止与2,4,5 的组合,而且还阻止与2,44,52,5 的组合。此外,不可能(或至少不容易)只拥有1,31,5 之一,但允许3,5。 (也许可以将其扩展到这些情况,但我还不确定是否以及如何。)


您可以将其包装在一个函数中,从您的(假定的)格式派生出稍微不同的输入格式并返回一个一致的生成器表达式。这里,lst 是元素列表,r 是每个组合的项目数,exclude_groups 是互斥元素组的列表:

from itertools import combinations, product

def comb_with_excludes(lst, r, exclude_groups):
    ex_set = e for es in exclude_groups for e in es
    tmp = exclude_groups + [[x] for x in lst if x not in ex_set]
    return (p for c in combinations(tmp, r) for p in product(*c))

lst = [1, 2, 3, 4, 5, 6, 7]
excludes = [[1, 3], [2, 4, 5]]
for x in comb_with_excludes(lst, 3, excludes):
    print(x)

【讨论】:

这个解决方案甚至没有改变问题本身,而且根本没有效率。查看我对这种方法的更多 Pythonic 版本的回答。 @Kasramvd 你能解释一下吗?在什么意义上它不是有效的?正如我所说,它不会产生任何要过滤的“垃圾”,但由于开销可能它仍然较慢?我没有计时。另外,“甚至不改变问题”是什么意思?您的意思是“不仅改变问题”吗?如果是这样,在什么意义上?只需稍微改变输入格式? 是的,更改输入格式是另一个问题。问题的重点是如何处理这些输入。关于您使用嵌套循环并同时使用productscombinations 的性能。想象一下,如果排除和列表的数量更大,这种方法将如何执行。此外,组合使用列表对象也不是内存效率高(由于不变性和缺乏兑现等)。 @Kasramvd Ehrm... 是的,它有嵌套循环,但它仍然生成完全 OP 想要的组合(前提是我正确猜到了那些不清楚的排除列表是关于什么的确切地说,请参阅我的 cmets)。当然,如果这是问题所在,列表理解可以轻松地更改为生成器。并且可以预先对输入进行转换。 @Kasramvd 我认为 OP 的问题仅通过一个示例并没有得到很好的定义,这就是我要求澄清的原因。但是,我的回答确实为该确切问题提供了一个有效的解决方案(请参阅我对转换输入的编辑)以及我认为是“一般”情况的情况。【参考方案2】:

(事实证明,我的previous answer 并没有真正满足问题的限制,这是另一个问题。我将其作为单独的答案发布,因为方法大不相同,原始答案可能仍然有帮助其他人。)

您可以递归地实现这一点,每次在递归之前将另一个元素添加到组合中,检查这是否会违反其中一个排除集。这不会生成无效的组合,它适用于重叠的排除集(如(1,3), (1,5)),以及具有两个以上元素的排除集(如(2,4,5),允许除所有元素之外的任何组合)。

def comb_with_excludes(lst, n, excludes, i=0, taken=()):
    if n == 0:
        yield taken  # no more needed
    elif i <= len(lst) - n:
        t2 = taken + (lst[i],)  # add current element
        if not any(e.issubset(t2) for e in excludes):
            yield from comb_with_excludes(lst, n-1, excludes, i+1, t2)
        if i < len(lst) - n:  # skip current element
            yield from comb_with_excludes(lst, n, excludes, i+1, taken)

例子:

>>> lst = [1, 2, 3, 4, 5, 6]
>>> excludes = [1, 3, 1, 5, 2, 4, 5]
>>> list(comb_with_excludes(lst, 4, excludes))
[[1, 2, 4, 6], [2, 3, 4, 6], [2, 3, 5, 6], [3, 4, 5, 6]]

好吧,我现在已经计时了,结果证明这比在带有过滤器的生成器表达式中天真地使用 itertools.combination 慢得多,就像你已经做的那样:

def comb_naive(lst, r, excludes):
    return (comb for comb in itertools.combinations(lst, r)
                 if not any(e.issubset(comb) for e in excludes))

在 Python 中计算组合只是比使用库(可能在 C 中实现)并随后过滤结果要慢。根据可以排除的组合数量,这可能在某些情况下会更快,但老实说,我对此表示怀疑。

如果您可以将itertools.combinations 用于子问题(如Kasramvd's answer),您可以获得更好的结果,但对于多个非分离的排除集则更加困难。一种方法可能是将列表中的元素分成两组:有约束的,没有约束的。然后,对两者都使用itertoolc.combinations,但只检查那些重要元素的组合的约束。您仍然需要检查和过滤结果,但只是其中的一部分。 (不过需要注意的是:结果不是按顺序生成的,并且生成的组合中元素的顺序也有些混乱。)

def comb_with_excludes2(lst, n, excludes):
    wout_const = [x for x in lst if not any(x in e for e in excludes)]
    with_const = [x for x in lst if     any(x in e for e in excludes)]
    k_min, k_max = max(0, n - len(wout_const)), min(n, len(with_const))
    return (c1 + c2 for k in range(k_min, k_max)
                    for c1 in itertools.combinations(with_const, k)
                    if not any(e.issubset(c1) for e in excludes)
                    for c2 in itertools.combinations(wout_const, n - k))

这已经比递归纯 Python 解决方案好得多,但仍不如上述示例的“幼稚”方法:

>>> lst = [1, 2, 3, 4, 5, 6]
>>> excludes = [1, 3, 1, 5, 2, 4, 5]
>>> %timeit list(comb_with_excludes(lst, 4, excludes))
10000 loops, best of 3: 42.3 µs per loop
>>> %timeit list(comb_with_excludes2(lst, 4, excludes))
10000 loops, best of 3: 22.6 µs per loop
>>> %timeit list(comb_naive(lst, 4, excludes))
10000 loops, best of 3: 16.4 µs per loop

但是,结果很大程度上取决于输入。对于更大的列表,约束仅适用于其中的几个元素,这种方法实际上比简单的方法更快:

>>> lst = list(range(20))
>>> %timeit list(comb_with_excludes(lst, 4, excludes))
10 loops, best of 3: 15.1 ms per loop
>>> %timeit list(comb_with_excludes2(lst, 4, excludes))
1000 loops, best of 3: 558 µs per loop
>>> %timeit list(comb_naive(lst, 4, excludes))
100 loops, best of 3: 5.9 ms per loop

【讨论】:

感谢您的回答,这正是我想要的!你之前的回答也很有趣,我也可以用它来解决另一个问题!再次感谢! 经过测试,这个解决方案似乎比计算所有组合要慢得多:/你知道为什么吗? pyfiddle.io/fiddle/a1f2b4f3-16d5-47b5-8bee-4984b3502931/?i=true @SyedElec 好吧,我猜itertools.combinations 效率太高(可能用 C 和所有其他语言实现),所以在纯 Python 中重新创建它时,它总是会慢得多。根据要排除的组合数量,在某些情况下可能仍然会更快,但总的来说,您可能确实最好只使用带有 itertools.combinations 和过滤器的生成器表达式......【参考方案3】:

从算法的角度来看,您可以将有效项目的排除和重置分开,并分别计算每个集合的组合,然后根据期望长度连接结果。这种方法将完全拒绝同时包含所有排除的项目,但会省略实际的顺序。

from itertools import combinations

def comb_with_exclude(iterable, comb_num, excludes):
    iterable = tuple(iterable)
    ex_len = len(excludes)
    n = len(iterable)

    if comb_num < ex_len or comb_num > n:
        yield from combinations(iterable, comb_num)

    else:
        rest = [i for i in iterable if not i in excludes]
        ex_comb_rang = range(0, ex_len)
        rest_comb_range = range(comb_num, comb_num - ex_len, -1)
        # sum of these pairs is equal to the comb_num
        pairs = zip(ex_comb_rang, rest_comb_range)

        for i, j in pairs:
            for p in combinations(excludes, i):
                for k in combinations(rest, j):
                    yield k + p
       """
       Note that instead of those nested loops you could wrap the combinations within a product function like following:
       for p, k in product(combinations(excludes, i), combinations(rest, j)):
            yield k + p
       """

演示:

l = [1, 2, 3, 4, 5, 6, 7, 8]
ex = [2, 5, 6]
print(list(comb_with_exclude(l, 6, ex)))

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

l = [1, 2, 3, 4, 5]
ex = [1, 3]
print(list(comb_with_exclude(l, 4, ex)))

[(2, 4, 5, 1), (2, 4, 5, 3)]

以其他答案为基准:

结果:这种方法比其他方法更快

# this answer
In [169]: %timeit list(comb_with_exclude(lst, 3, excludes[0]))
100000 loops, best of 3: 6.47 µs per loop

# tobias_k
In [158]: %timeit list(comb_with_excludes(lst, 3, excludes))
100000 loops, best of 3: 13.1 µs per loop

# Vikas Damodar
In [166]: %timeit list(combinations_exc(lst, 3))
10000 loops, best of 3: 148 µs per loop

# mikuszefski
In [168]: %timeit list(sub_without(lst, 3, excludes[0]))
100000 loops, best of 3: 12.52 µs per loop

【讨论】:

不错。但不是真正可比的,因为函数计算的东西略有不同。这并不意味着任何一个都是错误的,只是 OP 的问题并不完全清楚。另外,您忘记在第一个%timeit 中使用生成器;-)(对不起) @tobias_k 是的,刚刚修复;))。他们都试图返回相同的结果,但关于您接受多个排除的方法,我只是将一个传递给它。 这不仅仅是多个排除集:在[2,4,5] 的情况下,我只允许三个数字中的一个,而您允许其中两个,只要不是全部三个即可。不过,对于[1,3],两者都应该产生相同的结果。但正如我所说,我们真的不知道它应该是哪一个。【参考方案4】:

我已尝试根据您的要求编辑组合:

def combinations(iterable, r):
   # combinations('ABCD', 2) --> AB AC AD BC BD CD
   # combinations(range(4), 3) --> 012 013 023 123
   pool = tuple(iterable)
   n = len(pool)
   if r > n:
      return
   indices = list(range(r))
   # yield tuple(pool[i] for i in indices)
   while True:
       for i in reversed(range(r)):
           if indices[i] != i + n - r:
               break
    else:
        return
    indices[i] += 1
    for j in range(i+1, r):
        indices[j] = indices[j-1] + 1
    # print(tuple(pool[i] for i in indices ), "hai")
    if 1 in tuple(pool[i] for i in indices ) and 3  in tuple(pool[i] for i in indices ):
        pass
    else:
        yield tuple(pool[i] for i in indices)


d = combinations(list(range(1, 6)),4)
for i in d:
   print(i)

它将返回如下内容:

(1, 2, 4, 5) (2, 3, 4, 5)

【讨论】:

true,但这是问题中的示例 1,它仍然计算所有组合。 我知道这不是一个好办法,但是如果他想继续我给他的第一个解决方案,可能是一个临时解决方案。 我看不到建议在哪里。这是 OPs 代码,增加了创建列表的开销。 @hiroprotagonist 我已经尝试按照 OP 要求的方式去做,请教你更好的建议 @Matthias 我已经进行了编辑,我不确定这是不是一个好方法。【参考方案5】:

我在组合过程中使用以下代码进行了排除以节省第二次循环时间。您只需要将排除元素的索引作为一组传递。

更新:working fiddle

from itertools import permutations

def combinations(iterable, r, combIndeciesExclusions=set()):
    pool = tuple(iterable)
    n = len(pool)
    for indices in permutations(range(n), r):
        if ( len(combIndeciesExclusions)==0 or not combIndeciesExclusions.issubset(indices)) and sorted(indices) == list(indices):
            yield tuple(pool[i] for i in indices)


l = list(range(1, 6))
comb = combinations(l, 4, set([0,2]))
print list(comb)

【讨论】:

你的解决方案不起作用,例如combinations([1, 2, 3], 2, set([1, 2])),谢谢 @SyedElec 你在说什么?您的查询给出了这个结果[(1, 2), (1, 3)],这是正确的。排除索引 1 和索引 2 意味着您不希望将值 2 和 3 放在同一个组合中。请注意,您正在设置索引而不是值。这将为您提供您想要的结果。 那么,接受元素集并在内部将它们转换为索引集可能会更简单。此外,这仍在计算所有组合,然后才检查它们是否有效。更糟糕的是,事实上:它生成所有排列,然后检查它们是否是组合(其中数量要少得多),然后 然后 检查它们是否有效。 @SyedElec 实际上这个方法来自于你的实现,查看下一个替代实现块docs.python.org/3/library/itertools.html#itertools.combinations 我只是调整了过滤过程以排除通过的索引集。你可以做基准测试,它不会有很大的不同。 @CME64 抱歉,我没有正确阅读您的答案,我正在测试所有答案的同一过程中,我没有读到您正在使用索引而不是值,python3 doc 中的代码并不是真的针对我的问题进行了优化。【参考方案6】:

我想我的答案与这里的其他人相似,但这是我并行摆弄的

from itertools import combinations, product

"""
with help from
https://***.com/questions/374626/how-can-i-find-all-the-subsets-of-a-set-with-exactly-n-elements
https://***.com/questions/32438350/python-merging-two-lists-with-all-possible-permutations
https://***.com/questions/952914/making-a-flat-list-out-of-list-of-lists-in-python
"""
def sub_without( S, m, forbidden ):
    out = []
    allowed = [ s for s in S if s not in forbidden ]
    N = len( allowed )
    for k in range( len( forbidden ) ):
        addon = [ list( x ) for x in combinations( forbidden, k) ]
        if N + k >= m:
            base = [ list( x ) for x in combinations( allowed, m - k ) ]
            leveltotal = [ [ item for sublist in x for item in sublist ] for x in product( base, addon ) ]
            out += leveltotal
    return out

val = sub_without( range(6), 4, [ 1, 3, 5 ] )

for x in val:
    print sorted(x)

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

【讨论】:

【参考方案7】:

从算法上讲,您必须计算列表中不属于排除项目的项目组合,然后将排除项目的相应组合添加到其余项目的组合中。这种方法当然需要大量检查并且需要跟踪索引,即使您在 python 中执行它也不会给您带来显着的性能差异(称为Constraint satisfaction problem 的缺点)。 (而不仅仅是使用combination 计算它们并过滤掉不需要的项目)。

因此,我认为在大多数情况下这是最好的方法:

In [77]: from itertools import combinations, filterfalse

In [78]: list(filterfalse(1, 3.issubset, combinations(l, 4)))
Out[78]: [(1, 2, 4, 5), (2, 3, 4, 5)]

【讨论】:

@tobias_k 我刚刚决定删除该解决方案。检查更新。 好吧,现在它基本上是 OP 已经在做的事情,只是在一行中,并且没有在每次迭代中重新创建集合。 (但并不是说这很糟糕;完全取决于实际更大的数据集中的“垃圾”数量。) @downvoter 你能解释一下你投反对票的原因吗? 这不是我的反对意见,但如果我不得不猜测,可能是因为“没有显着差异”的说法,这完全取决于组合的总数和比例那些不必生成的。 @tobias_k 我知道不是你。但不是!没那么简单。正如我在 in python 中提到的,如果您可以使用较低抽象的语言来执行此操作,它将给您带来显着的差异,但在 python 中,为了满足这些条件,您必须跟踪项目的索引,删除排除的一次并进行许多其他需要花费大量时间的操作。这被称为Constraint satisfaction problem 的缺点。

以上是关于在计算之前删除包含某些值的组合的主要内容,如果未能解决你的问题,请参考以下文章

如何在mongodb中获取包含某些(字符串)值的集合的所有键

如何删除具有太多 NULL 值的行?

使用 TSQL 从 XML 中删除具有特定值的节点

命名查询根据包含某些值的列表(实例变量)查找所有实体

Postgresql:如何查询包含某些值的 JSONb 数组

计算百分位数以去除异常值的快速算法