如何按照它们的总和顺序迭代大量的整数元组?

时间:2013-02-13 22:56:33

标签: python iterator combinatorics itertools

我正在使用itertools.combinations()来迭代整数元组。

我对满足某些条件的最低金额的元组感兴趣:

def findLowestNiceTuple:
    for tup in itertools.combinations(range(1, 6), 2):
        if niceTuple(tup):
            return tup

生成器的默认顺序不是元素总和的顺序。例如:

>>> itertools.combinations(range(1, 6), 2)

给出一个生成器,它将产生以下元素:

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

如您所见,(1,5)的总和大于(2,3)的总和。为了提前终止,我需要按..., (1, 4), (2, 3), (1, 5), ...的顺序添加元组。

对于适度数量的组合,您可以使用sorted()

解决此问题
>>> sorted(itertools.combinations(range(1, 6), 2), key=sum)
[(1, 2), (1, 3), (1, 4), (2, 3), (1, 5), (2, 4), (2, 5), (3, 4), (3, 5), (4, 5)]

但是,sorted()将生成器转换为完全保留在内存中的列表。这意味着它不再能很好地扩展。像itertools.combinations(range(1, 600), 400)之类的东西将不可避免地产生MemoryError

是否有更友好的方式来达到预期效果?

PS:我确实意识到完全迭代我提到的最后一个序列需要很长时间,但我正在寻找的元组应该非常接近开始。如果我可以指望订单,我可以像第一个片段一样提前终止。

1 个答案:

答案 0 :(得分:3)

以下是我如何解决它,使用递归函数找到总和为给定值的所有组合:

def ordered_combinations(pop, n):
    pop = sorted(pop)

    for s in range(sum(pop[:n]), sum(pop[-n:])+1):
        yield from get_sums(pop, s, n)

def get_sums(pop, s, n):
    if n == 1:
        if s in pop:
            yield [s]
        return

    for i, v in enumerate(pop):
        if sum(pop[i:i+n]) > s:
            return
        for rest in get_sums(pop[i+1:], s-v, n-1):
            rest.append(v)
            yield rest

以下是输出的示例:

>>> for c in ordered_combinations(range(1, 8), 4):
    print(c, sum(c))


[4, 3, 2, 1] 10
[5, 3, 2, 1] 11
[6, 3, 2, 1] 12
[5, 4, 2, 1] 12
[7, 3, 2, 1] 13
[6, 4, 2, 1] 13
[5, 4, 3, 1] 13
[7, 4, 2, 1] 14
[6, 5, 2, 1] 14
[6, 4, 3, 1] 14
[5, 4, 3, 2] 14
[7, 5, 2, 1] 15
[7, 4, 3, 1] 15
[6, 5, 3, 1] 15
[6, 4, 3, 2] 15
[7, 6, 2, 1] 16
[7, 5, 3, 1] 16
[6, 5, 4, 1] 16
[7, 4, 3, 2] 16
[6, 5, 3, 2] 16
[7, 6, 3, 1] 17
[7, 5, 4, 1] 17
[7, 5, 3, 2] 17
[6, 5, 4, 2] 17
[7, 6, 4, 1] 18
[7, 6, 3, 2] 18
[7, 5, 4, 2] 18
[6, 5, 4, 3] 18
[7, 6, 5, 1] 19
[7, 6, 4, 2] 19
[7, 5, 4, 3] 19
[7, 6, 5, 2] 20
[7, 6, 4, 3] 20
[7, 6, 5, 3] 21
[7, 6, 5, 4] 22

组合总是首先产生最大值,作为我如何将它们构建为列表的工件(通过在末尾添加小值,而不是通过连接到前面)。如果您希望从最小到最大排序,可以将rest.append(v); yield rest行更改为yield [v]+rest

代码使用Python 3.3中引入的yield from语法。如果您使用的是不支持该版本的早期版本,则可以使用此等效代码:

for v in get_sums(pop, s, n):
    yield v

该代码甚至可以处理您描述的800个组合范围内的400种组合的极端情况。这是该计算的前20个结果(仅显示其最大的10个值,因为其余的都是相同的390到1),以及它们的总和:

>>> for i, v in enumerate(ordered_combinations(range(1, 800), 400)):
    if i >= 20:
        break
    print(v[:10], sum(v))


[400, 399, 398, 397, 396, 395, 394, 393, 392, 391] 80200
[401, 399, 398, 397, 396, 395, 394, 393, 392, 391] 80201
[402, 399, 398, 397, 396, 395, 394, 393, 392, 391] 80202
[401, 400, 398, 397, 396, 395, 394, 393, 392, 391] 80202
[403, 399, 398, 397, 396, 395, 394, 393, 392, 391] 80203
[402, 400, 398, 397, 396, 395, 394, 393, 392, 391] 80203
[401, 400, 399, 397, 396, 395, 394, 393, 392, 391] 80203
[404, 399, 398, 397, 396, 395, 394, 393, 392, 391] 80204
[403, 400, 398, 397, 396, 395, 394, 393, 392, 391] 80204
[402, 401, 398, 397, 396, 395, 394, 393, 392, 391] 80204
[402, 400, 399, 397, 396, 395, 394, 393, 392, 391] 80204
[401, 400, 399, 398, 396, 395, 394, 393, 392, 391] 80204
[405, 399, 398, 397, 396, 395, 394, 393, 392, 391] 80205
[404, 400, 398, 397, 396, 395, 394, 393, 392, 391] 80205
[403, 401, 398, 397, 396, 395, 394, 393, 392, 391] 80205
[403, 400, 399, 397, 396, 395, 394, 393, 392, 391] 80205
[402, 401, 399, 397, 396, 395, 394, 393, 392, 391] 80205
[402, 400, 399, 398, 396, 395, 394, 393, 392, 391] 80205
[401, 400, 399, 398, 397, 395, 394, 393, 392, 391] 80205
[406, 399, 398, 397, 396, 395, 394, 393, 392, 391] 80206

因为它是递归的,所以如果你请求1000组合,这段代码可能会失败(这是由于Python的默认递归限制)。如有必要,您可以使用sys.setrecursionlimit修改限制。

如果由于get_sums切片(并且因此复制)递归步骤中的总体而导致非常大的人群,它也可能存在内存问题。如果您对此代码的使用仅使用range,则可以通过从pop = sorted(pop)删除ordered_combinations行来解决内存问题,因为Python 3的range对象可以有效切片(即range(1,100)[10:]range(11,100))。