概述
关键在于从累积的组合中走过,直到达到索引。
解决方案
from math import factorial
def comb(n, r):
'Combinations of n things taken r at a time'
return factorial(n) // (factorial(r) * factorial(n - r))
def nth_combination(population, r, index):
'Equivalent to list(combinations(population, r))[index]'
n = len(population)
c = comb(n, r)
if index < 0:
index += c
if index < 0 or index >= c:
raise IndexError
if r < 0 or r > n:
raise ValueError
result = []
while r:
c, n, r = c*r//n, n-1, r-1
while index >= c:
index -= c
c, n = c*(n-r)//n, n-1
result.append(population[-1-n])
return tuple(result)
优化
如果速度是一个问题,可以构建comb()函数的更快版本。
其中一种方法是预先计算阶乘,然后在需要时查找它们:
fact = [1]
for i in range(1, 1000):
fact.append(fact[-1] * i)
def comb(n, r):
return fact[n] // (fact[r] * fact[n - r])
还有一种避免使用大阶乘并且不需要辅助存储的方法:
def comb(n, r):
c = 1
r = min(r, n-r)
for i in range(1, r+1):
c = c * (n - r + i) // i
return c
它是如何工作的
首先将组合拆分为其组成部分:
def groups(n, r):
return [comb(n-i-1, r-1) for i in range(n-r+1)]
>>> comb(8, 3)
56
>>> groups(8, 3)
[21, 15, 10, 6, 3, 1]
这意味着当你对长度为8的字符串'ABCDEFGH'进行
itertools.combinations('ABCDEFGH', 3)
组合运算时,每次取出
r=3
个字母,会有56种组合方式。前21个以
A
开头,接下来的15个以
B
开头,再接下来的10个以
C
开头,之后的6个以
D
开头,再之后的3个以
E
开头,最后1个以
F
开头。
假设你想找到56种组合中的第25种,那么它属于第二组,所以第一个字母是
B
。
由于25-21等于4,所以你需要在
itertools.combinations('CDEFGH', 2)
定义的"B"组的15个成员中找到第4个组合。只需重复以上过程,直到提取完所有字母。
测试
以下是一个测试,用于确保它给出了预期结果:
from itertools import combinations
population = 'ABCDEFGH'
for r in range(len(population) + 1):
for i, expected in enumerate(combinations(population, r)):
actual = locate(list(population), r, i)
assert expected == actual