|
| 1 | +#!env python |
| 2 | +""" |
| 3 | +--- Day 5: Supply Stacks --- |
| 4 | +The expedition can depart as soon as the final supplies have been unloaded |
| 5 | +from the ships. Supplies are stored in stacks of marked crates, but because |
| 6 | +the needed supplies are buried under many other crates, the crates need to |
| 7 | +be rearranged. |
| 8 | +
|
| 9 | +The ship has a giant cargo crane capable of moving crates between stacks. |
| 10 | +To ensure none of the crates get crushed or fall over, the crane operator |
| 11 | +will rearrange them in a series of carefully-planned steps. After the |
| 12 | +crates are rearranged, the desired crates will be at the top of each stack. |
| 13 | +
|
| 14 | +The Elves don't want to interrupt the crane operator during this delicate |
| 15 | +procedure, but they forgot to ask her which crate will end up where, and |
| 16 | +they want to be ready to unload them as soon as possible so they can |
| 17 | +embark. |
| 18 | +
|
| 19 | +They do, however, have a drawing of the starting stacks of crates and the |
| 20 | +rearrangement procedure (your puzzle input). For example: |
| 21 | +
|
| 22 | + [D] |
| 23 | +[N] [C] |
| 24 | +[Z] [M] [P] |
| 25 | + 1 2 3 |
| 26 | +
|
| 27 | +move 1 from 2 to 1 |
| 28 | +move 3 from 1 to 3 |
| 29 | +move 2 from 2 to 1 |
| 30 | +move 1 from 1 to 2 |
| 31 | +
|
| 32 | +In this example, there are three stacks of crates. Stack 1 contains two |
| 33 | +crates: crate Z is on the bottom, and crate N is on top. Stack 2 contains |
| 34 | +three crates; from bottom to top, they are crates M, C, and D. Finally, |
| 35 | +stack 3 contains a single crate, P. |
| 36 | +
|
| 37 | +Then, the rearrangement procedure is given. In each step of the procedure, |
| 38 | +a quantity of crates is moved from one stack to a different stack. In the |
| 39 | +first step of the above rearrangement procedure, one crate is moved from |
| 40 | +stack 2 to stack 1, resulting in this configuration: |
| 41 | +
|
| 42 | +[D] |
| 43 | +[N] [C] |
| 44 | +[Z] [M] [P] |
| 45 | + 1 2 3 |
| 46 | +
|
| 47 | +In the second step, three crates are moved from stack 1 to stack 3. Crates |
| 48 | +are moved one at a time, so the first crate to be moved (D) ends up below |
| 49 | +the second and third crates: |
| 50 | +
|
| 51 | + [Z] |
| 52 | + [N] |
| 53 | + [C] [D] |
| 54 | + [M] [P] |
| 55 | + 1 2 3 |
| 56 | +
|
| 57 | +Then, both crates are moved from stack 2 to stack 1. Again, because crates |
| 58 | +are moved one at a time, crate C ends up below crate M: |
| 59 | +
|
| 60 | + [Z] |
| 61 | + [N] |
| 62 | +[M] [D] |
| 63 | +[C] [P] |
| 64 | + 1 2 3 |
| 65 | +
|
| 66 | +Finally, one crate is moved from stack 1 to stack 2: |
| 67 | +
|
| 68 | + [Z] |
| 69 | + [N] |
| 70 | + [D] |
| 71 | +[C] [M] [P] |
| 72 | + 1 2 3 |
| 73 | +
|
| 74 | +The Elves just need to know which crate will end up on top of each stack; |
| 75 | +in this example, the top crates are C in stack 1, M in stack 2, and Z in |
| 76 | +stack 3, so you should combine these together and give the Elves the message CMZ. |
| 77 | +
|
| 78 | +After the rearrangement procedure completes, what crate ends up on top of |
| 79 | +each stack? |
| 80 | +
|
| 81 | +
|
| 82 | +--- Part Two --- |
| 83 | +As you watch the crane operator expertly rearrange the crates, you notice |
| 84 | +the process isn't following your prediction. |
| 85 | +
|
| 86 | +Some mud was covering the writing on the side of the crane, and you quickly |
| 87 | +wipe it away. The crane isn't a CrateMover 9000 - it's a CrateMover 9001. |
| 88 | +
|
| 89 | +The CrateMover 9001 is notable for many new and exciting features: air |
| 90 | +conditioning, leather seats, an extra cup holder, and the ability to pick up |
| 91 | +and move multiple crates at once. |
| 92 | +
|
| 93 | +Again considering the example above, the crates begin in the same |
| 94 | +configuration: |
| 95 | +
|
| 96 | + [D] |
| 97 | +[N] [C] |
| 98 | +[Z] [M] [P] |
| 99 | + 1 2 3 |
| 100 | +
|
| 101 | +Moving a single crate from stack 2 to stack 1 behaves the same as before: |
| 102 | +
|
| 103 | +[D] |
| 104 | +[N] [C] |
| 105 | +[Z] [M] [P] |
| 106 | + 1 2 3 |
| 107 | +
|
| 108 | +However, the action of moving three crates from stack 1 to stack 3 means |
| 109 | +that those three moved crates stay in the same order, resulting in this new |
| 110 | +configuration: |
| 111 | +
|
| 112 | + [D] |
| 113 | + [N] |
| 114 | + [C] [Z] |
| 115 | + [M] [P] |
| 116 | + 1 2 3 |
| 117 | +
|
| 118 | +Next, as both crates are moved from stack 2 to stack 1, they retain their |
| 119 | +order as well: |
| 120 | +
|
| 121 | + [D] |
| 122 | + [N] |
| 123 | +[C] [Z] |
| 124 | +[M] [P] |
| 125 | + 1 2 3 |
| 126 | +
|
| 127 | +Finally, a single crate is still moved from stack 1 to stack 2, but now |
| 128 | +it's crate C that gets moved: |
| 129 | +
|
| 130 | + [D] |
| 131 | + [N] |
| 132 | + [Z] |
| 133 | +[M] [C] [P] |
| 134 | + 1 2 3 |
| 135 | +
|
| 136 | +In this example, the CrateMover 9001 has put the crates in a totally |
| 137 | +different order: MCD. |
| 138 | +
|
| 139 | +Before the rearrangement process finishes, update your simulation so that |
| 140 | +the Elves know where they should stand to be ready to unload the final |
| 141 | +supplies. After the rearrangement procedure completes, what crate ends up |
| 142 | +on top of each stack? |
| 143 | +""" |
| 144 | +import sys |
| 145 | +from collections import deque |
| 146 | +from typing import Dict, Literal, Optional |
| 147 | + |
| 148 | +import pytest |
| 149 | +from parse import Result, compile |
| 150 | + |
| 151 | + |
| 152 | +@pytest.fixture |
| 153 | +def example_data(): |
| 154 | + return { |
| 155 | + "input": """ |
| 156 | + [D] |
| 157 | +[N] [C] |
| 158 | +[Z] [M] [P] |
| 159 | + 1 2 3 |
| 160 | +
|
| 161 | +move 1 from 2 to 1 |
| 162 | +move 3 from 1 to 3 |
| 163 | +move 2 from 2 to 1 |
| 164 | +move 1 from 1 to 2 |
| 165 | +""", |
| 166 | + "a": "CMZ", |
| 167 | + "b": "MCD", |
| 168 | + } |
| 169 | + |
| 170 | + |
| 171 | +def test_solve_a(example_data): |
| 172 | + if example_data.get("a") is not None: |
| 173 | + assert solve(input=example_data["input"], part="a") == example_data["a"] |
| 174 | + |
| 175 | + |
| 176 | +def test_solve_b(example_data): |
| 177 | + if example_data.get("b") is not None: |
| 178 | + assert solve(input=example_data["input"], part="b") == example_data["b"] |
| 179 | + |
| 180 | + |
| 181 | +def solve(input: str, part: Literal["a", "b"]) -> Optional[str]: |
| 182 | + mode = "crates" |
| 183 | + unnumbered_stacks: Dict[int, deque[str]] = {} |
| 184 | + stacks: Dict[str, deque[str]] = {} |
| 185 | + parser = compile("move {:d} from {} to {}") |
| 186 | + for line in input.splitlines(): |
| 187 | + if line == "" and len(stacks) and mode == "crates": |
| 188 | + mode = "instructions" |
| 189 | + continue |
| 190 | + if mode == "crates": |
| 191 | + for stack_number in range(0, len(line), 4): |
| 192 | + crate = line[stack_number : stack_number + 3] |
| 193 | + if crate == " ": |
| 194 | + # Empty crate |
| 195 | + continue |
| 196 | + if "[" in line: |
| 197 | + # This is a crate specification |
| 198 | + if unnumbered_stacks.get(stack_number, None) is None: |
| 199 | + unnumbered_stacks[stack_number] = deque() |
| 200 | + unnumbered_stacks[stack_number].appendleft(crate[1]) |
| 201 | + else: |
| 202 | + # This is the list of crate "names" |
| 203 | + stacks[crate[1]] = unnumbered_stacks[stack_number] |
| 204 | + elif mode == "instructions": |
| 205 | + r = parser.parse(line) |
| 206 | + assert type(r) is Result |
| 207 | + size_before = len(stacks[str(r[2])]) |
| 208 | + assert len(stacks[r[1]]) >= r[0] |
| 209 | + if part == "a": |
| 210 | + # Grab boxes and put them directly on the target |
| 211 | + for _ in range(r[0]): |
| 212 | + stacks[str(r[2])].append(stacks[str(r[1])].pop()) |
| 213 | + else: |
| 214 | + # Grab boxes into a holding group, then |
| 215 | + # Put them in order onto the target |
| 216 | + grab: deque[str] = deque() |
| 217 | + for _ in range(r[0]): |
| 218 | + grab.appendleft(stacks[str(r[1])].pop()) |
| 219 | + while len(grab): |
| 220 | + stacks[r[2]].append(grab.popleft()) |
| 221 | + assert len(stacks[str(r[2])]) == size_before + r[0] |
| 222 | + |
| 223 | + return "".join([x.pop() for x in stacks.values()]) |
| 224 | + |
| 225 | + |
| 226 | +if __name__ == "__main__": |
| 227 | + sys.exit(pytest.main([__file__])) |
0 commit comments