简体   繁体   中英

Python : Stairstep DP solution understanding

I have been looking at this problem.

  • The goal is to build staircases with bricks
  • There are N bricks and all of them must be used to build a staircase
  • Staircases consist of steps of different sizes in a strictly increasing order
  • It is not allowed for a staircase to have steps of equal sizes
  • Every staircase consists of at least two steps and each step contains at least one brick

Link to the full problem http://acm.timus.ru/problem.aspx?num=1017&locale=en

I know already that this is with dealing distinct partitions and number theory/knapsack problem. The goal is effectively given a list n = [1,2,3,....n -1] determine how many unordered sets that add up to N exist. I say unordered because given the list has no duplicates, so any combination can be sorted as a valid specific answer to a given size to fit the rules. I also understand the general concept is you start with height 1 and branch/add all of the possible combinations, going until the new height goes over on bricks and only adding to the total combinations if the new height uses up all the left over bricks at that point. I realize that there are patterns like you already know how many partitions exist for n = 3, when going into 4, so using that data(dynamic programming) is part of the solution.

I eventually came across the following solution.

n = int(input())
m = [[0 for i in range(n + 1)] for j in range(n + 1)]
m[0][0] = 1  # base case

for last in range(1, n + 1):
    for left in range(0, n + 1):
        m[last][left] = m[last - 1][left]
        if left >= last:
            m[last][left] += m[last - 1][left - last]

print(m[n][n] - 1)

So I understand that the last variable represents how many bricks it is using. And the left loop has it run across and transfers the cached data. So I understand m[last][left] being assigned to the entry up one because its already has the calculated partition sum for all possible stairs using last - 1 bricks.

I also get that the diagonal holds all of the partition counts( [3,3] = distinct partitions of bricks = 3) The part I am unsure about is the way the data is determined after the diagonal check( if left >= last), how does the algorithm know that adding that exact matrix location to the current index gets the right values? What is the relationship between the data at those points.

Below is a matrix of the 2d array after running on 10, where the answer is 9

=0 1 2 3 4 5 6 7 8 9 10

0 |1 0 0 0 0 0 0 0 0 0 0

1 |1 1 0 0 0 0 0 0 0 0 0

2 |1 1 1 1 0 0 0 0 0 0 0

3 |1 1 1 2 1 1 1 0 0 0 0

4 |1 1 1 2 2 2 2 2 1 1 1

5 |1 1 1 2 2 3 3 3 3 3 3

6 |1 1 1 2 2 3 4 4 4 5 5

7 |1 1 1 2 2 3 4 5 5 6 7

8 |1 1 1 2 2 3 4 5 6 7 8

9 |1 1 1 2 2 3 4 5 6 8 9

10 |1 1 1 2 2 3 4 5 6 8 10

The intuition behind the bottom-up solution to this problem is a little difficult to understand, but here goes:

First, let's rename m to something more intuitive: ways . Now when we examine the problem, we see that there are a finite number of states in this problem. The state-space is defined by last , which you can think of as the number of bricks in the last step you've made , and left , which is the number of bricks you have left to use .

Thus, ways[last][left] represents the number of staircases you can build if the highest step of your staircase has height last , and you have left bricks to work with.

Now, let's look at the base case. ways[0][0] tells us how many staircases we can build if we have a step of height 0 and we have 0 bricks left. Well, there's only one way to do that: put 0 bricks down! Consequently, ways[0][0] = 1 . If you look at ways[0][1] , this asks: how many ways can we build a staircase if the last step was of 0 height and we have 1 brick left? That is impossible, because the height of the steps going from left to right must be strictly increasing. As you can see, ways[0][1], ways[0][2], ... , ways[0][k], k > 0 will all be zero.

The toughest part of the bottom-up solution is the recurrence. Let's look at the first line inside the nested for loop.

ways[last][left] = ways[last - 1][left]

This says that the number of staircases we can make with the last step of height last and left bricks remaining is equal to the number of staircases we can make with a last step of height last-1 with left bricks remaining. This should make sense, because if you have a taller last step, it becomes less restrictive, and ways[last][left] becomes a superset of ways[last-1][left] . Think of it like this: we have 5 bricks to work with. How many staircases are we guaranteed to be able to make? The same number as we could make with 4 bricks. At the very least, you could simply add the extra brick to the tallest step on the right, and it would still be valid.

4 bricks                           5 bricks
                                       #
   #                                   #
   #                                   #
  ##                                  ##
  13                                  14

What happens when the number of bricks you have left is greater than or equal to the number of bricks you have in your last level? In this case, we can build a new staircase to the left of the existing step. But this new staircase cannot be greater than last-1 bricks high, because again, steps must be strictly increasing. So how many staircases is that? Well, we're using last bricks to make the step, so we have left-last bricks remaining to create the staircases to the left. That number is in the cell ways[last-1][left-last] . Fortunately, we've calculated that value before, so it's just a simple lookup.

An example might help with actual numbers, so I'll run through a calculation for n=2 .

# initial state with the base case
[1, 0, 0]
[0, 0, 0]
[0, 0, 0]
# ways[1][0] = ways[0][0] at least b/c the spare brick can go on highest step
[1, 0, 0]
[1, 0, 0]
[0, 0, 0]
# ways[1][1] = ways[0][1] by the same logic
# ways[1][1] += ways[0][0] because we use up 1 brick making the step,
# and we have 0 bricks left, and we need the max height to be 0
[1, 0, 0]
[1, 1, 0]
[0, 0, 0]
# ways[1][2] = ways[0][2] by the same logic
# ways[1][2] += ways[0][1] because we use up 1 brick making the step,
# and we have 1 bricks left, and we need the max height to be 0 (impossible!)
[1, 0, 0]
[1, 1, 0]
[0, 0, 0]
# ways[2][0] = ways[1][0] by the same logic
[1, 0, 0]
[1, 1, 0]
[1, 0, 0]
# ways[2][1] = ways[1][1] by the same logic
# ways[2][1] += ways[1][0] because we use up 1 brick making the step,
# and we have 0 bricks left, and we need the max height to be 0
[1, 0, 0]
[1, 1, 0]
[1, 1, 0]
# ways[2][2] = ways[1][2] by the same logic
# ways[2][2] += ways[1][0] because we use up 2 bricks making the step,
# and we have 0 bricks left, and we need the max height to be 1. 
# That's perfect, because we can make a step of max height 1 with 0 steps
[1, 0, 0]
[1, 1, 0]
[1, 1, 1]

This is the logic behind filling up the ways table. This is the last line of the code:

print(ways[n][n] - 1)

The reason why we need to subtract 1 is due in part to our base case. We assume that there is 1 way to make a staircase with 0 bricks and 0 height. However, this doesn't really constitute a "staircase" according to the rules, because a staircase must have two or more steps. Because of this, every diagonal entry includes one extra "invalid" staircase: n bricks stacked on top of each other.

4 bricks
          #
 #        #
 #        #
##        #
13        4

We need this because in future staircases, we could use n bricks stacked on top of each other, like if we had 9 bricks.

9 bricks
  #       #
  #      ##
 ##      ##
 ##      ##
###      ##
135      45

It's just that when you only have n bricks, you need to subtract out that invalid case.

I hope this helped--good luck!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM