Google Interview: Arrangement of Blocks

前端 未结 5 2099
终归单人心
终归单人心 2020-12-22 17:23

You are given N blocks of height 1…N. In how many ways can you arrange these blocks in a row such that when viewed from left you see only L blocks (rest are hidden by taller

相关标签:
5条回答
  • 2020-12-22 17:44

    This is a counting problem, not a construction problem, so we can approach it using recursion. Since the problem has two natural parts, looking from the left and looking from the right, break it up and solve for just one part first.

    Let b(N, L, R) be the number of solutions, and let f(N, L) be the number of arrangements of N blocks so that L are visible from the left. First think about f because it's easier.

    APPROACH 1

    Let's get the initial conditions and then go for recursion. If all are to be visible, then they must be ordered increasingly, so

    f(N, N) = 1
    

    If there are suppose to be more visible blocks than available blocks, then nothing we can do, so

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

    If only one block should be visible, then put the largest first and then the others can follow in any order, so

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

    Finally, for the recursion, think about the position of the tallest block, say N is in the kth spot from the left. Then choose the blocks to come before it in (N-1 choose k-1) ways, arrange those blocks so that exactly L-1 are visible from the left, and order the N-k blocks behind N it in any you like, giving:

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

    In fact, since f(x-1,L-1) = 0 for x<L, we may as well start k at L instead of 1:

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

    Right, so now that the easier bit is understood, let's use f to solve for the harder bit b. Again, use recursion based on the position of the tallest block, again say N is in position k from the left. As before, choose the blocks before it in N-1 choose k-1 ways, but now think about each side of that block separately. For the k-1 blocks left of N, make sure that exactly L-1 of them are visible. For the N-k blocks right of N, make sure that R-1 are visible and then reverse the order you would get from f. Therefore the answer is:

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

    where f is completely worked out above. Again, many terms will be zero, so we only want to take k such that k-1 >= L-1 and N-k >= R-1 to get

    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)
    

    APPROACH 2

    I thought about this problem again and found a somewhat nicer approach that avoids the summation.

    If you work the problem the opposite way, that is think of adding the smallest block instead of the largest block, then the recurrence for f becomes much simpler. In this case, with the same initial conditions, the recurrence is

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

    where the first term, f(N-1,L-1), comes from placing the smallest block in the leftmost position, thereby adding one more visible block (hence L decreases to L-1), and the second term, (N-1) * f(N-1,L), accounts for putting the smallest block in any of the N-1 non-front positions, in which case it is not visible (hence L stays fixed).

    This recursion has the advantage of always decreasing N, though it makes it more difficult to see some formulas, for example f(N,N-1) = (N choose 2). This formula is fairly easy to show from the previous formula, though I'm not certain how to derive it nicely from this simpler recurrence.

    Now, to get back to the original problem and solve for b, we can also take a different approach. Instead of the summation before, think of the visible blocks as coming in packets, so that if a block is visible from the left, then its packet consists of all blocks right of it and in front of the next block visible from the left, and similarly if a block is visible from the right then its packet contains all blocks left of it until the next block visible from the right. Do this for all but the tallest block. This makes for L+R packets. Given the packets, you can move one from the left side to the right side simply by reversing the order of the blocks. Therefore the general case b(N,L,R) actually reduces to solving the case b(N,L,1) = f(N,L) and then choosing which of the packets to put on the left and which on the right. Therefore we have

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

    Again, this reformulation has some advantages over the previous version. Putting these latter two formulas together, it's much easier to see the complexity of the overall problem. However, I still prefer the first approach for constructing solutions, though perhaps others will disagree. All in all it just goes to show there's more than one good way to approach the problem.


    What's with the Stirling numbers?

    As Jason points out, the f(N,L) numbers are precisely the (unsigned) Stirling numbers of the first kind. One can see this immediately from the recursive formulas for each. However, it's always nice to be able to see it directly, so here goes.

    The (unsigned) Stirling numbers of the First Kind, denoted S(N,L) count the number of permutations of N into L cycles. Given a permutation written in cycle notation, we write the permutation in canonical form by beginning the cycle with the largest number in that cycle and then ordering the cycles increasingly by the first number of the cycle. For example, the permutation

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

    would be written in canonical form as

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

    Now drop the parentheses and notice that if these are the heights of the blocks, then the number of visible blocks from the left is exactly the number of cycles! This is because the first number of each cycle blocks all other numbers in the cycle, and the first number of each successive cycle is visible behind the previous cycle. Hence this problem is really just a sneaky way to ask you to find a formula for Stirling numbers.

    0 讨论(0)
  • 2020-12-22 17:47

    We derive a general solution F(N, L, R) by examining a specific testcase: F(10, 4, 3).

    1. We first consider 10 in the leftmost possible position, the 4th ( _ _ _ 10 _ _ _ _ _ _ ).
    2. Then we find the product of the number of valid sequences in the left and in the right of 10.
    3. Next, we'll consider 10 in the 5th slot, calculate another product and add it to the previous one.
    4. This process will go on until 10 is in the last possible slot, the 8th.
    5. We'll use the variable named pos to keep track of N's position.
    6. Now suppose pos = 6 ( _ _ _ _ _ 10 _ _ _ _ ). In the left of 10, there are 9C5 = (N-1)C(pos-1) sets of numbers to be arranged.
    7. Since only the order of these numbers matters, we could look at 1, 2, 3, 4, 5.
    8. To construct a sequence with these numbers so that 3 = L-1 of them are visible from the left, we can begin by placing 5 in the leftmost possible slot ( _ _ 5 _ _ ) and follow similar steps to what we did before.
    9. So if F were defined recursively, it could be used here.
    10. The only difference now is that the order of numbers in the right of 5 is immaterial.
    11. To resolve this issue, we'll use a signal, INF (infinity), for R to indicate its unimportance.
    12. Turning to the right of 10, there will be 4 = N-pos numbers left.
    13. We first consider 4 in the last possible slot, position 2 = R-1 from the right ( _ _ 4 _ ).
    14. Here what appears in the left of 4 is immaterial.
    15. But counting arrangements of 4 blocks with the mere condition that 2 of them should be visible from the right is no different than counting arrangements of the same blocks with the mere condition that 2 of them should be visible from the left.
      • ie. instead of counting sequences like 3 1 4 2, one can count sequences like 2 4 1 3
    16. So the number of valid arrangements in the right of 10 is F(4, 2, INF).
    17. Thus the number of arrangements when pos == 6 is 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. Similarly, in F(5, 3, INF), 5 will be considered in a succession of slots with L = 2 and so on.
    19. Since the function calls itself with L or R reduced, it must return a value when L = 1, that is F(N, 1, INF) must be a base case.
    20. Now consider the arrangement _ _ _ _ _ 6 7 10 _ _.
      • The only slot 5 can take is the first, and the following 4 slots may be filled in any manner; thus F(5, 1, INF) = 4!.
      • Then clearly F(N, 1, INF) = (N-1)!.
      • Other (trivial) base cases and details could be seen in the C implementation below.

    Here is a link for testing the code

    #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;
    }
    
    0 讨论(0)
  • 2020-12-22 17:48

    Based on @PengOne answer, here is 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()));
    });
    
    0 讨论(0)
  • 2020-12-22 17:50

    Here is my construction solution inspired by @PengOne's ideas.

    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]]
    
    0 讨论(0)
  • 2020-12-22 17:57

    well, just as an empirical solution for small 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
    

    and sample output:

    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
    

    You'll note a few obvious (well, mostly obvious) things from the problem statement:

    • the total # of permutations is always N!
    • with the exception of N=1, there is no solution for L,R = (1,1): if a count in one direction is 1, then it implies the tallest block is on that end of the stack, so the count in the other direction has to be >= 2
    • the situation is symmetric (reverse each permutation and you reverse the L,R count)
    • if p is a permutation of N-1 blocks and has count (Lp,Rp), then the N permutations of block N inserted in each possible spot can have a count ranging from L = 1 to Lp+1, and R = 1 to Rp + 1.

    From the empirical output:

    • the leftmost column or topmost row (where L = 1 or R = 1) with N blocks is the sum of the rows/columns with N-1 blocks: i.e. in @PengOne's notation,

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

    • Each diagonal is a row of Pascal's triangle, times a constant factor K for that diagonal -- I can't prove this, but I'm sure someone can -- i.e.:

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

    So the computational complexity of computing b(N,L,R) is the same as the computational complexity of computing b(N,1,L+R-1) which is the first column (or row) in each triangle.

    This observation is probably 95% of the way towards an explicit solution (the other 5% I'm sure involves standard combinatoric identities, I'm not too familiar with those).


    A quick check with the Online Encyclopedia of Integer Sequences shows that b(N,1,R) appears to be OEIS sequence A094638:

    A094638 Triangle read by rows: T(n,k) =|s(n,n+1-k)|, where s(n,k) are the signed Stirling numbers of the first kind (1<=k<=n; in other words, the unsigned Stirling numbers of the first kind in reverse order). 1, 1, 1, 1, 3, 2, 1, 6, 11, 6, 1, 10, 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, 1172700

    As far as how to efficiently compute the Stirling numbers of the first kind, I'm not sure; Wikipedia gives an explicit formula but it looks like a nasty sum. This question (computing Stirling #s of the first kind) shows up on MathOverflow and it looks like O(n^2), as PengOne hypothesizes.

    0 讨论(0)
提交回复
热议问题