生成非连续组合
Posted
技术标签:
【中文标题】生成非连续组合【英文标题】:Generating non-consecutive combinations 【发布时间】:2013-03-03 13:07:06 【问题描述】:我正在尝试创建一个生成器(支持执行下一个操作的迭代器,可能在 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 choose r) 的生成。
就我们的目的而言,在事物生成之后使用映射就足够了。
选择svkcr答案的原因
所有很好的答案,但我选择了 svkcr 的答案。
以下是一些原因
它实际上是无状态的(或者更准确地说是“马尔可夫”)。下一个排列可以从前一个排列生成。它在某种程度上几乎是最优的:O(r) 空间和时间。
这是可以预见的。我们确切地知道生成组合的顺序(字典顺序)。
这两个属性使生成的并行化变得容易(在可预测的点拆分和委托),并引入了容错(如果 CPU/机器出现故障,可以从最后生成的组合中挑选出来)!
抱歉,前面没有提到并行化,因为我在写这个问题时没有想到它,后来才知道。
【问题讨论】:
生成和过滤不会是 O(n) 吗?或者,实际上,O(r)?从 1 到 r 的每个槽只有一个非法值,因此最多可以跳过 (r-1) 个组合。 PS,“生成所有组合(作为迭代器)”是与itertools
的单行。
@abarnert:可能是 Omega(nr)(或更糟),不是吗?感谢您提供有关 itertools 的提示。
怎么会是nr?我们中的一个人需要仔细考虑这一点。但请记住,您可以假设组合按排序顺序到达。
@abarnert:底层生成器可能是 Omega(n),所以如果你跳过 r 次调用,你就有了 Omega(nr)。不过,您是对的,需要(由我)给予更多思考。
【参考方案1】:
这很有趣!这个怎么样:
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
元素成为结果集一部分的条件:
-
元组的任何元素至少与前一个元素加 2 一样大。
(
c[x] >= c[x-1] + 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),但是当单步执行元组时,我们实际上希望将最后一项替换为前一项的值加上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 (日志 r)。它不消耗额外的堆栈帧,并且除了结果和两个变量之外不消耗任何内存。但显然这不是 C,所以这些都不重要。
【讨论】:
感谢您的回答。你能用英语解释一下你的算法吗? 我尽力了;希望这对您有所帮助! 感谢您的解释。你也可以修改它来获得组合:-)!【参考方案2】:这是我的递归生成器(如果选择了n
th 项,它只会跳过n+1
th 项):
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
,调用堆栈的成本可能比非递归生成更昂贵。有足够的n
和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
如您所见,随着。n
的上升,递归解决方案与基于 itertools.combination 的解决方案之间的差距越来越大
事实上,两种解决方案之间的差距在很大程度上取决于r
- 更大的r
意味着您必须丢弃更多从itertools.combinations
生成的组合。例如,在n=20, r=9
的情况下:我们过滤并在 167960(20C9) 个组合中仅取 220 个组合。如果n
和r
很小,使用itertools.combinations
可以更快,因为它在更少的r 下更有效,并且不使用堆栈,正如我解释的那样。 (如您所见,itertools 非常优化(如果使用for
、if
、while
和一堆生成器和列表推导编写您的逻辑,它不会像使用 itertools 抽象的那样快),这也是人们喜欢 python 的原因之一——你把你的代码带到更高的水平,你会得到回报。这方面的语言并不多。
【讨论】:
这显然不是next
调用中的 O(1)。准确计算它是什么(就n
和r
而言)有点困难……但好消息是它似乎与filtered-itertools解决方案具有相同的复杂性,所以如果我们弄清楚如何计算一个,我们就知道另一个的答案。
PS,为什么[each for each in foo]
而不仅仅是list(foo)
?
@abarnert 我从for each in foo: ..
重写了它们;只是添加一些括号意味着更少的打字。
@thkang:好的,但是,list(foo)
的输入比什么都不做的列表理解更多?
@abarnert 都是因为我的懒惰。将所有代码编辑为list(generator())
,而不是无意义的列表理解【参考方案3】:
如果有一种方法可以在 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
使得递归的更快,过滤的更慢......但无论如何它是一个很小的常数因素,所以谁在乎呢?)
【讨论】:
谢谢,我需要尝试一下,并更多地考虑您的主张。实际上,既然我看到了您的优点,即使底层生成器是 O(1),它也可能是 Omega(r^2)(因此是 Omega(n^2)),不是吗?不过,需要更多的想法(来自我)。 @Knoothe:如果我使用timeit
来演示时间如何随 n 和 r 增长,会有所帮助吗?
我将在我的工作负载上进行尝试,但我确实需要尽可能快地完成它。我目前的解决方案是基于过滤的,但根据我所做的性能测量,这似乎还不够。
我认为这不可能是每个next()
的O(r)
,因为从itertools
的总组合中识别出连续组合。
@thkang:我不确定这个解释是什么意思,但如果你通读全文,我很清楚它实际上不是 O(r),事实上它显然完全与您的代码相同的复杂性(但我不确定那是什么)。【参考方案4】:
这是我对递归生成器的尝试:
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 的代码好一点,与 n
或 r
值没有明确的联系。
我得到的关键见解是,当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。
【讨论】:
您能否从my answer 运行combinations_stack()
和combinations_knoothe()
的基准测试?
@J.F.Sebastian:我正在玩弄它,但我得到的答案不同,具体取决于我是否将创建输入列表所需的时间包含在函数运行时的一部分中。有趣的是,在 Python 3 上,您还可以在 range
对象上运行代码(range
s 可以切片),但性能比使用列表差。一旦我弄清楚如何解决这个问题,我将用新的东西替换上面的大时间转储。
你可以使用包装器:f = lambda r, n: combinations_stack(list(range(1, n+1)), r)
。或将seq[i]
替换为i+1
。结果中有(n-r+1)! / r! / (n-2r+1)!
项,所以除非n
很小;初始 O(n) 设置无关紧要。
我选择了 svkcr 的答案,并用原因更新了问题。谢谢!【参考方案5】:
这是一个类似于 @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)
其他函数来自对应的答案(修改为接受统一格式的输入)。
【讨论】:
我选择了 svkcr 的答案,并用原因更新了问题。谢谢!以上是关于生成非连续组合的主要内容,如果未能解决你的问题,请参考以下文章