【问题标题】:Efficient enumeration of non-negative integer composition非负整数组成的高效枚举
【发布时间】:2019-09-07 01:44:39
【问题描述】:

我想写一个函数my_func(n,l),对于一些正整数n,有效地枚举长度为l的有序非负整数组合*(其中l大于n) .比如我想让my_func(2,3)返回[[0,0,2],[0,2,0],[2,0,0],[1,1,0],[1,0,1],[0,1,1]]

我最初的想法是将现有代码用于正整数分区(例如 this post 中的 accel_asc()),将正整数分区扩展几个零并返回所有排列。

def my_func(n, l):
    for ip in accel_asc(n):
        nic = numpy.zeros(l, dtype=int)
        nic[:len(ip)] = ip
        for p in itertools.permutations(nic):
            yield p

这个函数的输出是错误的,因为每个数字出现两次(或多次)的非负整数组合在my_func的输出中出现了多次。例如,list(my_func(2,3)) 返回[(1, 1, 0), (1, 0, 1), (1, 1, 0), (1, 0, 1), (0, 1, 1), (0, 1, 1), (2, 0, 0), (2, 0, 0), (0, 2, 0), (0, 0, 2), (0, 2, 0), (0, 0, 2)]

我可以通过生成所有非负整数组合的列表、删除重复条目、然后返回剩余列表(而不是生成器)来纠正此问题。但这似乎非常低效,并且可能会遇到内存问题。有什么更好的方法来解决这个问题?

编辑

我对这篇文章和 cglacet 在 cmets 中指出的 another post 的答案中提供的解决方案进行了快速比较。

左边是l=2*n,右边是l=n+1。在这两种情况下,user2357112 的第二个解决方案比其他解决方案更快,n<=5。对于n>5,user2357112、Nathan Verzemnieks 和 AndyP 提出的解决方案或多或少是相关的。但在考虑ln 之间的其他关系时,结论可能会有所不同。

.......

*我最初要求的是非负整数分区。 Joseph Wood 正确地指出,我实际上是在寻找整数组合,因为序列中数字的顺序对我很重要。

【问题讨论】:

  • @Green Cloak Guy 它仍然需要在生成第一个分区之前生成所有非负整数分区。 nl 的非负整数分区的数量增长得非常快,所以我真的很想避免这种情况。
  • @BadZen 该线程在 positive 整数分区上。我想要非负整数分区。
  • @AliceSchwarze:将正元素解应用于非负元素解的其他方法 - 将 n+l 划分为正整数并从划分元素中减去 1。当然,这并不能解决更深层次的多集/元组问题。
  • 对于它的价值,我刚刚测试了@cglacet 提到的问题中的所有解决方案,但没有一个比这里的两个更快的解决方案快,尽管this one 很接近。
  • IMO 值得一提,我不知道 Stackoverflow 在这种情况下应该如何工作,但也许最好的解决方案是合并这两个问题(因为这两个问题完全相同,除非我遗漏了什么)。

标签: python python-3.x numpy permutation combinatorics


【解决方案1】:

使用stars and bars 概念:选择在n 星之间放置l-1 条的位置,并计算每个部分中有多少星:

import itertools

def diff(seq):
    return [seq[i+1] - seq[i] for i in range(len(seq)-1)]

def generator(n, l):
    for combination in itertools.combinations_with_replacement(range(n+1), l-1):
        yield [combination[0]] + diff(combination) + [n-combination[-1]]

我在这里使用了combinations_with_replacement 而不是combinations,因此索引处理与combinations 所需要的有点不同。带有combinations 的代码更符合标准的星形和条形处理方式。


另外,使用combinations_with_replacement 的另一种方式:从l 零的列表开始,从l 可能的位置中选择n 位置并替换,然后将每个选定位置加1 以产生输出:

def generator2(n, l):
    for combination in itertools.combinations_with_replacement(range(l), n):
        output = [0]*l
        for i in combination:
            output[i] += 1
        yield output

【讨论】:

  • 哦,这样写的真好!
  • 哦,我很喜欢generator2——非常聪明。而且它几乎和我的一样快 - 在大多数情况下可能足够接近,不重要。
  • 更正 - 在某些情况下更快,但在 n > l 时尤其快。
【解决方案2】:

从一个简单的递归解决方案开始,它与您的问题相同:

def nn_partitions(n, l):
    if n == 0:
        yield [0] * l
    else:
        for part in nn_partitions(n - 1, l):
            for i in range(l):
                new = list(part)
                new[i] += 1
                yield new

也就是说,对于下一个较小数字的每个分区,对于该分区中的每个位置,将该位置的元素加 1。它会产生与您相同的副本。不过,我记得一个类似问题的技巧:当您将n 的分区p 更改为n+1 的一个分区时,将p 的所有元素修复到您增加的元素的左侧。也就是说,跟踪p 的修改位置,并且永远不要修改p 左侧的任何“后代”。这是代码:

def _nn_partitions(n, l):
    if n == 0:
        yield [0] * l, 0
    else:
        for part, start in _nn_partitions(n - 1, l):
            for i in range(start, l):
                new = list(part)
                new[i] += 1
                yield new, i

def nn_partitions(n, l):
    for part, _ in _nn_partitions(n, l):
        yield part

非常相似——只是在每一步都传递了额外的参数,所以我添加了包装器来为调用者移除它。

我尚未对其进行广泛测试,但这似乎相当快 - nn_partitions(3, 5) 大约 35 微秒,nn_partitions(10, 20) 大约 18 秒(产生超过 2000 万个分区)。 (来自user2357112 的非常优雅的解决方案对于较小的情况大约需要两倍的时间,对于较大的情况大约需要四倍的时间。编辑:这是指该答案中的第一个解决方案;在某些情况下,第二个解决方案比我的要快在其他情况下速度较慢。)

【讨论】:

  • 这需要与n 的值成比例的递归深度,因此它不适用于nn_partitions(1000, 3) 之类的东西。
  • 发帖人指定l > n,所以在我们用完递归深度之前我们会用完时间long :-D
  • 新的非复制方法可能会节省时间,但这也意味着调用者无法在不手动复制的情况下存储或修改生成的列表对象。这将不可避免地成为错误的主要来源,即使已明确记录。
  • Nathan,您在第一个代码块中恢复了您的编辑,但在第二个代码块中没有。不应该两者兼而有之吗?
  • 哎呀!是的,确实,道歉。我今天下午有点散。会修复的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多