第 N 个组合

Posted

技术标签:

【中文标题】第 N 个组合【英文标题】:Nth Combination 【发布时间】:2010-12-19 02:04:22 【问题描述】:

有没有直接的方法得到 nCr 所有组合的有序集合的?

示例:我有四个元素:[6, 4, 2, 1]。一次取三个的所有可能组合是: [[6, 4, 2], [6, 4, 1], [6, 2, 1], [4, 2, 1]]。

有没有一种算法可以给我,例如第三个答案 [6, 2, 1],在有序结果集中,没有枚举所有先前的答案?

【问题讨论】:

你如何定义“订单”?假设您有序列 (1, 2, 3, 4, 5, 6, 7, 8, 9)。在一次取 4 个的所有可能组合中,第一个是什么?第二个呢? 输入将是一个有序的集合/数组,例如[n1,n2,n3,n4]。然后顺序将是数组中的索引。 忽略我之前的评论 :-).. 对于组合之间的顺序。我正在寻找的顺序是具有元素 n1 的组合已经比不具有元素 n1 的组合排序更高。然后对于 n2 等也是如此 因此,对于 nC3 组合,完整顺序为:[n1, n2, n3], [n1, n2, n4], [n1, n2, 1], ..., [n1 , 2, 1], [n2, n3, n4], ..., [3, 2, 1] 让我们再试一次: [ [i1, i2, i3] [i1, i2, i4] ... [i1, i2, in] [i1, i3, i4] ... .. . [i1, i(n-1), in] ... ... [i(n-2),i(n-1),in] ] 【参考方案1】:

请注意,您可以通过递归生成包含第一个元素的所有组合,然后生成所有不包含的组合来生成序列。在这两种递归情况下,您删除第一个元素以从 n-1 个元素中获取所有组合。在 Python 中:

def combination(l, r):
    if r == 0:
        yield []
    elif len(l) == r:
        yield l
    else:
        for c in (combination(l[1:], r-1)):
            yield l[0:1]+c
        for c in (combination(l[1:], r)):
            yield c

每当您通过这样的选择生成序列时,您都可以通过计算选择生成的元素数量并将计数与 k 进行比较来递归地生成第 kth 元素。如果 k 小于计数,则您做出该选择。否则,减去计数并重复您当时可以做出的其他可能选择。如果总是有b 选项,您可以将其视为在基数b 中生成一个数字。如果选择的数量不同,该技术仍然有效。在伪代码中(当所有选项都可用时):

kth(k, choicePoints)
    if choicePoints is empty
        return empty list
    for each choice in head of choicePoints:
        if k < size of choice
            return choice and kth(k, tail of choicePoints)
        else
            k -= size of choice
    signal exception: k is out-of-bounds

这为您提供了一个从 0 开始的索引。如果您想要基于 1,请将比较更改为 k &lt;= size of choice

棘手的部分(以及伪代码中未指定的部分)是选择的大小取决于先前的选择。请注意,伪代码可用于解决比问题更普遍的情况。

对于这个特定的问题,有两个选择(b= 2),第一个选择的大小(即包括第一个st元素)由n-1给出Cr-1。这是一个实现(需要合适的nCr):

def kthCombination(k, l, r):
    if r == 0:
        return []
    elif len(l) == r:
        return l
    else:
        i=nCr(len(l)-1, r-1)
        if k < i:
            return l[0:1] + kthCombination(k, l[1:], r-1)
        else:
            return kthCombination(k-i, l[1:], r)

如果你颠倒选择的顺序,你就会颠倒顺序。

def reverseKthCombination(k, l, r):
    if r == 0:
        return []
    elif len(l) == r:
        return l
    else:
        i=nCr(len(l)-1, r)
        if k < i:
            return reverseKthCombination(k, l[1:], r)
        else:
            return l[0:1] + reverseKthCombination(k-i, l[1:], r-1)

使用它:

>>> l = [6, 4, 2, 1]
>>> [kthCombination(k, [6, 4, 2, 1], 3) for k in range(nCr(len(l), 3)) ]
[[6, 4, 2], [6, 4, 1], [6, 2, 1], [4, 2, 1]]
>>> powOf2s=[2**i for i in range(4,-1,-1)]
>>> [sum(kthCombination(k, powOf2s, 3)) for k in range(nCr(len(powOf2s), 3))]
[28, 26, 25, 22, 21, 19, 14, 13, 11, 7]
>>> [sum(reverseKthCombination(k, powOf2s, 3)) for k in range(nCr(len(powOf2s), 3))]
[7, 11, 13, 14, 19, 21, 22, 25, 26, 28]

【讨论】:

【参考方案2】: TLDR?只需滚动到最底部即可查看我的最终解决方案

我在寻找方法来获取 index 一个指定的 combination 如果它在 字典排序 列出和反之亦然,用于从一些可能非常大量对象中选择对象,并且在后者中找不到太多内容(与您的相反)问题不是那么难以捉摸)。

因为我也解决了(我认为是)你的确切问题,然后我才想到我会在这里发布我的解决方案。

** 编辑:我的要求你的要求也是 - 我看到了答案并认为递归很好。好吧,经过六年的漫长岁月,你拥有了它;只需向下滚动即可。**

对于您在问题中提出的要求(我认为是),这可以很好地完成工作:

def iterCombinations(n, k):
if k==1:
    for i in range(n):
        yield [i]
    return
result = []
for a in range(k-1, n):
    for e in iterCombinations(n, k-1):
        if e[-1] == a:
            break
        yield e + [a]

然后,您可以在按降序排列的集合中查找该项目(或使用一些等效的比较方法),因此对于有问题的情况:

>>> itemsDescending = [6,4,2,1]
>>> for c in iterCombinations(4, 3):
...     [itemsDescending[i] for i in c]
...
[6, 4, 2]
[6, 4, 1]
[6, 2, 1]
[4, 2, 1]

这在 Python 中也可以直接使用,但是:

>>> import itertools
>>> for c in itertools.combinations(itemsDescending, 3):
...     c
...
(6, 4, 2)
(6, 4, 1)
(6, 2, 1)
(4, 2, 1)

这是我为我的要求(真的是为你的要求!)所做的,即 非递归 算法,它不会创建或遍历任一方向的有序列表,而是使用简单但nCr的有效非递归实现,choose(n, k):

def choose(n, k):
    '''Returns the number of ways to choose k items from n items'''
    reflect = n - k
    if k > reflect:
        if k > n:
            return 0
        k = reflect
    if k == 0:
        return 1
    for nMinusIPlus1, i in zip(range(n - 1, n - k, -1), range(2, k + 1)):
        n = n * nMinusIPlus1 // i
    return n

要在 forward 排序列表中的某个(从零开始的)索引处获取组合:

def iterCombination(index, n, k):
    '''Yields the items of the single combination that would be at the provided
    (0-based) index in a lexicographically sorted list of combinations of choices
    of k items from n items [0,n), given the combinations were sorted in 
    descending order. Yields in descending order.
    '''
    if index < 0 or index >= choose(n, k):
        return
    n -= 1
    for i in range(k):
        while choose(n, k) > index:
            n -= 1
        yield n
        index -= choose(n, k)
        n -= 1
        k -= 1

要获取某个组合将驻留在 reverse 有序列表中的(从零开始的)索引:

def indexOfCombination(combination):
    '''Returns the (0-based) index the given combination would have if it were in
    a reverse-lexicographically sorted list of combinations of choices of
    len(combination) items from any possible number of items (given the
    combination's length and maximum value)
   - combination must already be in descending order,
     and it's items drawn from the set [0,n).
    '''
    result = 0
    for i, a in enumerate(combination):
        result += choose(a, i + 1)
    return result

你的例子有点过头了(但我现在意识到这只是一个例子);这就是每个索引的顺序:

def exampleUseCase(itemsDescending=[6,4,2,1], k=3):
    n = len(itemsDescending)
    print("index -> combination -> and back again:")
    for i in range(choose(n, k)):
        c = [itemsDescending[j] for j in iterCombination(i, n, k)][-1::-1]
        index = indexOfCombination([itemsDescending.index(v) for v in c])
        print("0 -> 1 -> 2".format(i, c, index))

>>> exampleUseCase()
index -> combination -> and back again:
0 -> [6, 4, 2] -> 0
1 -> [6, 4, 1] -> 1
2 -> [6, 2, 1] -> 2
3 -> [4, 2, 1] -> 3

这可以在眨眼间中找到给定一些长列表的索引或返回某个天文索引处的组合,例如:

>>> choose(2016, 37)
9617597205504126094112265433349923026485628526002095715212972063686138242753600
>>> list(iterCombination(_-1, 2016, 37))
[2015, 2014, 2013, 2012, 2011, 2010, 2009, 2008, 2007, 2006, 2005, 2004, 2003,
2002, 2001, 2000, 1999, 1998, 1997, 1996, 1995, 1994, 1993, 1992, 1991, 1990, 1989,
1988, 1987, 1986, 1985, 1984, 1983, 1982, 1981, 1980, 1979]

或者,因为这是最后一个,并且由于选择(n,k)中的反射可能很快,所以这是从中间开始的一个,它似乎同样快......

>>> choose(2016, 37)//2
4808798602752063047056132716674961513242814263001047857606486031843069121376800
>>> list(iterCombination(_, 2016, 37))
[1978, 1973, 1921, 1908, 1825, 1775, 1747, 1635, 1613, 1598, 1529, 1528, 1521,
1445, 1393, 1251, 1247, 1229, 1204, 1198, 922, 901, 794, 699, 685, 633, 619, 598,
469, 456, 374, 368, 357, 219, 149, 93, 71]

最后一个例子会停下来思考片刻,但你不会吗?

>>> import random
>>> rSet = set(random.randint(0, 10000000) for i in range(900))
>>> len(rSet)
900
>>> rList = sorted(rSet, reverse=True)
>>> combinations.indexOfCombination(rList)
61536587905102303838316048492163850175478325236595592744487336325506086930974887
88085020093159925576117511028315621934208381981476407812702689774826510322023536
58905845549371069786639595263444239118366962232872361362581506476113967993096033
00541202874946853699568596881200225925266331936183173583581021914595163799417151
30442624813775945054888304722079206982972852037480516813527237183254850056012217
59834465303543702263588008387352235149083914737690225710105023486226582087736870
38383323140972279867697434315252036074490127510158752080225274972225311906715033
86851377357968649982293794242170046400174118714525559851836064661141086690326842
25236658978135989907667078625869419802333512020715700514133380517628637151215549
05922388534567108671308819960483147825031620798631811671493891643972220604919591
22785587505280326638477135315176731640100473359830821781905546117103137944239120
34912084544221250309244925308316352643060056100719194985568284049903555621750881
39419639825279398618630525081169688672242833238889454445237928356800414839702024
66807635358129606994342005075585962080795273287472139515994244684088406544976674
84183671032002497594936116837768233617073949894918741875863985858049825755901232
89317507965160689287607868119414903299382093412911433254998227245783454244894604
83654290108678890682359278892580855226717964180806265176337132759167920384512456
91624558534942279041452960272707049107641475225516294235268581475735143470692000
78400891862852130481822509803019636619427631175355448729708451565341764545325720
79277290914349746541071731127111532099038538549697091038496002102703737347343739
96398832832674081286904287066696046621691978697914823322322650123025472624927566
99891468668052668317066769517155581261265629289158798073055495539590686279250097
27295943276536772955923599217742543093669565147228386873469711200278811335649924
13587219640724942441913695193417732608127949738209466313175361161142601108707568
19470026889319648128790363676253707359290547393198350533094409863254710237344552
47692325209744353688541868412075798500629908908768438513508959321262250985142709
19794478379412756202638771417821781240327337108495689300616872374578607430951230
96908870723878513999404242546015617238957825116802801618973562178005776911079790
22026655573872019955677676783191505879571719659770550759779880002320421606755826
75809722478174545846409923210824885805972611279030267270741509747224602604003738
30411365119180944456819762167312738395140461035991994771968906979578667047734952
21981545694935313345331923300019842406900689401417602004228459137311983483386802
30352489602769346000257761959413965109940729263098747702427952104316612809425394
85037536245288888254374135695390839718978818689595231708490351927063849922772653
26064826999661128817511630298712833048667406916285156973335575847429111697259113
53969532522640227276562651123634766230804871160471143157687290382053412295542343
14022687833967461351170188107671919648640149202504369991478703293224727284508796
06843631262345918398240286430644564444566815901074110609701319038586170760771099
41252989796265436701638358088345892387619172572763571929093224171759199798290520
71975442996399826830220944004118266689537930602427572308646745061258472912222347
18088442198837834539211242627770833874751143136048704550494404981971932449150098
52555927020553995188323691320225317096340687798498057634440618188905647503384292
79493920419695886724506109053220167190536026635080266763647744881063220423654648
36855624855494077960732944499038847158715263413026604773216510801253044020991845
89652657529729792772055725210165026891724511953666038764273616212464901231675592
46950937136633665320781952510620087284589083139308516989522633786063418913473703
96532777760440118656525488729217328376766171004246127636983612583177565603918697
15557602015171235214344399010185766876727226408494760175957535995025356361689144
85181975631986409708533731043231896096597038345028523539733981468056497208027899
6245509252811753667386001506195

但是,从该索引返回到它所表示的 900-choose-10,000,000 与先前实现的组合会非常慢(因为它只是在每次迭代时从 n 中减去一个)。

对于如此大的组合列表,我们可以改为对空间进行二分搜索,而我们添加的开销意味着对于小的组合列表只会慢一点:

def iterCombination(index, n, k):
    '''Yields the items of the single combination that would be at the provided
    (0-based) index in a lexicographically sorted list of combinations of choices
    of k items from n items [0,n), given the combinations were sorted in 
    descending order. Yields in descending order.
    '''
    if index < 0 or n < k or n < 1 or k < 1 or choose(n, k) <= index:
        return
    for i in range(k, 0, -1):
        d = (n - i) // 2 or 1
        n -= d
        while 1:
            nCi = choose(n, i)
            while nCi > index:
                d = d // 2 or 1
                n -= d
                nCi = choose(n, i)
            if d == 1:
                break
            n += d
            d //= 2
            n -= d
        yield n
        index -= nCi

从此人们可能会注意到,所有对choose 的调用都有取消的条款,如果我们取消所有内容,我们最终会得到一个更快的实现,我认为是什么......

这个问题的最优函数

def iterCombination(index, n, k):
    '''Yields the items of the single combination that would be at the provided
    (0-based) index in a lexicographically sorted list of combinations of choices
    of k items from n items [0,n), given the combinations were sorted in 
    descending order. Yields in descending order.
    '''
    nCk = 1
    for nMinusI, iPlus1 in zip(range(n, n - k, -1), range(1, k + 1)):
        nCk *= nMinusI
        nCk //= iPlus1
    curIndex = nCk
    for k in range(k, 0, -1):
        nCk *= k
        nCk //= n
        while curIndex - nCk > index:
            curIndex -= nCk
            nCk *= (n - k)
            nCk -= nCk % k
            n -= 1
            nCk //= n
        n -= 1
        yield n

最后提醒一下,对于问题的用例,可以这样做:

def combinationAt(index, itemsDescending, k):
    return [itemsDescending[i] for i in
            list(iterCombination(index, len(itemsDescending), k))[-1::-1]]

>>> itemsDescending = [6,4,2,1]
>>> numberOfItemsBeingChosen = 3
>>> zeroBasedIndexWanted = 1
>>> combinationAt(zeroBasedIndexWanted, itemsDescending, numberOfItemsBeingChosen)
[6, 4, 1]

【讨论】:

不错。 Stanton 和 White 有一本老书《Constructive Combinatorics》,里面有很多这样的算法。例如 - 它具有用于生成排列的 Johnson-Trotter 算法(很容易找到)以及用于确定结果排序中排列的等级的算法(很难找到)。这本书使用了有点过时的类似 Pascal 的伪代码,但很容易将代码翻译成更现代的语言。 我必须找到一份副本!【参考方案3】:

一种方法是使用位的属性。这仍然需要一些枚举,但您不必枚举每个集合。

对于您的示例,您的集合中有 4 个数字。因此,如果您要生成 4 个数字的所有可能组合,则可以按如下方式枚举它们:

6、4、2、1 0000 - (集合中没有数字) 0001 - 1 0010 - 2 0011 - 2, 1 ... 1111 - 6, 4, 2, 1

查看每个“位”如何对应“该数字是否在您的集合中”?我们在这里看到有 16 种可能性 (2^4)。

所以现在我们可以遍历并找到仅打开 3 位的所有可能性。这将告诉我们所有存在的“3”组合:

0111 - 4, 2, 1 1011 - 6, 2, 1 1101 - 6, 4, 1 1110 - 6, 4, 2

让我们将每个二进制值重写为十进制值:

0111 = 7 1011 = 11 1101 = 13 1110 = 14

现在我们已经完成了 - 好吧,您说您想要“第 3 个”枚举。那么让我们看看第三大数字:11。它的位模式为 1011。它对应于... 6, 2, 1

酷!

基本上,您可以对任何集合使用相同的概念。所以现在我们所做的就是将问题从“枚举所有集合”转换为“枚举所有整数”。对于您的问题,这可能会容易得多。

【讨论】:

这只是列举了我要找的之前的所有组合,这正是我不想做的。这对于像 5C3 这样的玩具问题很有效,但是如果数字很大,你就有麻烦了。【参考方案4】:

来自 Python 3.6 itertools recipes:

def nth_combination(iterable, r, index):
    'Equivalent to list(combinations(iterable, r))[index]'
    pool = tuple(iterable)
    n = len(pool)
    if r < 0 or r > n:
        raise ValueError
    c = 1
    k = min(r, n-r)
    for i in range(1, k+1):
        c = c * (n - k + i) // i
    if index < 0:
        index += c
    if index < 0 or index >= c:
        raise IndexError
    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(pool[-1-n])
    return tuple(result)

在实践中:

iterable, r, index = [6, 4, 2, 1], 3, 2

nth_combination(iterable, r, index)
# (6, 2, 1)

或者,如文档字符串中所述:

import itertools as it


list(it.combinations(iterable, r))[index]
# (6, 2, 1)

另请参阅 more_itertools - 为您实现 this recipe 的第三方库。安装方式:

> pip install more_itertools

【讨论】:

【参考方案5】:

只是一个粗略的草图: 将您的数字排列成元组的上三角矩阵:

A(n-1,n-1)   
Aij = [i+1, j-1]

如果你先遍历矩阵行,你会得到两个元素的升序组合。要概括为三个元素,请将矩阵行视为另一个三角矩阵,而不是向量。它有点像创建立方体的一个角。

至少这是我解决问题的方法

让我澄清一下,您不必存储矩阵,您需要计算索引。 让我研究一下维度示例,您原则上可以扩展到 20 个维度(记账可能很糟糕)。

ij = (i*i + i)/2 + j // ij is also the combination number
(i,j) = decompose(ij) // from ij one can recover i,j components
I = i // actual first index
J = j + 1 // actual second index

这个二维示例适用于任何数字 n,您不必将排列列表化。

【讨论】:

抱歉,没有采用这种方法。比如说,我们会为 30C20 做这个,这需要 20 个维度,这真的不可能吗?【参考方案6】:

是的,有没有直接的方法来获得所有 nCr 组合的有序集合的第 N 个组合?假设您需要生成给定集合的第 0、第 3、第 6 .. 组合。您可以直接生成它,而无需在使用 JNuberTools 之间生成组合。您甚至可以生成下一个十亿组合(如果您的集合规模很大) 下面是代码示例:

JNumberTools.combinationsOf(list)
        .uniqueNth(8,1000_000_000) //skip to billionth combination of size 8
        .forEach(System.out::println);

JNumberTools 的 maven 依赖是:

<dependency>
    <groupId>io.github.deepeshpatel</groupId>
    <artifactId>jnumbertools</artifactId>
    <version>1.0.0</version>
</dependency>

【讨论】:

以上是关于第 N 个组合的主要内容,如果未能解决你的问题,请参考以下文章

组合函数

N个不同球取出M个的组合个数求解

组合数学/Prufer编码/Cayley公式

模板 - 数学 - 组合数学 - 第二类斯特林数

排列组合+组合数取模 HDU 5894

n中的k个组合[重复]