生成非连续组合

时间:2013-03-14 23:06:54

标签: python algorithm combinations

我正在尝试创建一个生成器(迭代器支持执行下一个,也许在python中使用yield),它提供了来自{1,2,... n}的r元素的所有组合(n和r是参数)在选定的r元素中,没有两个是连续的。

例如,对于r = 2且n = 4

生成的组合为{1,3}, {1,4}, {2, 4}

我可以生成所有组合(作为迭代器)并过滤那些不符合标准的组合,但我们将做不必要的工作。

是否有一些生成算法使next为O(1)(如果不可能,则为O(r)或O(n))。

返回集合的顺序不相关(并且希望允许O(1)算法)。

注意:我已将其标记为python,但语言无关的算法也会有所帮助。

更新

我找到了一种将它映射到生成纯组合的方法!网络搜索显示组合可能有O(1)(虽然看起来很复杂)。

这是映射。

假设我们的组合x_1, x_2, ... , x_rx_1 + 1 < x_2, x_2 + 1 < x_3, ...

我们按以下方式映射到y_1, y_2, ..., y_r

y_1 = x_1

y_2 = x_2 - 1

y_3 = x_3 - 2

...

y_r = x_r - (r-1)

这样我们就可以y_1 < y_2 < y_3 ...没有非连续约束!

这基本上等于从n-r + 1中选择r个元素。因此,我需要做的就是运行(n-r + 1选择r)。

出于我们的目的,在生成内容后使用映射就足够了。

选择svkcr答案的原因

所有好的答案,但我选择了svkcr的答案。

以下是

的原因
  1. 它实际上是无国籍的(或者更准确地说是“马尔可夫”)。下一个排列可以从前一个排列生成。它几乎是最佳的:O(r)空间和时间。

  2. 这是可以预测的。我们确切地知道生成组合的顺序(词典)。

  3. 这两个属性可以很容易地并行生成(在可预测的点和委托中拆分),并且引入容错(如果CPU /机器发生故障,可以从上一个生成的组合中选择)!

    很抱歉,之前没有提到并行化,因为当我写这个问题时我没有想到并且我后来才知道这个想法。

5 个答案:

答案 0 :(得分:3)

这是我的递归生成器(如果选择了n+1项,它只会跳过n项:

def non_consecutive_combinator(rnge, r, prev=[]):
    if r == 0:
        yield prev

    else:
        for i, item in enumerate(rnge):
            for next_comb in non_consecutive_combinator(rnge[i+2:], r-1, prev+[item]):
                yield next_comb

print list(non_consecutive_combinator([1,2,3,4], 2))
#[[1, 3], [1, 4], [2, 4]]
print list(non_consecutive_combinator([1,2,3,4,5], 2))
#[[1, 3], [1, 4], [1, 5], [2, 4], [2, 5], [3, 5]]
print list(non_consecutive_combinator(range(1, 10), 3))
#[[1, 3, 5], [1, 3, 6], [1, 3, 7], [1, 3, 8], [1, 3, 9], [1, 4, 6], [1, 4, 7], [1, 4, 8], [1, 4, 9], [1, 5, 7], [1, 5, 8], [1, 5, 9], [1, 6, 8], [1, 6, 9], [1, 7, 9], [2, 4, 6], [2, 4, 7], [2, 4, 8], [2, 4, 9], [2, 5, 7], [2, 5, 8], [2, 5, 9], [2, 6, 8], [2, 6, 9], [2, 7, 9], [3, 5, 7], [3, 5, 8], [3, 5, 9], [3, 6, 8], [3, 6, 9], [3, 7, 9], [4, 6, 8], [4, 6, 9], [4, 7, 9], [5, 7, 9]]

关于效率:

此代码不能为O(1),因为遍历堆栈并在每次迭代时构建新集合不会是O(1)。此外,递归生成器意味着您必须使用r最大堆栈深度来获取r - 项组合。这意味着低r,调用堆栈的成本可能比非递归生成更昂贵。如果有足够的nr,它可能比基于itertools的解决方案更有效。

我在这个问题中测试了两个上传的代码:

from itertools import ifilter, combinations
import timeit

def filtered_combi(n, r):
    def good(combo):
        return not any(combo[i]+1 == combo[i+1] for i in range(len(combo)-1))
    return ifilter(good, combinations(range(1, n+1), r))

def non_consecutive_combinator(rnge, r, prev=[]):
    if r == 0:
        yield prev

    else:
        for i, item in enumerate(rnge):
            for next_comb in non_consecutive_combinator(rnge[i+2:], r-1, prev+[item]):
                yield next_comb

def wrapper(n, r):
    return non_consecutive_combinator(range(1, n+1), r)   

def consume(f, *args, **kwargs):
    deque(f(*args, **kwargs))

t = timeit.timeit(lambda : consume(wrapper, 30, 4), number=100)
f = timeit.timeit(lambda : consume(filtered_combi, 30, 4), number=100)

结果和更多结果(编辑)(在windows7,python 64bit 2.7.3,核心i5常春藤桥与8gb ram):

(n, r)  recursive   itertools
----------------------------------------
(30, 4) 1.6728046   4.0149797   100 times   17550 combinations
(20, 4) 2.6734657   7.0835997   1000 times  2380 combinations
(10, 4) 0.1253318   0.3157737   1000 times  35 combinations
(4, 2)  0.0091073   0.0120918   1000 times  3 combinations
(20, 5) 0.6275073   2.4236898   100 times   4368 combinations
(20, 6) 1.0542227   6.1903468   100 times   5005 combinations
(20, 7) 1.3339530   12.4065561  100 times   3432 combinations
(20, 8) 1.4118724   19.9793801  100 times   1287 combinations
(20, 9) 1.4116702   26.1977839  100 times   220 combinations

正如您所看到的,递归解决方案和基于itertools.combination的解决方案之间的差距随着n上升而变宽。

实际上,两个解决方案之间的差距很大程度上取决于r - 更大的r意味着您必须丢弃从itertools.combinations生成的更多组合。例如,在n=20, r=9的情况下:我们在167960(20C9)组合中过滤并仅采用220种组合。如果nr很小,使用itertools.combinations可能会更快,因为它在更少的r下效率更高,并且不会像我解释的那样使用堆栈。 (正如您所看到的,itertools已经过优化(如果使用forifwhile和一堆生成器和列表推导来编写您的逻辑,它将不会像用itertools抽象出一个),这是人们喜欢python的原因之一 - 你将代码提升到更高的水平,并获得奖励。没有多少语言。

答案 1 :(得分:3)

这很有趣!怎么样:

def nonconsecutive_combinations(r, n):
  # first combination, startng at 1, step-size 2
  combination = range(1, r*2, 2)
  # as long as all items are less than or equal to n
  while combination[r-1] <= n:
    yield tuple(combination)
    p = r-1 # pointer to the element we will increment
    a = 1   # number that will be added to the last element
    # find the rightmost element we can increment without
    # making the last element bigger than n
    while p > 0 and combination[p] + a > n:
      p -= 1
      a += 2
    # increment the item and
    # fill the tail of the list with increments of two
    combination[p:] = range(combination[p]+1, combination[p] + 2*(r-p), 2)

每个next()调用应该有一个O(r).. 我在考虑如何将其转化为自然数字时得到了这个想法,但是花了相当长的时间来确定细节。

> list(nonconsecutive_combinations(2, 4))
[(1, 3), (1, 4), (2, 4)]
> list(nonconsecutive_combinations(3, 6))
[(1, 3, 5), (1, 3, 6), (1, 4, 6), (2, 4, 6)]

让我试着解释一下它是如何工作的。

具有c元素的元组r的条件是结果集的一部分:

  1. 元组的任何元素至少与前面的元素加2一样大。 (c[x] >= c[x-1] + 2
  2. 所有元素均小于或等于n。 因为1.足以说最后一个元素小于 或等于n。 (c[r-1] <= n
  3. 可能成为结果集一部分的最小元组是(1, 3, 5, ..., 2*r-1)。 当我说一个元组比另一个更“小”时,我假设了字典顺序。

    正如Blckknght指出的那样,即使是最小的元组也可能很大,以满足条件2。

    上面的函数包含两个while循环:

    • 外部循环逐步执行结果并假设它们以字典顺序出现并满足条件1。一旦有问题的元组违反条件二,我们就知道我们已经用尽了结果集并完成了:

      combination = range(1, r*2, 2)
      while combination[r-1] <= n:
      

      第一行根据条件1用第一个可能的结果初始化结果元组。第二行正好转化为条件二。

    • 内循环找到满足条件1的下一个可能元组。

      yield tuple(combination)
      

      由于while条件(2)为真,我们确保结果满足条件1,我们可以得到当前的结果元组。

      接下来,为了找到按字典顺序排列的下一个元组,我们将在最后一个元素中添加“1”。

      # we don't actually do this:
      combination[r-1] += 1
      

      然而,这可能会过早地破坏条件2。所以, if 该操作会破坏条件2,我们增加前面的元素并相应地调整最后一个元素。这有点像计算整数基数10:“如果最后一个数字大于9,则增加前一个数字并使最后一个数字为0”。但是我们不是添加零,而是填充元组,以便条件1为真。

      # if above does not work
      combination[r-2] += 1
      combination[r-1]  = combination[r-2] + 2
      

      问题是,第二行可能再次破坏条件二。所以我们实际做的是,我们跟踪最后一个元素,这就是a所做的事情。我们还使用p变量来引用我们正在查看的索引当前元素。

      p = r-1
      a = 1
      while p > 0 and combination[p] + a > n:
        p -= 1
        a += 2
      

      我们通过结果元组的项目从右向左迭代(p = r-1,p - = 1)。 最初我们想要在最后一个项目(a = 1)中添加一个,但是当单步执行元组时,我们实际上想要将前一个项目的值替换为2*x,其中xcombination[p:] = range(combination[p]+1, combination[p] + 2*(r-p), 2) 这两个项目之间的距离。 (a + = 2,组合[p] + a)

      最后,我们找到了要增加的项目,并使用从递增项开始的序列填充元组的其余部分,步长为2:

      {{1}}

      就是这样。当我第一次想到它时,它似乎很容易,但整个函数中的所有算术都为逐个错误提供了一个很好的位置,并且描述它比它应该更难。当我添加内循环时,我应该知道我遇到了麻烦:)

    关于表现..

    不幸的是,填充算术的循环不是用Python编写的最有效的东西。其他解决方案接受这种现实并使用列表推导或过滤来将繁重的工作推向Python运行时。在我看来,正确的事情

    另一方面,我非常肯定我的解决方案会比大多数情况好得多,如果这是C.内部while循环是O(log r)并且它将结果变异并且相同的O (log r)。除了结果和两个变量之外,它不消耗额外的堆栈帧并且不消耗任何内存。但显然这不是C,所以这一切都不重要。

答案 2 :(得分:2)

如果有办法在O(1)中生成所有组合,你可以通过生成和过滤在O(r)中完成。假设itertools.combinations有一个O(1)next,最多可以跳过r-1个值,所以最坏的情况是r-1乘以O(1),对吗?

向前跳了一下以避免混淆,我认为combinations没有O(1)实现,因此不是 O(r)。但是否有可能吗?我不确定。总之...

所以:

def nonconsecutive_combinations(p, r):
    def good(combo):
        return not any(combo[i]+1 == combo[i+1] for i in range(len(combo)-1))
    return filter(good, itertools.combinations(p, r))

r, n = 2, 4
print(list(nonconsecutive_combinations(range(1, n+1), r))

打印:

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

itertools文档并不保证combinations具有O(1)next。但在我看来,如果有可能的O(1)算法,他们会使用它,如果没有,你就不会找到它。

你可以阅读the source code,或者我们可以计算时间......但我们会这样做,让我们把时间花在整个事情上。

http://pastebin.com/ydk1TMbD有我的代码,thkang的代码和测试驱动程序。它打印的时间是迭代整个序列的成本除以序列的长度。

n范围为4到20,r固定为2,我们可以看到两者的时间都下降了。 (请记住,迭代序列的时间当然会上升。它只是the total length中的次线性)n范围从7到20和r固定在4,同样如此。

n固定为12,r固定为2到5,两者的时间从2到5线性上升,但是它们比1和6高得多期望的。

经过反思,这是有道理的 - 在924中只有6个好的值,对吧?这就是为什么每next的时间随着n的增加而下降的原因。总时间在增加,但产生的值的数量上升得更快。

因此,combinations没有O(1)next;它所拥有的东西是复杂的。我的算法没有O(r)next;它也很复杂。我认为在整个迭代中,性能保证要比每next更容易指定(如果您知道如何计算,则很容易除以值的数量)。

无论如何,我测试的两种算法的性能特征完全相同。 (奇怪的是,将包装器return切换到yield from会使递归更快,过滤速度更慢......但无论如何它都是一个小的常数因素,所以谁关心?)

答案 3 :(得分:2)

这是我对递归生成器的尝试:

def combinations_nonseq(r, n):
    if r == 0:
        yield ()
        return

    for v in range(2*r-1, n+1):
        for c in combinations_nonseq(r-1, v-2):
            yield c + (v,)

这与thkang的递归生成器大致相同,但它具有更好的性能。如果n接近r*2-1,则改进非常大,而对于较小的r值(相对于n),这是一个很小的改进。它也比svckr的代码好一点,没有与nr值的明确连接。

我的主要观点是当n小于2*r-1时,可能没有没有相邻值的组合。这让我的发电机比thkang更少工作。

这是一些时间,使用thkang的test代码的修改版本运行。它使用timeit模块来确定消耗生成器的整个内容十次所需的时间。 #列显示了我的代码产生的值的数量(我很确定所有其他值都相同)。

( n, r)      # |abarnert |  thkang |   svckr |BlckKnght| Knoothe |JFSebastian
===============+=========+=========+=========+=========+=========+========
(16, 2)    105 |  0.0037 |  0.0026 |  0.0064 |  0.0017 |  0.0047 |  0.0020
(16, 4)    715 |  0.0479 |  0.0181 |  0.0281 |  0.0117 |  0.0215 |  0.0143
(16, 6)    462 |  0.2034 |  0.0400 |  0.0191 |  0.0125 |  0.0153 |  0.0361
(16, 8)      9 |  0.3158 |  0.0410 |  0.0005 |  0.0006 |  0.0004 |  0.0401
===============+=========+=========+=========+=========+=========+========
(24, 2)    253 |  0.0054 |  0.0037 |  0.0097 |  0.0022 |  0.0069 |  0.0026
(24, 4)   5985 |  0.2703 |  0.1131 |  0.2337 |  0.0835 |  0.1772 |  0.0811
(24, 6)  27132 |  3.6876 |  0.8047 |  1.0896 |  0.5517 |  0.8852 |  0.6374
(24, 8)  24310 | 19.7518 |  1.7545 |  1.0015 |  0.7019 |  0.8387 |  1.5343

对于较大的n值,abarnert的代码花了太长时间,所以我从下一次测试中删除了它:

( n, r)      # |  thkang |   svckr |BlckKnght| Knoothe |JFSebastian
===============+=========+=========+=========+=========+========
(32, 2)    465 |  0.0069 |  0.0178 |  0.0040 |  0.0127 |  0.0064
(32, 4)  23751 |  0.4156 |  0.9330 |  0.3176 |  0.7068 |  0.2766
(32, 6) 296010 |  7.1074 | 11.8937 |  5.6699 |  9.7678 |  4.9028
(32, 8)1081575 | 37.8419 | 44.5834 | 27.6628 | 37.7919 | 28.4133

我一直在测试的代码是here

答案 4 :(得分:1)

这是一个类似于@thkang's answer但具有显式堆栈的解决方案:

def combinations_stack(seq, r):
    stack = [(0, r, ())]
    while stack:
        j, r, prev = stack.pop()
        if r == 0:
            yield prev
        else:
            for i in range(len(seq)-1, j-1, -1):
                stack.append((i+2, r-1, prev + (seq[i],)))

示例:

print(list(combinations_stack(range(1, 4+1), 2)))
# -> [(1, 3), (1, 4), (2, 4)]

对于某些(n, r)值,根据我机器上的the benchmark,这是最快的解决方案:

name                                time ratio comment
combinations_knoothe           17.4 usec  1.00 8 4
combinations_blckknght         17.9 usec  1.03 8 4
combinations_svckr             20.1 usec  1.16 8 4
combinations_stack             62.4 usec  3.59 8 4
combinations_thkang            69.6 usec  4.00 8 4
combinations_abarnert           123 usec  7.05 8 4
name                                time ratio comment
combinations_stack              965 usec  1.00 16 4
combinations_blckknght         1e+03 usec  1.04 16 4
combinations_knoothe           1.62 msec  1.68 16 4
combinations_thkang            1.64 msec  1.70 16 4
combinations_svckr             1.84 msec  1.90 16 4
combinations_abarnert           3.3 msec  3.42 16 4
name                                time ratio comment
combinations_stack               18 msec  1.00 32 4
combinations_blckknght         28.1 msec  1.56 32 4
combinations_thkang            40.4 msec  2.25 32 4
combinations_knoothe           53.3 msec  2.96 32 4
combinations_svckr             59.8 msec  3.32 32 4
combinations_abarnert          68.3 msec  3.79 32 4
name                                time ratio comment
combinations_stack             1.84  sec  1.00 32 8
combinations_blckknght         2.27  sec  1.24 32 8
combinations_svckr             2.83  sec  1.54 32 8
combinations_knoothe           3.08  sec  1.68 32 8
combinations_thkang            3.29  sec  1.79 32 8
combinations_abarnert            22  sec 11.99 32 8

其中combinations_knoothe是问题中描述的算法的实现:

import itertools
from itertools import imap as map

def _combinations_knoothe(n, r):
    def y2x(y):
        """
        y_1 = x_1
        y_2 = x_2 - 1
        y_3 = x_3 - 2
        ...
        y_r = x_r - (r-1)
        """
        return tuple(yy + i for i, yy in enumerate(y))
    return map(y2x, itertools.combinations(range(1, n-r+1+1), r))

def combinations_knoothe(seq, r):
    assert seq == list(range(1, len(seq)+1))
    return _combinations_knoothe(len(seq), r)

和其他功能来自相应的答案(修改为接受统一格式的输入)。