【问题标题】:Iterative Permutations of Distribution分布的迭代排列
【发布时间】:2018-06-06 22:10:27
【问题描述】:

我正在尝试生成各种分布的所有可能组合。

例如,假设您可以在 4 个类别上花费 5 分,但您最多只能在任何给定类别上花费 2 分。 在这种情况下,所有可能的解决方案如下:

[0, 1, 2, 2]
[0, 2, 1, 2]
[0, 2, 2, 1]
[1, 0, 2, 2]
[1, 1, 1, 2]
[1, 1, 2, 1]
[1, 2, 0, 2]
[1, 2, 1, 1]
[1, 2, 2, 0]
[2, 0, 1, 2]
[2, 0, 2, 1]
[2, 1, 0, 2]
[2, 1, 1, 1]
[2, 1, 2, 0]
[2, 2, 0, 1]
[2, 2, 1, 0]

我已经成功地制作了一个递归函数来实现这一点,但是对于大量的类别,它需要很长时间才能生成。我曾尝试创建一个迭代函数,以希望加快它的速度,但我似乎无法让它考虑到类别最大值。

这是我的递归函数(count = 点,dist = 零填充数组,大小与 max_allo 相同)

def distribute_recursive(count, max_allo, dist, depth=0):
    for ration in range(max(count - sum(max_allo[depth + 1:]), 0), min(count, max_allo[depth]) + 1):
        dist[depth] = ration
        count -= ration
        if depth + 1 < len(dist):
            distribute_recursive(count, max_allo, dist, depth + 1)
        else:
            print(dist)
        count += ration

【问题讨论】:

标签: python recursion iteration permutation distribution


【解决方案1】:

递归并不慢

递归并不是让它变慢的原因;考虑一个更好的算法

def dist (count, limit, points, acc = []):
  if count is 0:
    if sum (acc) is points:
      yield acc
  else:
    for x in range (limit + 1):
      yield from dist (count - 1, limit, points, acc + [x])

您可以将生成的结果收集到一个列表中

print (list (dist (count = 4, limit = 2, points = 5)))

修剪无效组合

在上面,我们使用limit + 1 的固定范围,但请注意如果我们生成与(例如)limit = 2points = 5 的组合会发生什么...

[ 2, ... ]    # 3 points remaining
[ 2, 2, ... ] # 1 point remaining

此时,使用limit + 1 ([ 0, 1, 2 ]) 的固定范围是愚蠢的,因为我们知道我们只剩下 1 点可花费。这里剩下的唯一选项是01...

[ 2, 2, 1 ... ] # 0 points remaining

以上我们知道我们可以使用[ 0 ] 的空范围,因为没有剩余积分可以花费。这将阻止我们尝试验证组合,例如

[ 2, 2, 2, ... ] # -1 points remaining
[ 2, 2, 2, 0, ... ] # -1 points remaining
[ 2, 2, 2, 1, ... ] # -2 points remaining
[ 2, 2, 2, 2, ... ] # -3 points remaining

如果count 非常大,这可以排除大量无效组合

[ 2, 2, 2, 2, 2, 2, 2, 2, 2, ... ] # -15 points remaining 

为了实现这种优化,我们可以在dist 函数中再添加一个参数,但是在 5 个参数时,它会开始看起来很乱。相反,我们引入了一个辅助函数来控制loop。添加我们的优化后,我们将固定范围换成min (limit, remaining) + 1动态 范围。最后,由于我们知道分配了多少点,我们不再需要测试每个组合的sum;从我们的算法中删除了另一个昂贵的操作

# revision: prune invalid combinations
def dist (count, limit, points):
  def loop (count, remaining, acc):
    if count is 0:
      if remaining is 0:
        yield acc
    else:
      for x in range (min (limit, remaining) + 1):
        yield from loop (count - 1, remaining - x, acc + [x])
  yield from loop (count, points, [])

基准测试

在下面的基准测试中,我们程序的第一个版本被重命名为dist1,更快的程序使用动态范围dist2。我们设置了三个测试,smallmediumlarge

def small (prg):
  return list (prg (count = 4, limit = 2, points = 5))

def medium (prg):
  return list (prg (count = 8, limit = 3, points = 7))

def large (prg):
  return list (prg (count = 16, limit = 5, points = 10))

现在我们运行测试,将每个程序作为参数传递。 large 测试的注意事项,只有 1 次通过,因为 dist1 需要一段时间才能生成结果

print (timeit ('small (dist1)', number = 10000, globals = globals ()))
print (timeit ('small (dist2)', number = 10000, globals = globals ()))

print (timeit ('medium (dist1)', number = 100, globals = globals ()))
print (timeit ('medium (dist2)', number = 100, globals = globals ()))

print (timeit ('large (dist1)', number = 1, globals = globals ()))
print (timeit ('large (dist2)', number = 1, globals = globals ()))

small 测试的结果表明,修剪无效组合不会产生太大影响。但是在mediumlarge 的情况下,差异是巨大的。我们的旧程序需要 30 多分钟才能完成大型集,但使用新程序只需 1 秒多一点!

dist1 small      0.8512216459494084
dist2 small      0.8610155049245805   (0.98x speed-up)

dist1 medium     6.142372329952195
dist2 medium     0.9355670949444175   (6.57x speed-up)

dist1 large   1933.0877765258774
dist2 large      1.4107366011012346   (1370.26x speed-up)

为了参考,每个结果的大小都打印在下面

print (len (small (dist2)))   # 16      (this is the example in your question)
print (len (medium (dist2)))  # 2472
print (len (large (dist2)))   # 336336

检查我们的理解

count = 12limit = 5large 基准测试中,使用我们未优化的程序,我们迭代了 512 或 244,140,​​625 个可能的组合。使用我们优化的程序,我们跳过所有无效组合,从而产生 336,336 个有效答案。通过单独分析组合计数,我们发现 99.86% 的可能组合是无效的。如果对每个组合的分析花费相同的时间,由于无效的组合修剪,我们可以预期优化后的程序的性能至少提高 725.88 倍。

large 基准测试中,测得的速度提高了 1370.26 倍,优化后的程序符合我们的预期,甚至超出了预期。额外的加速可能是由于我们取消了对sum的调用

huuuuge

为了证明这种技术适用于超大型数据集,请考虑huge 基准。我们的程序在 716 或 33,232,930,569,601 种可能性中找到 17,321,844 个有效组合。

在此测试中,我们优化的程序修剪了 99.99479% 的无效组合。将这些数字与之前的数据集相关联,我们估计优化后的程序运行速度比未优化版本快 1,918,556.16 倍。

使用未优化程序的该基准测试的理论运行时间为117.60 年。优化后的程序只需 1 多分钟就能找到答案。

def huge (prg):
  return list (prg (count = 16, limit = 7, points = 12))

print (timeit ('huge (dist2)', number = 1, globals = globals ()))
# 68.06868170504458

print (len (huge (dist2)))
# 17321844

【讨论】:

    【解决方案2】:

    您可以使用生成器函数进行递归,同时应用额外的逻辑来减少所需的递归调用次数:

    def listings(_cat, points, _max, current = []):
       if len(current) == _cat:
          yield current
       else:
          for i in range(_max+1):
            if sum(current+[i]) <= points:
              if sum(current+[i]) == points or len(current+[i]) < _cat:
                 yield from listings(_cat, points, _max, current+[i])
    
    
    print(list(listings(4, 5, 2)))
    

    输出:

    [[0, 1, 2, 2], [0, 2, 1, 2], [0, 2, 2, 1], [1, 0, 2, 2], [1, 1, 1, 2], [1, 1, 2, 1], [1, 2, 0, 2], [1, 2, 1, 1], [1, 2, 2, 0], [2, 0, 1, 2], [2, 0, 2, 1], [2, 1, 0, 2], [2, 1, 1, 1], [2, 1, 2, 0], [2, 2, 0, 1], [2, 2, 1, 0]]
    

    虽然不清楚您的解决方案在多大的类别大小上会显着减慢速度,但对于不超过 24 的类别大小,此解决方案将在 1 秒内运行,搜索总共 5 个点,最大槽值为 2。请注意,对于较大的点和槽值,在一秒钟内计算的可能类别大小的数量会增加:

    import time
    
    def timeit(f):
       def wrapper(*args):
         c = time.time()
         _ = f(*args)
         return time.time() - c
       return wrapper
    
    @timeit
    def wrap_calls(category_size:int) -> float:
      _ = list(listings(category_size, 5, 2))
    
    benchmark = 0
    category_size = 1
    while benchmark < 1:
       benchmark = wrap_calls(category_size)
       category_size += 1
    
    print(category_size)
    

    输出:

    24
    

    【讨论】:

    • dangit,我刚刚写完同样的答案,然后才读到你的答案。不开玩笑,几乎是completely identical:D
    • @user633183 我真的很喜欢你的解决方案,因为它比我的更干净。我鼓励你取消删除它:)
    • 谢谢,我让答案再次可见。虽然我只是有一个想法,它可能会加快速度。我会看看它是否有效......
    • Ajax1234,这个调整似乎产生了很大的影响。我创建了一个大型测试用例并最终走开了,因为我的第一个程序花了很长时间才完成它。我在更新我的答案时分享了结果:D
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-06-26
    • 2017-04-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多