{"id":"dsa-dp-bottom-up","slug":"bottom-up","title":"Bottom-up tables","text":"Memoization is **top-down**: the first call asks the big question, the recursion works its way down to the small ones, and each answer is saved as it comes back up. **Bottom-up** DP, also called **tabulation**, turns this around. A loop fills a list, smallest questions first, going in an order where every answer an entry needs has been filled in before the loop gets to it. With no recursion there's no recursion limit, and a loop that indexes a list is faster than calls and dict lookups.\n\n### Climbing stairs\n\nYou climb a staircase one or two steps at a time. In how many ways can you climb n steps? Think about the **last move**: it was one step, from step n - 1, or two steps, from step n - 2. So `ways[n] = ways[n - 1] + ways[n - 2]`. Standing at the bottom counts as the one way to climb 0 steps (no moves at all), and a single step can only be climbed one way. Fill the list from there up:\n\n```python\ndef stairs(n):\n    ways = [0] * (n + 1)\n    ways[0] = 1\n    for i in range(1, n + 1):\n        ways[i] = ways[i - 1]\n        if i >= 2:\n            ways[i] += ways[i - 2]\n    return ways\n\n\nprint(stairs(10))\n```\n\n```output\n[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]\n```\n\nThe list has `n + 1` places, for 0 to n steps. Leaving out the `+ 1` is the classic off-by-one: `ways[n]` then stops with an `IndexError`. Negative indexes are the other trap: at `i = 1`, `ways[i - 2]` is `ways[-1]`, the **last** item of the list, and Python hands it over without an error. The `if i >= 2` keeps the loop from reading it. No call waits on another, so the recursion limit is gone: the loop goes as high as you like. The cost is O(n) additions and O(n) entries, but that counts each entry as O(1), which is true only while the numbers stay small. These counts grow by about one digit every five steps, and a sum of numbers with thousands of digits is slow to work out and takes room to keep: \"Huge answers\" below shows the fix.\n\n### Tilings\n\nThe same thinking counts tilings. In how many ways can 1 × 2 dominoes cover a strip 2 high and n long? Look at the left end. Either one domino stands upright there, covering the first column and leaving a strip n - 1 long, or two dominoes lie flat, one above the other, covering two columns and leaving n - 2:\n\n```text\nupright:  |...      flat:  ==..\n          |...             ==..\n```\n\nSo `tilings(n) = tilings(n - 1) + tilings(n - 2)`, with one tiling of the empty strip and one of a strip 1 long: the stairs again. That's the heart of DP: say what an entry means, then find how it's built from smaller entries by looking at the **first or last choice**.\n\n### Keeping only the last values\n\n`ways[i]` reads only the two entries before it, and once they're used, the older ones are never read again. So two variables are enough, and the memory drops to O(1):\n\n```python\ndef tilings(n):\n    before, current = 1, 1          # strips 0 and 1 long\n    for _ in range(n - 1):\n        before, current = current, before + current\n    return current\n\n\nprint([tilings(n) for n in range(1, 11)])\n```\n\n```output\n[1, 2, 3, 5, 8, 13, 21, 34, 55, 89]\n```\n\nThe two variables must change **together**. `before, current = current, before + current` works out both values on the right from the old ones before it assigns either. Written as two lines, `before = current` and then `current = before + current`, the second line already sees the new `before`, so it adds `current` to itself, and the numbers just double: 2, 4, 8, 16.\n\n### Huge answers\n\nCounts grow fast: the ways to climb 1,000 steps have 209 digits. Python's whole numbers never overflow, but adding numbers of thousands of digits is slower than adding small ones, and Python won't even turn a number of more than 4,300 digits into text:\n\n```python\na, b = 0, 1\nfor _ in range(30_000):\n    a, b = b, a + b\nprint(a)\n```\n\n```crash\nTraceback (most recent call last):\n  File \"main.py\", line 4, in <module>\n    print(a)\n    ~~~~~^^^\nValueError: Exceeds the limit (4300 digits) for integer string conversion; use sys.set_int_max_str_digits() to increase the limit\n```\n\nSo counting problems usually ask for the answer **modulo 1,000,000,007**, the remainder after dividing by that prime. Take `% MOD` after every addition: `(a + b) % m` is the same as `(a % m + b % m) % m`, so the remainders add up like the numbers do, and every entry stays below 1,000,000,007.\n","task":"A strip 2 high and n long can be tiled with 2 × 2 squares as well as 1 × 2 dominoes. At the left end there are now three choices: an upright domino (leaving n - 1), two flat dominoes (leaving n - 2) or a square (leaving n - 2). So `tiles(n) = tiles(n - 1) + 2 * tiles(n - 2)`, and `tiles(0)` and `tiles(1)` are both 1.\n\nThis `tiles` keeps only the last two values, but from n = 3 on its answers are wrong. Fix it, so that `print([tiles(n) for n in range(6)])` prints:\n\n```\n[1, 1, 3, 5, 11, 21]\n```\n","kind":"code","seed":"def tiles(n):\n    before, current = 1, 1\n    for _ in range(n - 1):\n        before = current\n        current = current + 2 * before\n    return current\n\n\nif __name__ == \"__main__\":\n    print([tiles(n) for n in range(6)])\n","solution":"def tiles(n):\n    before, current = 1, 1\n    for _ in range(n - 1):\n        before, current = current, current + 2 * before\n    return current\n\n\nif __name__ == \"__main__\":\n    print([tiles(n) for n in range(6)])\n","hints":["Follow `tiles(3)` by hand. The first round comes out right, since `before` and `current` both start at 1. After the second round, `before` should be `tiles(2)` = 3 and `current` should be `tiles(3)` = 5. What do the two lines leave in them?","`before = current` runs first, so when the next line uses `before`, it gets the new value, not `tiles(n - 2)`.","Work out both new values from the old ones at once, with one assignment of two variables, as the lesson's `tilings` does."],"tests":[{"name":"print([tiles(n) for n in range(6)])","stdin":"","call":"print([tiles(n) for n in range(6)])\n","expect":"[1, 1, 3, 5, 11, 21]","hidden":false},{"name":"tiles(10)","stdin":"","call":"tiles(10)\n","expect":"683","hidden":true,"returns":true},{"name":"tiles(30)","stdin":"","call":"tiles(30)\n","expect":"715827883","hidden":true,"returns":true},{"name":"tiles(100)","stdin":"","call":"tiles(100)\n","expect":"845100400152152934331135470251","hidden":true,"returns":true}],"require":[],"forbid":[],"more":[{"task":"On a steep staircase with long legs, every move takes you up **2 or 3** steps, never just 1. Write `stairs(n)`: the number of ways to climb exactly n steps, modulo `MOD`. There's 1 way to climb 0 steps, none to climb 1, and 2 ways to climb 5 (2 + 3 and 3 + 2). The tests go up to 200,000 steps, where plain recursion never ends and memoized recursion would wait on about 100,000 calls at once, far past Python's limit, so fill a table from the bottom up.\n\nFor example, `print(stairs(5), stairs(10))` prints:\n\n```\n2 7\n```\n","kind":"code","seed":"MOD = 1_000_000_007\n\n\ndef stairs(n):\n    pass\n\n\nif __name__ == \"__main__\":\n    print(stairs(5), stairs(10))\n","solution":"MOD = 1_000_000_007\n\n\ndef stairs(n):\n    ways = [0] * (n + 1)\n    ways[0] = 1\n    for i in range(1, n + 1):\n        total = 0\n        if i >= 2:\n            total += ways[i - 2]\n        if i >= 3:\n            total += ways[i - 3]\n        ways[i] = total % MOD\n    return ways[n]\n\n\nif __name__ == \"__main__\":\n    print(stairs(5), stairs(10))\n","hints":["The last move went up 2 or 3 steps, so the ways to climb `i` steps are the ways for `i - 2` and for `i - 3` added up.","Make `ways = [0] * (n + 1)` with `ways[0] = 1`. For `i` from 1 to `n`, add up the entries that exist (`i - 2` and `i - 3` only when they're 0 or more: a negative index would read from the end of the list) and store the sum `% MOD`.","`ways[1]` stays 0, since no move is 1 step long. Return `ways[n]`."],"tests":[{"name":"print(stairs(5), stairs(10))","stdin":"","call":"print(stairs(5), stairs(10))\n","expect":"2 7","hidden":false},{"name":"print(stairs(0), stairs(1), stairs(2), stairs(3), stairs(4))","stdin":"","call":"print(stairs(0), stairs(1), stairs(2), stairs(3), stairs(4))\n","expect":"1 0 1 1 1","hidden":true},{"name":"stairs(30)","stdin":"","call":"stairs(30)\n","expect":"1897","hidden":true,"returns":true},{"name":"stairs(160)","stdin":"","call":"stairs(160)\n","expect":"257276706","hidden":true,"big":true,"returns":true},{"name":"stairs(200_000)","stdin":"","call":"stairs(200_000)\n","expect":"361038733","hidden":true,"big":true,"returns":true}],"require":[],"forbid":[{"pattern":"setrecursionlimit","message":"Don't raise the recursion limit: the browser's Python can crash. Fill a table from the bottom up instead."}]},{"task":"A temp worker is offered a shift on each of the coming days, and `pay[i]` is what the shift on day i pays, counting the days from 0. The rule: never two days in a row. Write `best_total(pay)`, the most the worker can earn. `pay` may be empty: then the answer is 0.\n\nLook at the last day. Either the worker skips it and earns the best total of the days before it, or works it and earns its pay plus the best total of all the days but the last two. The tests have 300,000 days, so fill the answers in from the first day on. Two variables are enough: the best total up to the day before, and up to the day before that.\n\nFor example, `best_total([5, 1, 2, 9, 3])` returns 14, working the first and the fourth day:\n\n```\n14\n```\n","kind":"code","seed":"def best_total(pay):\n    pass\n\n\nif __name__ == \"__main__\":\n    print(best_total([5, 1, 2, 9, 3]))\n","solution":"def best_total(pay):\n    two_back, one_back = 0, 0\n    for p in pay:\n        two_back, one_back = one_back, max(one_back, two_back + p)\n    return one_back\n\n\nif __name__ == \"__main__\":\n    print(best_total([5, 1, 2, 9, 3]))\n","hints":["Think of `best[i]` as the most there is to earn in the first `i` days, days 0 to `i - 1`. Skipping the last of those days (day `i - 1`) gives `best[i - 1]`; working it gives `best[i - 2] + pay[i - 1]`. Take the bigger.","With no days, the best is 0, and with one day it's that day's pay. A list of `len(pay) + 1` entries works, with `best[1]` set only when there is a day; so do two variables, `two_back` and `one_back`, both 0 at the start, which handle an empty `pay` on their own.","For each day's pay `p`, the new best is `max(one_back, two_back + p)`. Then shift: the old `one_back` becomes `two_back`. Assign both at once."],"tests":[{"name":"best_total([5, 1, 2, 9, 3])","stdin":"","call":"best_total([5, 1, 2, 9, 3])\n","expect":"14","hidden":false,"returns":true},{"name":"best_total([])","stdin":"","call":"best_total([])\n","expect":"0","hidden":true,"returns":true},{"name":"best_total([7])","stdin":"","call":"best_total([7])\n","expect":"7","hidden":true,"returns":true},{"name":"best_total([3, 8])","stdin":"","call":"best_total([3, 8])\n","expect":"8","hidden":true,"returns":true},{"name":"best_total([2, 7, 9, 3, 1])","stdin":"","call":"best_total([2, 7, 9, 3, 1])\n","expect":"12","hidden":true,"returns":true},{"name":"best_total([4, 1, 1, 4])","stdin":"","call":"best_total([4, 1, 1, 4])\n","expect":"8","hidden":true,"returns":true},{"name":"best_total([10, 0, 0, 10, 0, 0, 10])","stdin":"","call":"best_total([10, 0, 0, 10, 0, 0, 10])\n","expect":"30","hidden":true,"returns":true},{"name":"best_total([3, 4, 3])","stdin":"","call":"best_total([3, 4, 3])\n","expect":"6","hidden":true,"returns":true},{"name":"best_total([2, 9, 10, 9, 2])","stdin":"","call":"best_total([2, 9, 10, 9, 2])\n","expect":"18","hidden":true,"returns":true},{"name":"best_total([i * i * 7 % 1009 for i in range(80)])","stdin":"","call":"best_total([i * i * 7 % 1009 for i in range(80)])\n","expect":"22049","hidden":true,"big":true,"returns":true},{"name":"best_total([i * i * 7 % 1009 for i in range(300_000)])","stdin":"","call":"best_total([i * i * 7 % 1009 for i in range(300_000)])\n","expect":"89659091","hidden":true,"big":true,"returns":true}],"require":[],"forbid":[{"pattern":"setrecursionlimit","message":"Don't raise the recursion limit: the browser's Python can crash. Fill the answers in from the first day on instead."}]}]}