6

我正在尝试创建一个生成器(支持执行下一个操作的迭代器,可能在 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_r组合x_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. 这是可以预见的。我们确切地知道生成组合的顺序(字典顺序)。

这两个属性使生成并行化变得容易(在可预测的点拆分和委托),并引入了容错(如果 CPU/机器发生故障,可以从最后生成的组合中挑选出来)!

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

4

5 回答 5

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-item 组合。这意味着使用低r,调用堆栈的成本可能比非递归生成更昂贵。有了足够的nand r,它可能比基于 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 ivy 桥与 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——你把你的代码提升到更高的水平,你会得到回报。这方面的语言不多。

于 2013-03-14T23:13:38.190 回答
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)

可能是结果集一部分的最小元组是(1, 3, 5, ..., 2*r-1). 当我说一个元组比另一个元组“小”时,我假设的是字典顺序。

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

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

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

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

    第一行根据条件一使用第一个可能的结果初始化结果元组。第二行直接转换为条件二。

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

    yield tuple(combination)
    

    由于while条件 (2) 为真,并且我们确保结果满足条件一,我们可以产生当前的结果元组。

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

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

    但是,这可能会过早地打破条件 2。因此,如果该操作会破坏条件 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),但是当单步执行元组时,我们实际上希望将最后一项替换为前一项的值 plus 2*x,其中x是两项之间的距离。(a += 2, 组合[p] + a)

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

    combination[p:] = range(combination[p]+1, combination[p] + 2*(r-p), 2)
    

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

关于性能..

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

另一方面,我很确定如果这是 C,我的解决方案会比大多数解决方案执行得更好。内部 while 循环是 O(log r),它会就地改变结果并且相同的 O(log r )。它不消耗额外的堆栈帧,并且除了结果和两个变量之外不消耗任何内存。但显然这不是 C,所以这些都不重要。

于 2013-03-15T01:09:17.087 回答
2

如果有一种方法可以在 O(1) 中生成所有组合,那么您可以在 O(r) 中通过生成和过滤来做到这一点。假设itertools.combinations有一个 O(1) next,最多有 r-1 个值要跳过,所以最坏的情况是 r-1 乘以 O(1),对吧?

为了避免混淆,我认为没有 O(1) 的实现combinations,因此这不是O(r)。但是有什么可能?我不知道。总之……</p>

所以:

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) 算法,他们会使用它,如果没有,你就找不到。

你可以阅读源代码,或者我们可以给它计时……但我们会这样做,让我们为整个事情计时。

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

n4 到 20 并r固定为 2,我们可以看到两者的时间都在下降。(请记住,迭代序列的时间当然会增加。它只是 中的亚线性the total lengthn范围从 7 到 20 并r固定为 4,同样如此。

n固定为 12 且范围从r2 到 5,两者的时间都从 2 到 5 线性上升,但是 1 和 6 的时间比您预期的要高得多。

仔细想想,这是有道理的——924 个值中只有 6 个好值,对吧?这就是为什么每次时间next都在下降的原因n。总时间在增加,但产生的值的数量增加得更快。

所以,combinations没有 O(1) next;它确实有一些复杂的东西。而且我的算法没有 O(r) next;这也很复杂。我认为在整个迭代中指定性能保证会比每次更next容易(如果你知道如何计算,那么很容易除以值的数量)。

无论如何,我测试的两种算法的性能特征完全相同。(奇怪的是,切换包装器returnyield from使递归更快,过滤更慢......但无论如何它是一个小的常数因素,所以谁在乎呢?)

于 2013-03-14T23:17:40.123 回答
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 的少。

这是一些时间,使用 thkangtest代码的修改版本运行。它使用该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

我一直在测试的代码在这里

于 2013-03-15T06:18:02.027 回答
1

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

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)值,它是最快的解决方案:

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)

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

于 2013-03-15T07:06:42.170 回答