【问题标题】:Fastest way to find unique combinations of list查找列表的唯一组合的最快方法
【发布时间】:2018-07-14 03:17:04
【问题描述】:

我正在尝试解决从Python 的列表中获取唯一组合的一般问题

https://www.mathsisfun.com/combinatorics/combinations-permutations-calculator.html 数学上,我可以看到组合数的公式是n!/r!(n-r)!,其中n 是序列的长度,r 是要选择的数字。

如下python所示,其中n为4,r为2:

lst = 'ABCD'
result = list(itertools.combinations(lst, len(lst)/2))
print len(result)
6

以下是显示我遇到的问题的辅助函数:

def C(lst):
    l = list(itertools.combinations(sorted(lst), len(lst)/2))
    s = set(l)
    print 'actual', len(l), l
    print 'unique', len(s), list(s)

如果我从 iPython 运行它,我可以这样称呼它:

In [41]: C('ABCD')
actual 6 [('A', 'B'), ('A', 'C'), ('A', 'D'), ('B', 'C'), ('B', 'D'), ('C', 'D')]
unique 6 [('B', 'C'), ('C', 'D'), ('A', 'D'), ('A', 'B'), ('A', 'C'), ('B', 'D')]

In [42]: C('ABAB')
actual 6 [('A', 'A'), ('A', 'B'), ('A', 'B'), ('A', 'B'), ('A', 'B'), ('B', 'B')]
unique 3 [('A', 'B'), ('A', 'A'), ('B', 'B')]

In [43]: C('ABBB')
actual 6 [('A', 'B'), ('A', 'B'), ('A', 'B'), ('B', 'B'), ('B', 'B'), ('B', 'B')]
unique 2 [('A', 'B'), ('B', 'B')]

In [44]: C('AAAA')
actual 6 [('A', 'A'), ('A', 'A'), ('A', 'A'), ('A', 'A'), ('A', 'A'), ('A', 'A')]
unique 1 [('A', 'A')]

我想要得到的是如上所示的唯一计数,但是执行 combinations 然后 set 无法扩展。 当lst(即n)的长度变长时,它会随着组合越来越大而变慢。

有没有办法使用数学或Python 技巧来解决计数唯一组合的问题?

【问题讨论】:

  • 数一下就够了,还是你也想要实际的组合?
  • 您似乎可以通过从原始列表中省略任何重复项然后从缩减列表中生成组合来找到唯一组合。例如。从 ABBC 获取 ABC 并从中生成组合。也许我在这里遗漏了一些东西。
  • @Robert 无法从您的精简列表中获取 (B,B)。
  • stackoverflow.com/questions/36429507/… .. 第 4 个回答下来.. 使用计数器
  • @johnashu 看着那个反例可能会做我想做的事:O - 尽管我不得不对其进行一些修复以使其正常工作,因为它在发布时失败了。

标签: python math


【解决方案1】:

这似乎被称为多集组合。我遇到了同样的问题,最后想出了从sympy (here) 重写一个函数。

您无需将可迭代对象传递给 itertools.combinations(p, r) 之类的东西,而是将 collections.Counter(p).most_common() 传递给以下函数以直接检索不同的组合。这比过滤所有组合要快得多,而且内存安全!

def counter_combinations(g, n):
    if sum(v for k, v in g) < n or not n:
        yield []
    else:
        for i, (k, v) in enumerate(g):
            if v >= n:
                yield [k]*n
                v = n - 1
            for v in range(min(n, v), 0, -1):
                for j in counter_combinations(g[i + 1:], n - v):
                    rv = [k]*v + j
                    if len(rv) == n:
                        yield rv

这是一个例子:

from collections import Counter

p = Counter('abracadabra').most_common()
print(p)
c = [_ for _ in counter_combinations(p, 4)]
print(c)
print(len(c))

输出:

[('a', 5), ('b', 2), ('r', 2), ('c', 1), ('d', 1)]
[['a', 'a', 'a', 'a'], ['a', 'a', 'a', 'b'], ['a', 'a', 'a', 'r'], ['a', 'a', 'a', 'c'], ['a', 'a', 'a', 'd'], ['a', 'a', 'b', 'b'], ['a', 'a', 'b', 'r'], ['a', 'a', 'b', 'c'], ['a', 'a', 'b', 'd'], ['a', 'a', 'r', 'r'], ['a', 'a', 'r', 'c'], ['a', 'a', 'r', 'd'], ['a', 'a', 'c', 'd'], ['a', 'b', 'b', 'r'], ['a', 'b', 'b', 'c'], ['a', 'b', 'b', 'd'], ['a', 'b', 'r', 'r'], ['a', 'b', 'r', 'c'], ['a', 'b', 'r', 'd'], ['a', 'b', 'c', 'd'], ['a', 'r', 'r', 'c'], ['a', 'r', 'r', 'd'], ['a', 'r', 'c', 'd'], ['b', 'b', 'r', 'r'], ['b', 'b', 'r', 'c'], ['b', 'b', 'r', 'd'], ['b', 'b', 'c', 'd'], ['b', 'r', 'r', 'c'], ['b', 'r', 'r', 'd'], ['b', 'r', 'c', 'd'], ['r', 'r', 'c', 'd']]
31

【讨论】:

    【解决方案2】:

    combinations() 的常规递归定义开始,但添加一个测试以仅在该级别的前导值之前未使用过时才递归:

    def uniq_comb(pool, r):
        """ Return an iterator over a all distinct r-length
        combinations taken from a pool of values that
        may contain duplicates.
    
        Unlike itertools.combinations(), element uniqueness
        is determined by value rather than by position.
    
        """
        if r:
            seen = set()
            for i, item in enumerate(pool):
                if item not in seen:
                    seen.add(item)
                    for tail in uniq_comb(pool[i+1:], r-1):
                        yield (item,) + tail
        else:
            yield ()
    
    if __name__ == '__main__':
        from itertools import combinations
    
        pool = 'ABRACADABRA'
        for r in range(len(pool) + 1):
            assert set(uniq_comb(pool, r)) == set(combinations(pool, r))
            assert dict.fromkeys(uniq_comb(pool, r)) == dict.fromkeys(combinations(pool, r))
    

    【讨论】:

      【解决方案3】:

      这是一些基于this Math Forum article 中概述的生成函数方法的 Python 代码。对于输入中出现的每个字母,我们创建一个多项式1 + x + x^2 + ... + x^k,其中k 是该字母出现的次数。然后我们将这些多项式相乘:得到的多项式的nth 系数然后告诉您有多少个长度组合n

      我们将多项式简单地表示为其(整数)系数的列表,第一个系数表示常数项,下一个系数表示x 的系数,依此类推。我们需要能够将这样的多项式相乘,所以这里有一个函数可以做到这一点:

      def polymul(p, q):
          """
          Multiply two polynomials, represented as lists of coefficients.
          """
          r = [0]*(len(p) + len(q) - 1)
          for i, c in enumerate(p):
              for j, d in enumerate(q):
                  r[i+j] += c*d
          return r
      

      有了上面的内容,下面的函数计算组合的数量:

      from collections import Counter
      from functools import reduce
      
      def ncombinations(it, k):
          """
          Number of combinations of length *k* of the elements of *it*.
          """
          counts = Counter(it).values()
          prod = reduce(polymul, [[1]*(count+1) for count in counts], [1])
          return prod[k] if k < len(prod) else 0
      

      在您的示例上进行测试:

      >>> ncombinations("abcd", 2)
      6
      >>> ncombinations("abab", 2)
      3
      >>> ncombinations("abbb", 2)
      2
      >>> ncombinations("aaaa", 2)
      1
      

      在一些更长的例子中,证明这种方法即使对于长输入也是可行的:

      >>> ncombinations("abbccc", 3)  # the math forum example
      6
      >>> ncombinations("supercalifragilisticexpialidocious", 10)
      334640
      >>> from itertools import combinations  # double check ...
      >>> len(set(combinations(sorted("supercalifragilisticexpialidocious"), 10)))
      334640
      >>> ncombinations("supercalifragilisticexpialidocious", 20)
      1223225
      >>> ncombinations("supercalifragilisticexpialidocious", 34)
      1
      >>> ncombinations("supercalifragilisticexpialidocious", 35)
      0
      >>> from string import printable
      >>> ncombinations(printable, 50)  # len(printable)==100
      100891344545564193334812497256
      >>> from math import factorial
      >>> factorial(100)//factorial(50)**2  # double check the result
      100891344545564193334812497256
      >>> ncombinations("abc"*100, 100)
      5151
      >>> factorial(102)//factorial(2)//factorial(100)  # double check (bars and stars)
      5151
      

      【讨论】:

      • 又一次仔细检查:len(set(itertools.combinations(sorted("supercalifragilisticexpialidocious"), 10))) 很快就确认了 334640。我相信这比反复检查极端情况下没有重复和无休止的供应更重要。
      • 1223225 for k=20 也证实了这一点(大约 3 分钟),以及 k 从 17 到 34 的所有其他结果。
      • @StefanPochmann:谢谢!我在示例中添加了k=10 双重检查。
      • @MarkDickinson 有没有办法同时获得独特的组合(不仅仅是计数)?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-01-05
      • 2021-07-06
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多