谷歌访谈:块的安排

时间:2011-10-07 20:40:02

标签: algorithm language-agnostic combinatorics

您将获得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}

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

5 个答案:

答案 0 :(得分:47)

答案 1 :(得分:10)

好吧,就像小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。

从经验输出:

  • 最左边的列或最上面的行(其中L = 1或R = 1),其中N个块是 带有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是每个三角形中的第一列(或行)

这个观察结果可能是显性解决方案的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 <= k&lt; ; = n;换句话说,第一类的无符号斯特林数以相反的顺序排列)。     1,1,1,1,3,2,1,6,11,1,1,15,35,50,24,1,15,85,225,274,120,1,21,175,735, 1624,1764,720,1,28,322,1960,6769,13132,13068,5040,1,36,546,4536,22449,67284,118124,109584,40320,1,45,870,9450,63273, 269325,723680,1172900

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

答案 2 :(得分:5)

根据@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()));
});

答案 3 :(得分:3)

这是我的构建解决方案,灵感来自@ 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]]

答案 4 :(得分:1)

我们通过检查特定的测试用例来推导出一般解决方案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. 要解决此问题,我们将为R使用信号INF(无穷大)来表示其不重要。
  12. 转到10的右边,将会有4 = N-pos数字。
  13. 我们首先在最后一个可能的位置考虑4,位置2 =右侧( _ _ 4 _ )的R-1。
  14. 这里出现在4的左边是无关紧要的。
  15. 但计算4个街区的安排只是条件是从右边可以看到其中2个,与计算相同街区的安排没有什么不同,只是条件是从左边可以看到其中2个。
    • 即。而不是计算像3 1 4 2这样的序列,可以计算像2 4 1 3
    • 这样的序列
  16. 因此,10右边的有效安排数量为F(4, 2, INF)
  17. 因此pos == 69C5 * 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)中,将在L = 2的一系列广告位中考虑5,依此类推。
  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实现中看到。
  21. 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;
    }