【问题标题】:Google Interview: Arrangement of Blocks谷歌专访:区块排列
【发布时间】:2011-10-07 20:40:02
【问题描述】:

给你 N 个高度为 1…N 的块。有多少种方法可以将这些积木排成一排,从左看时只能看到 L 个积木(其余的被更高的积木遮住了),而从右侧看时只能看到 R 个积木?给定的示例 N=3, L=2, R=1 只有一种排列 {2, 1, 3} 而对于 N=3, L=2, R=2 有两种排列方式 {1, 3, 2}{2, 3, 1}

我们应该如何通过编程来解决这个问题?有什么有效的方法吗?

【问题讨论】:

  • 第一个例子应该是{1,2,3}而不是{2,1,3}
  • @user384706 一点也不。 {1,2,3} 左侧有3 可见块,右侧有1 可见块。
  • @user384706 否,如果 {1,2,3} L 为 3。
  • 并非如此。 {1,2,3} 意味着 L=3。还是谢谢大家。
  • @user384706 从左边看{2,1,3},您会看到高度为2 的块和高度为3 的块,但看不到高度为1 的块因为它被高度 2 的方块挡住了。

标签: algorithm language-agnostic combinatorics


【解决方案1】:

这是一个计数问题,而不是构造问题,因此我们可以使用递归来解决它。由于这个问题有两个自然部分,从左看和从右看,把它分解,先解决一个部分。

b(N, L, R) 为解决方案的数量,让f(N, L)N 块的排列数量,以便L 从左侧可见。首先考虑f,因为它更容易。

方法 1

让我们得到初始条件,然后进行递归。如果要全部可见,那么它们必须越来越多地排序,所以

f(N, N) = 1

如果假设可见块比可用块多,那么我们无能为力,所以

f(N, M) = 0 if N < M

如果只有一个块是可见的,那么把最大的放在前面,然后其他的可以按任何顺序跟随,所以

f(N,1) = (N-1)!

最后,对于递归,考虑最高块的位置,比如N 位于左侧的第k 个位置。然后以(N-1 choose k-1) 的方式选择在它前面的块,排列这些块,使L-1 从左侧完全可见,然后将N-k 块按你喜欢的顺序排列在N 后面,给出:

f(N, L) = sum_{1<=k<=N} (N-1 choose k-1) * f(k-1, L-1) * (N-k)! 

其实,既然f(x-1,L-1) = 0换成x&lt;L,那我们还不如在L开始k而不是1

f(N, L) = sum_{L<=k<=N} (N-1 choose k-1) * f(k-1, L-1) * (N-k)! 

好的,既然理解了更容易的位,让我们使用f 来解决更难的位b。同样,使用基于最高块位置的递归,再次说N 位于左侧的k 位置。和以前一样,以N-1 choose k-1 的方式选择它之前的块,但现在分别考虑该块的每一侧。对于N 左侧的k-1 块,确保它们中的L-1 完全可见。对于N 右侧的N-k 块,确保R-1 可见,然后颠倒从f 获得的顺序。因此答案是:

b(N,L,R) = sum_{1<=k<=N}  (N-1 choose k-1) * f(k-1, L-1) * f(N-k, R-1)

f 在上面已经完全解决了。同样,许多项将为零,所以我们只想取k 这样k-1 &gt;= L-1N-k &gt;= R-1 得到

b(N,L,R) = sum_{L <= k <= N-R+1}  (N-1 choose k-1) * f(k-1, L-1) * f(N-k, R-1)

方法 2

我再次考虑了这个问题,发现了一种更好的方法来避免求和。

如果您以相反的方式解决问题,即考虑添加最小块而不是最大块,那么f 的递归变得更加简单。在这种情况下,在初始条件相同的情况下,递归是

f(N,L) = f(N-1,L-1) + (N-1) * f(N-1,L)

第一项 f(N-1,L-1) 来自于将最小块放置在最左边的位置,从而增加了一个可见块(因此 L 减少到 L-1),第二项 (N-1) * f(N-1,L),考虑将最小的块放在任何N-1 非前面位置,在这种情况下它是不可见的(因此L 保持固定)。

这种递归的优点是总是减少N,尽管它使查看某些公式变得更加困难,例如f(N,N-1) = (N choose 2)。这个公式很容易从前面的公式中显示出来,尽管我不确定如何从这个更简单的递归中很好地推导出它。

现在,要回到原来的问题并解决b,我们还可以采用不同的方法。与之前的求和不同,将可见块视为以包的形式出现,因此,如果一个块从左侧可见,那么它的包由它右侧和从左侧可见的下一个块前面的所有块组成,并且类似地,如果一个块从右侧可见,那么它的数据包包含它左侧的所有块,直到从右侧可见的下一个块。对除最高块之外的所有对象执行此操作。这使得L+R 数据包。给定数据包,您可以通过反转块的顺序将一个从左侧移动到右侧。因此,一般情况b(N,L,R) 实际上简化为解决b(N,L,1) = f(N,L) 的情况,然后选择将哪些数据包放在左边,哪些放在右边。所以我们有

b(N,L,R) = (L+R choose L) * f(N,L+R)

同样,这种重新表述比以前的版本有一些优势。将后两个公式放在一起,更容易看出整个问题的复杂性。然而,我仍然更喜欢第一种构建解决方案的方法,尽管也许其他人会不同意。总而言之,这表明解决问题的好方法不止一种。


斯特林数字是怎么回事?

正如 Jason 所指出的,f(N,L) 数字正是(无符号)Stirling numbers of the first kind。可以从每个递归公式中立即看到这一点。但是,能够直接看到它总是很高兴,所以就这样吧。

第一类(无符号)斯特林数,表示为S(N,L),计算NL 循环的排列数。给定一个用循环表示法编写的排列,我们以规范形式编写排列,方法是从该循环中具有最大数字的循环开始,然后按循环的第一个数字对循环进行递增排序。例如,排列

(2 6) (5 1 4) (3 7)

将被写成规范形式

(5 1 4) (6 2) (7 3)

现在去掉括号,注意如果这些是块的高度,那么从左边算起可见块的数量就是周期数!这是因为每个循环的第一个数字阻塞了循环中的所有其他数字,并且每个连续循环的第一个数字在前一个循环后面是可见的。因此,这个问题实际上只是一种偷偷摸摸的方式,要求您​​找到斯特林数的公式。

【讨论】:

  • 如果是施工问题怎么办?它闻起来像 NP-Hard 的。
  • 你可以根据上面的描述构造f的解,然后用它来构造b的解。这个问题比构造所有排列更简单。
  • @Ivan (N choose k)binomial coefficient,给出了N 元素集的k 元素子集的数量。你通过N!/(k! * (N-k)!)计算它。
  • 第二种方法应该是b(N,L,1) = f(N-1,L-1)。因为,最高的方块应该固定在最后一个位置(从左到右总是可见),所以从右边看只有一个可见的方块。
  • 在方法 2 中,我同意 Priyank 的观点:b(N,L,1) = f(N-1,L-1)。最高的块需要特别注意。而且我认为b(N,L,R) = (L+R-2 choose L-1) * f(N-1,L+R-2) 似乎更像是正确答案,而不是b(N,L,R) = (L+R choose L) * f(N,L+R)b(3, 2, 1) 给出错误答案“3”。无论如何,感谢您的出色解决方案
【解决方案2】:

好吧,就像小 N 的经验解决方案:

blocks.py:

import itertools
from collections import defaultdict

def countPermutation(p):
    n = 0
    max = 0
    for block in p:
        if block > max:
            n += 1
            max = block
    return n

def countBlocks(n):
    count = defaultdict(int)
    for p in itertools.permutations(range(1,n+1)):
        fwd = countPermutation(p)
        rev = countPermutation(reversed(p))
        count[(fwd,rev)] += 1
    return count

def printCount(count, n, places):
    for i in range(1,n+1):
        for j in range(1,n+1):
            c = count[(i,j)]
            if c > 0:
                print "%*d" % (places, count[(i,j)]),
            else:
                print " " * places ,
        print

def countAndPrint(nmax, places):
    for n in range(1,nmax+1):
        printCount(countBlocks(n), n, places)
        print

和样本输出:

blocks.countAndPrint(10)
     1

            1
     1

            1      1
     1      2
     1

            2      3      1
     2      6      3
     3      3
     1

            6     11      6      1
     6     22     18      4
    11     18      6
     6      4
     1

           24     50     35     10      1
    24    100    105     40      5
    50    105     60     10
    35     40     10
    10      5
     1

          120    274    225     85     15      1
   120    548    675    340     75      6
   274    675    510    150     15
   225    340    150     20
    85     75     15
    15      6
     1

          720   1764   1624    735    175     21      1
   720   3528   4872   2940    875    126      7
  1764   4872   4410   1750    315     21
  1624   2940   1750    420     35
   735    875    315     35
   175    126     21
    21      7
     1

         5040  13068  13132   6769   1960    322     28      1
  5040  26136  39396  27076   9800   1932    196      8
 13068  39396  40614  19600   4830    588     28
 13132  27076  19600   6440    980     56
  6769   9800   4830    980     70
  1960   1932    588     56
   322    196     28
    28      8
     1

        40320 109584 118124  67284  22449   4536    546     36      1
 40320 219168 354372 269136 112245  27216   3822    288      9
109584 354372 403704 224490  68040  11466   1008     36
118124 269136 224490  90720  19110   2016     84
 67284 112245  68040  19110   2520    126
 22449  27216  11466   2016    126
  4536   3822   1008     84
   546    288     36
    36      9
     1

您会从问题陈述中注意到一些明显(嗯,最明显)的事情:

  • 排列的总数总是 N!
  • 除了 N=1,没有 L,R = (1,1) 的解:如果一个方向的计数为 1,则意味着最高块位于堆栈的那一端,因此另一个方向的计数必须 >= 2
  • 情况是对称的(反转每个排列,然后反转 L、R 计数)
  • 如果p 是 N-1 个块的排列并且具有计数 (Lp,Rp),则插入每个可能点的块 N 的 N 个排列可以具有从 L = 1 到 Lp+1 的计数范围,和 R = 1 到 Rp + 1。

来自经验输出:

  • 具有 N 个块的最左边的列或最上面的行(其中 L = 1 或 R = 1)是 具有 N-1 个块的行/列:即@PengOne 的表示法,

    b(N,1,R) = sum(b(N-1,k,R-1) for k = 1 to N-R+1

  • 每条对角线都是帕斯卡三角形的一行,乘以该对角线的常数因子 K——我无法证明这一点,但我确信有人可以——即:

    b(N,L,R) = K * (L+R-2 choose L-1)K = b(N,1,L+R-1)

所以计算b(N,L,R)的计算复杂度和计算b(N,1,L+R-1)的计算复杂度是一样的,b(N,1,L+R-1)是每个三角形的第一列(或行) .

这个观察结果可能是 95% 的显式解决方案(我确信另外 5% 涉及标准组合恒等式,我不太熟悉这些)。


Online Encyclopedia of Integer Sequences 的快速检查表明 b(N,1,R) 似乎是OEIS sequence A094638

A094638 按行读取的三角形:T(n,k) =|s(n,n+1-k)|,其中 s(n,k) 是有符号的第一类斯特林数 (1

至于如何有效地计算Stirling numbers of the first kind,我不确定;维基百科给出了一个明确的公式,但它看起来像一个讨厌的总和。这个问题(计算第一类斯特林#s)shows up on MathOverflow 看起来像 O(n^2),正如 PengOne 假设的那样。

【讨论】:

  • 您对对角线的观察来自我上面给出的公式。
  • 我对组合代数很感兴趣,所以我必须相信你。 :-)
  • @JasonS 为您的诚实 +1。我没有勇气承认。现在我愿意。谢谢。
  • +1 用于补充研究。我更像是一名数学家而不是程序员,但我很欣赏这两种类型都很有用:-)
  • @JasonS@PengOne 理论与实践相结合,做出好答案!
【解决方案3】:

根据@PengOne 的回答,这里是my Javascript implementation

function g(N, L, R) {
    var acc = 0;
    for (var k=1; k<=N; k++) {
        acc += comb(N-1, k-1) * f(k-1, L-1) * f(N-k, R-1);
    }
    return acc;
}

function f(N, L) {
    if (N==L) return 1;
    else if (N<L) return 0;
    else {
        var acc = 0;
        for (var k=1; k<=N; k++) {
            acc += comb(N-1, k-1) * f(k-1, L-1) * fact(N-k);
        }
        return acc;
    }
}

function comb(n, k) {
    return fact(n) / (fact(k) * fact(n-k));
}

function fact(n) {
    var acc = 1;
    for (var i=2; i<=n; i++) {
        acc *= i;
    }
    return acc;
}

$("#go").click(function () {
    alert(g($("#N").val(), $("#L").val(), $("#R").val()));
});

【讨论】:

    【解决方案4】:

    这是我的构造解决方案,灵感来自@PengOne 的想法。

    import itertools
    
    def f(blocks, m):
        n = len(blocks)
        if m > n:
            return []
        if m < 0:
            return []
        if n == m:
            return [sorted(blocks)]
        maximum = max(blocks)
        blocks = list(set(blocks) - set([maximum]))
        results = []
        for k in range(0, n):
            for left_set in itertools.combinations(blocks, k):
                for left in f(left_set, m - 1):
                    rights = itertools.permutations(list(set(blocks) - set(left)))
                    for right in rights:
                        results.append(list(left) + [maximum] + list(right))
        return results
    
    def b(n, l, r):
        blocks = range(1, n + 1)
        results = []
        maximum = max(blocks)
        blocks = list(set(blocks) - set([maximum]))
        for k in range(0, n):
            for left_set in itertools.combinations(blocks, k):
                for left in f(left_set, l - 1):
                    other = list(set(blocks) - set(left))
                    rights = f(other, r - 1)
                    for right in rights:
                        results.append(list(left) + [maximum] + list(right))
        return results
    
    # Sample
    print b(4, 3, 2) # -> [[1, 2, 4, 3], [1, 3, 4, 2], [2, 3, 4, 1]]
    

    【讨论】:

      【解决方案5】:

      我们通过检查特定的测试用例推导出通用解决方案F(N, L, R)F(10, 4, 3)

      1. 我们首先考虑 10 在最左边的可能位置,即第 4 个( _ _ _ 10 _ _ _ _ _ _ )
      2. 然后我们找到10左右的有效序列数的乘积。
      3. 接下来,我们将考虑第 5 个插槽中的 10,计算另一个产品并将其添加到前一个产品中。
      4. 此过程将一直持续到 10 位于最后一个可能的插槽,即第 8 个。
      5. 我们将使用名为 pos 的变量来跟踪 N 的位置。
      6. 现在假设pos = 6 ( _ _ _ _ _ 10 _ _ _ _ )。在 10 的左边,有9C5 = (N-1)C(pos-1) 组要排列的数字。
      7. 由于只有这些数字的顺序很重要,我们可以查看 1、2、3、4、5。
      8. 要使用这些数字构造一个序列,以便从左侧可以看到其中的 3 = L-1,我们可以先将 5 放在最左边的可能插槽 ( _ _ 5 _ _ ) 中,然后按照与之前类似的步骤进行操作。
      9. 因此,如果 F 是递归定义的,则可以在此处使用。
      10. 现在唯一的区别是 5 右边的数字顺序无关紧要。
      11. 为解决此问题,我们将使用信号 INF(无穷大)来表示 R 的重要性。
      12. 转向 10 的右边,将剩下 4 = N-pos 个数字。
      13. 我们首先考虑在最后一个可能的插槽中的 4,位置 2 = R-1 从右边 ( _ _ 4 _ )
      14. 此处显示在 4 左侧的内容无关紧要。
      15. 但是,仅以其中 2 个应从右侧可见的条件计数 4 个块的排列与仅以其中 2 个应从左侧可见的条件计数相同块的排列没有什么不同。
        • 即。而不是像3 1 4 2这样计算序列,可以计算像2 4 1 3这样的序列
      16. 所以10右边的有效排列数是F(4, 2, INF)
      17. 因此pos == 6 时的排列数为9C5 * F(5, 3, INF) * F(4, 2, INF) = (N-1)C(pos-1) * F(pos-1, L-1, INF)* F(N-pos, R-1, INF)
      18. 同样,在F(5, 3, INF) 中,5 将被视为与L = 2 等连续的插槽。
      19. 由于函数调用自身时 L 或 R 减少,所以它必须在 L = 1 时返回一个值,即 F(N, 1, INF) 必须是基本情况。
      20. 现在考虑安排_ _ _ _ _ 6 7 10 _ _
        • 只能占用第5个槽位,后面4个槽位可以任意填满;因此F(5, 1, INF) = 4!
        • 那么显然F(N, 1, INF) = (N-1)!
        • 其他(琐碎的)基本案例和细节可以在下面的 C 实现中看到。

      Here是测试代码的链接

      #define INF UINT_MAX
      
      long long unsigned fact(unsigned n) { return n ? n * fact(n-1) : 1; }
      
      unsigned C(unsigned n, unsigned k) { return fact(n) / (fact(k) * fact(n-k)); }
      
      unsigned F(unsigned N, unsigned L, unsigned R)
      {
          unsigned pos, sum = 0;
          if(R != INF)
          {
              if(L == 0 || R == 0 || N < L || N < R) return 0;
              if(L == 1) return F(N-1, R-1, INF);
              if(R == 1) return F(N-1, L-1, INF);
              for(pos = L; pos <= N-R+1; ++pos)
                 sum += C(N-1, pos-1) * F(pos-1, L-1, INF) * F(N-pos, R-1, INF);
          }
          else
          {
              if(L == 1) return fact(N-1);
              for(pos = L; pos <= N; ++pos)
                 sum += C(N-1, pos-1) * F(pos-1, L-1, INF) * fact(N-pos);
          }
          return sum;
      }
      

      【讨论】:

        猜你喜欢
        • 2020-12-15
        • 1970-01-01
        • 2017-12-30
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2017-05-17
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多