【问题标题】:General bars and stars一般酒吧和明星
【发布时间】:2015-03-10 13:59:11
【问题描述】:

我有一个以下"bars and stars" 算法,用 Python 实现,它将所有的和分解打印到 3 个 bin 中,总和从 0 到 5。 我想概括我的代码,以便它适用于 N 个 bin(其中 N 小于最大总和,即此处为 5)。 模式是,如果您有 3 个 bin,则需要 2 个嵌套循环,如果您有 N 个 bin,则需要 N-1 个嵌套循环。

有人能想出一种通用的写法,可能不使用循环吗?

# bars and stars algorithm
N=5
for n in range(0,N):
    x=[1]*n
    for i in range(0,(len(x)+1)):
        for j in range(i,(len(x)+1)):
            print sum(x[0:i]), sum(x[i:j]), sum(x[j:len(x)])

【问题讨论】:

  • 这是一个实际问题还是只是一个脑筋急转弯?后者不适合 SO
  • @Tim:为什么不呢?如果 OP 有一个编程问题,我们为什么不尝试回答呢?
  • @TimCastelijns 这显然是一个问题:我想推广我的算法以使用 N 个 bin(目前它适用于三个)。
  • @Kevin 我认为 Tim 的意思是它可能在编程谜题和代码高尔夫之类的地方更热门。 OP——定义“无循环”——你的意思是不使用forwhile吗? sum 的本质需要某种迭代,即使从你的角度来看它只是在幕后。
  • 好吧,很公平。 here is a little brain teaser 让我有点困惑

标签: python algorithm


【解决方案1】:

如果这不仅仅是一个学习练习,那么您就没有必要使用自己的算法来生成分区:Python 的标准库已经以itertools.combinations 函数的形式提供了您需要的大部分功能。

从您链接到的Wikipedia page 上的定理 2,有n+k-1 choose k-1n 项目划分到k 箱的方法,并且该定理的证明给出了组合和分区之间的明确对应关系。所以我们只需要(1)一种生成这些组合的方法,以及(2)将每个组合转换为相应分区的代码。 itertools.combinations 函数已经提供了第一个成分。对于第二个,每个组合都给出了分隔符的位置;连续分隔器位置之间的差异(减一)给出了分区大小。代码如下:

import itertools

def partitions(n, k):
    for c in itertools.combinations(range(n+k-1), k-1):
        yield [b-a-1 for a, b in zip((-1,)+c, c+(n+k-1,))]

# Example usage
for p in partitions(5, 3):
    print(p)

这是运行上述代码的输出。

[0, 0, 5]
[0, 1, 4]
[0, 2, 3]
[0, 3, 2]
[0, 4, 1]
[0, 5, 0]
[1, 0, 4]
[1, 1, 3]
[1, 2, 2]
[1, 3, 1]
[1, 4, 0]
[2, 0, 3]
[2, 1, 2]
[2, 2, 1]
[2, 3, 0]
[3, 0, 2]
[3, 1, 1]
[3, 2, 0]
[4, 0, 1]
[4, 1, 0]
[5, 0, 0]

【讨论】:

    【解决方案2】:

    另一个递归变体,使用生成器函数,即不是立即打印结果,而是 yields 它们一个接一个地由调用者打印。

    将循环转换为递归算法的方法如下:

    • 确定“基本情况”:当没有更多条时,只打印星星
    • 对于第一段中的任意数量的星,递归确定其余的可能分区,并将它们组合起来

    你也可以把它变成一种算法,将任意序列分割成块:

    def partition(seq, n, min_size=0):
        if n == 0:
            yield [seq]
        else:
            for i in range(min_size, len(seq) - min_size * n + 1):
                for res in partition(seq[i:], n-1, min_size):
                    yield [seq[:i]] + res
    

    示例用法:

    for res in partition("*****", 2):
        print "|".join(res)
    

    【讨论】:

    • 我相信问题的OP版本允许条直接相邻,并且一直向左或向右。等效地,OP 允许“空箱”。
    • 不仅是 OP 的版本,这是条和星的问题,这是一个已知的问题,并且允许空箱
    【解决方案3】:

    一步一个脚印。

    首先,删除sum() 调用。我们不需要它们:

    N=5
    for n in range(0,N):
        x=[1]*n
        for i in range(0,(n+1)):  # len(x) == n
            for j in range(i,(n+1)):
                print i, j - i, n - j
    

    注意x 是一个未使用的变量:

    N=5
    for n in range(0,N):
        for i in range(0,(n+1)):
            for j in range(i,(n+1)):
                print i, j - i, n - j
    

    是时候概括了。上面的算法对于N星和三条是正确的,所以我们只需要对这些条进行泛化即可。

    递归执行此操作。对于基本情况,我们要么有零条,要么有零星,这都是微不足道的。对于递归情况,遍历最左边栏的所有可能位置并在每种情况下递归:

    from __future__ import print_function
    
    def bars_and_stars(bars=3, stars=5, _prefix=''):
        if stars == 0:
            print(_prefix + ', '.join('0'*(bars+1)))
            return
        if bars == 0:
            print(_prefix + str(stars))
            return
        for i in range(stars+1):
            bars_and_stars(bars-1, stars-i, '{}{}, '.format(_prefix, i))
    

    对于奖励积分,我们可以将range() 更改为xrange(),但这只会在您移植到 Python 3 时给您带来麻烦。

    【讨论】:

      【解决方案4】:

      这可以通过以下方法递归解决:

      #n bins, k stars,
      def F(n,k):
        #n bins, k stars, list holds how many elements in current assignment
        def aux(n,k,list):
              if n == 0: #stop clause
                  print list
              elif n==1: #making sure all stars are distributed
                  list[0] = k
                  aux(0,0,list)
              else: #"regular" recursion:
                  for i in range(k+1):
                      #the last bin has i stars, set them and recurse
                      list[n-1] = i
                      aux(n-1,k-i,list)
        aux(n,k,[0]*n)
      

      这个想法是“猜测”最后一个 bin 中有多少颗星,分配它们,然后递归到一个较小的问题,即更少的星(分配多少星)和一个更少的 bin。


      注意:换行很容易

      print list
      

      在设置每个 bin 中的星数后,使用您想要的任何输出格式。

      【讨论】:

        【解决方案5】:

        这是一个复制“条形星形”嵌套循环方法的非递归算法。这假设条形图都从右侧开始,并在左侧结束(箱从 [x,0,0,...] 到 [0,0,..,x])。当循环结束时,第一个 bin 中总会有一个零,因此您可以按照逻辑将其与“bars and stars”匹配。

        def combos(nbins, qty):
            bins = [0]*nbins
            bins[0] = qty #starting bin quantities
            while True:
                yield bins
                if bins[-1] == qty:
                    return #last combo, we're done!
                #leftmost bar movement (inner loop)
                if bins[0] > 0:
                    bins[0] -= 1
                    bins[1] += 1
                else:
                    #bump next bar in nested loops
                    #i.e., find first nonzero entry, and split it
                    nz = 1 
                    while bins[nz] == 0: 
                        nz +=1
                    bins[0]=bins[nz]-1
                    bins[nz+1] += 1
                    bins[nz] = 0
        

        这是 4 个 bin 的结果,数量 3:

        for m in combos(4, 3):
            print(m)
        
        [3, 0, 0, 0]
        [2, 1, 0, 0]
        [1, 2, 0, 0]
        [0, 3, 0, 0]
        [2, 0, 1, 0]
        [1, 1, 1, 0]
        [0, 2, 1, 0]
        [1, 0, 2, 0]
        [0, 1, 2, 0]
        [0, 0, 3, 0]
        [2, 0, 0, 1]
        [1, 1, 0, 1]
        [0, 2, 0, 1]
        [1, 0, 1, 1]
        [0, 1, 1, 1]
        [0, 0, 2, 1]
        [1, 0, 0, 2]
        [0, 1, 0, 2]
        [0, 0, 1, 2]
        [0, 0, 0, 3]
        

        【讨论】:

          【解决方案6】:

          我需要解决同样的问题并找到了这篇文章,但我真的想要一个不依赖于itertools并且找不到的非递归通用算法,所以想出了这个。

          默认情况下,生成器以任一词法顺序生成序列(如前面的递归示例),但也可以通过设置“reversed”标志来生成逆序序列。

          def StarsAndBars(bins, stars, reversed=False):
              if bins < 1 or stars < 1:
                  raise ValueError("Number of bins and objects must both be greater than or equal to 1.")
          
              if bins == 1:
                  yield stars,
                  return
          
              bars = [ ([0] * bins + [ stars ], 1) ]
          
              if reversed:           
                  while len(bars)>0:
                      b = bars.pop()
                      if b[1] == bins:
                          yield tuple(b[0][y] - b[0][y-1] for y in range(1, bins+1))
                      else:
                          bar = b[0][:b[1]]
                          for x in range(b[0][b[1]], stars+1):
                              newBar = bar + [ x ] * (bins - b[1]) + [ stars ]
                              bars.append( (newBar, b[1]+1) )
                  bars = [ ([0] * bins + [ stars ], 1) ]
              else:
                  while len(bars)>0:
                      newBars = []
                      for b in bars:
                          for x in range(b[0][-2], stars+1):
                              newBar = b[0][1:bins] +  [ x, stars ]      
                              if b[1] < bins-1 and x > 0:
                                  newBars.append( (newBar, b[1]+1) )
                              yield tuple(newBar[y] - newBar[y-1] for y in range(1, bins+1))
                      bars = newBars   
          

          【讨论】:

            【解决方案7】:

            这个问题也可以通过列表理解比以前的答案更简单地解决:

            from numpy import array as ar
            from itertools import product
            
            number_of_stars = M
            number_of_bins = N
            
            decompositions = ar([ar(i) for i in product(range(M+1), repeat=N) if sum(i)==M])
            

            这里 itertools.product() 生成一个列表,其中包含列表范围 (M+1) 与自身的笛卡尔积,其中该积已被应用 (repeats=)N 次。 if 语句删除数字加起来不等于星数的组合,例如其中一个组合是 0 和 0 和 0 或 [0,0,0]。 如果我们对列表列表感到满意,那么我们可以简单地删除 np.array() (在示例中为简洁起见仅 ar)。这是 3 个 bin 中 3 颗星的示例输出:

            array([[0, 0, 3],
                   [0, 1, 2],
                   [0, 2, 1],
                   [0, 3, 0],
                   [1, 0, 2],
                   [1, 1, 1],
                   [1, 2, 0],
                   [2, 0, 1],
                   [2, 1, 0],
                   [3, 0, 0]])
            

            我希望这个答案有帮助!

            【讨论】:

              【解决方案8】:

              由于我发现大多数答案中的代码很难理解,即问自己显示的算法与星形和条形的实际问题有何关系,让我们一步一步来:

              首先,我们定义一个函数,在给定位置p 处将条形| 插入到字符串stars 中:

              def insert_bar(stars, p):
                  head, tail = stars[:p], stars[p:]
                  return head + '|' + tail
              

              用法:

              insert_bar('***', 1) # returns '*|**'
              

              在不同位置插入多个条形,例如(1,3) 一个简单的方法是使用reduce(来自functools

              reduce(insert_bar, (1,3), '***') # returns '*|*|*'
              

              如果我们分支insert_bar 的定义来处理这两种情况,我们会得到一个很好且可重用的函数,可以将任意数量的柱形插入一串星形

              def insert_bars(stars, p):
                  if type(p) is int:
                      head, tail = stars[:p], stars[p:]
                      return head + '|' + tail
                  else:
                      return reduce(insert_bar, p, stars)
              

              正如@Mark Dickinson 在他的回答itertools.combinations 中解释的那样,让我们​​产生n+k-1 选择k-1 个柱位置组合。

              现在剩下要做的是创建一个长度为n'*' 字符串,在给定位置插入条形,在条形处拆分字符串并计算每个生成的bin 的长度。因此,下面的实现实际上是将问题陈述逐字翻译成代码

              def partitions(n, k):
                  for positions in itertools.combinations(range(n+k-1), k-1):
                     yield [len(bin) for bin in insert_bars(n*"*", positions).split('|')]
              

              【讨论】:

                【解决方案9】:

                任何寻找 k=2 特定情况的人都可以通过简单地创建一个范围并将其与反向堆叠来节省大量时间。比较与接受的答案。

                n = 500000
                
                %timeit np.array([[i,j] for i,j in partitions(n,2)])
                
                >>> 396 ms ± 13.6 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
                
                %%timeit
                rng = np.arange(n+1)
                np.vstack([rng, rng[::-1]]).T
                
                >>> 2.91 ms ± 190 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
                

                它们确实是等价的。

                it2k = np.array([[i,j] for i,j in partitions(n,2)])
                rng = np.arange(n+1)
                np2k = np.vstack([rng, rng[::-1]]).T
                
                (np2k == it2k).all()
                
                >>> True
                

                【讨论】:

                  猜你喜欢
                  • 2019-05-02
                  • 2014-01-10
                  • 1970-01-01
                  • 1970-01-01
                  • 2019-05-09
                  • 2014-06-09
                  • 1970-01-01
                  • 2012-02-28
                  • 1970-01-01
                  相关资源
                  最近更新 更多