library(tidyverse)
Advent of Code: 2022 Day 5 in R
Day 5: Supply Stacks
The expedition can depart as soon as the final supplies have been unloaded from the ships. Supplies are stored in stacks of marked crates, but because the needed supplies are buried under many other crates, the crates need to be rearranged.
The ship has a giant cargo crane capable of moving crates between stacks. To ensure none of the crates get crushed or fall over, the crane operator will rearrange them in a series of carefully-planned steps. After the crates are rearranged, the desired crates will be at the top of each stack.
The Elves don’t want to interrupt the crane operator during this delicate procedure, but they forgot to ask her which crate will end up where, and they want to be ready to unload them as soon as possible so they can embark.
They do, however, have a drawing of the starting stacks of crates and the rearrangement procedure (your puzzle input). For example:
[D]
[N] [C]
[Z] [M] [P]
1 2 3
move 1 from 2 to 1
move 3 from 1 to 3
move 2 from 2 to 1
move 1 from 1 to 2
In this example, there are three stacks of crates. Stack 1 contains two crates: crate Z is on the bottom, and crate N is on top. Stack 2 contains three crates; from bottom to top, they are crates M, C, and D. Finally, stack 3 contains a single crate, P.
Then, the rearrangement procedure is given. In each step of the procedure, a quantity of crates is moved from one stack to a different stack. In the first step of the above rearrangement procedure, one crate is moved from stack 2 to stack 1, resulting in this configuration:
[D]
[N] [C]
[Z] [M] [P]
1 2 3
In the second step, three crates are moved from stack 1 to stack 3. Crates are moved one at a time, so the first crate to be moved (D) ends up below the second and third crates:
[Z]
[N]
[C] [D]
[M] [P]
1 2 3
Then, both crates are moved from stack 2 to stack 1. Again, because crates are moved one at a time, crate C ends up below crate M:
[Z]
[N]
[M] [D]
[C] [P]
1 2 3
Finally, one crate is moved from stack 1 to stack 2:
[Z]
[N]
[D]
[C] [M] [P]
1 2 3
Part 1
The Elves just need to know which crate will end up on top of each stack; in this example, the top crates are C in stack 1, M in stack 2, and Z in stack 3, so you should combine these together and give the Elves the message CMZ.
After the rearrangement procedure completes, what crate ends up on top of each stack?
Well this will be much more challenging than the previous few puzzles. We need to read the visual representation of the different stacks into a usable data format, and then we need to parse the instructions.
I know I’m going to need at least some of the tidyverse packages, so I’ll load the whole thing now.
Link to test_input.txt and the real input.txt.
First I’ll just read in the raw file as is.
<- readLines("test_input.txt")
test_data test_data
[1] " [D] " "[N] [C] " "[Z] [M] [P]"
[4] " 1 2 3 " "" "move 1 from 2 to 1"
[7] "move 3 from 1 to 3" "move 2 from 2 to 1" "move 1 from 1 to 2"
Parsing Crates
I want to keep all of the lines that represent crates. I think I can disregard the line with the stack numbers because it won’t be easy to make that line up as far as R is concerned. I slice up to the line where the string is ""
and subtract 2 – one for that line and one for the line of stack numbers.
<- test_data[1:(which(test_data == "") - 2)]
crates crates
[1] " [D] " "[N] [C] " "[Z] [M] [P]"
I can now “read” that vector as if it’s a fixed width file. I first open a textConnection()
, use read.fwf()
on that connection, then close the connection. I use column widths of 4, to get the three characters in [D]
as well as a trailing space.
<- textConnection(crates)
connection <- read.fwf(
crates
connection, widths = rep(4, 3),
strip.white = TRUE,
na.strings = ""
)close(connection)
That’s something I can work with! Next I’m going to feed those stacks into vectors in a list, removing NA
s and reversing order so that the first element in the list is the bottom of the stack.
<- vector("list", ncol(crates))
stacks for (i in seq_along(stacks)) {
<- rev(crates[[i]])
stacks[[i]] <- stacks[[i]][!is.na(stacks[[i]])]
stacks[[i]]
} stacks
[[1]]
[1] "[Z]" "[N]"
[[2]]
[1] "[M]" "[C]" "[D]"
[[3]]
[1] "[P]"
To translate: We had stack 1 having two crates: N
on top and Z
on bottom. We now have a list element that is a vector of c("[Z]", "[N]")
, representing Z
first as being in the bottom position. That way, I know that no matter how many crates are in any given stack, the first element is the bottom, the last element is the top, and length()
is the number of crates.
Next I’m just going to clean up by removing the brackets from the crate labels. Notice that I have to escape the brackets themselves because brackets are part of regular expressions to denote a set of characters to be included. But because of the raw character syntax available since R 4.0.0, I can use r"{}"
and avoid having to double-escape everything.
for (i in seq_along(stacks)) {
<- str_remove_all(stacks[[i]], r"{[\[\]]}")
stacks[[i]]
} stacks
[[1]]
[1] "Z" "N"
[[2]]
[1] "M" "C" "D"
[[3]]
[1] "P"
Parsing Instructions
Now it’s time to read in the instructions from the file. We still have the test_data
object that includes the full text of the input. I can now read it starting after the line that’s just the empty string.
<- test_data[(which(test_data == "") + 1):length(test_data)]
instructions_vector instructions_vector
[1] "move 1 from 2 to 1" "move 3 from 1 to 3" "move 2 from 2 to 1"
[4] "move 1 from 1 to 2"
I can make this into a data frame with columns number
, from
, and to
by pulling out the numbers from each line. The regular expression I use indicates that I want to “capture” the first one or more digits that appear, then skip whatever comes next until the next set of digits, and then repeat for a third set of digits, which I expect to be the end of the string. So in total I’m pulling out three sets of digits, represented by the three (\d+)
elements.
<- str_match(instructions_vector, r"{(\d+).*(\d+).*(\d+)$}")
matches matches
[,1] [,2] [,3] [,4]
[1,] "1 from 2 to 1" "1" "2" "1"
[2,] "3 from 1 to 3" "3" "1" "3"
[3,] "2 from 2 to 1" "2" "2" "1"
[4,] "1 from 1 to 2" "1" "1" "2"
The first column of matches
is the full string, and each column thereafter has the captured group. Now I’ll turn it into a data frame by converting those strings into numeric, converting to tibble, and renaming the variables.
<-
instructions apply(matches[, -1], MARGIN = 2, FUN = as.numeric) |>
as_tibble() |>
rename(number = V1, from = V2, to = V3)
instructions
# A tibble: 4 × 3
number from to
<dbl> <dbl> <dbl>
1 1 2 1
2 3 1 3
3 2 2 1
4 1 1 2
Executing Instructions
I’ll start with just the first row of instructions: move 1 from 2 to 1
. We expect to end up with stack 1 being c("Z", "N", "D")
and stack 2 being c("M", "C")
.
<- function(stacks, number, from, to) {
move for (i in seq_len(number)) {
1<- c(stacks[[to]], stacks[[from]][length(stacks[[from]])])
stacks[[to]] 2<- stacks[[from]][-length(stacks[[from]])]
stacks[[from]]
}
stacks }
- 1
- The stacks vector where the crates are moving to becomes the vector concatenated with the final element in the stacks vector where the crates are coming from.
- 2
-
The stacks vector where the crates are coming from gets reassigned to that vector minus the element at
length(vector)
– the final element.
Iterating over the instruction set:
<- function(stacks, instructions) {
execute_instructions for (i in seq_len(nrow(instructions))) {
<- move(stacks, instructions$number[i], instructions$from[i], instructions$to[i])
stacks
}
stacks
}
execute_instructions(stacks, instructions)
[[1]]
[1] "C"
[[2]]
[1] "M"
[[3]]
[1] "P" "D" "N" "Z"
That looks like the example!
Now we just have to read off the crate letters on top of each stack.
<- function(stacks) {
read_crate_labels <- character(length(stacks))
labels for (i in seq_along(stacks)) {
<- stacks[[i]][length(stacks[[i]])]
labels[i]
}paste(labels, collapse = "")
}
read_crate_labels(execute_instructions(stacks, instructions))
[1] "CMZ"
Putting It All Together
Let’s put it all together into a clean function.
<- function(input_data, num_stacks) {
parse_stacks <- input_data[1:(which(input_data == "") - 2)]
crates <- textConnection(crates)
connection <- read.fwf(connection, widths = rep(4, num_stacks), strip.white = TRUE, na.strings = "")
crates close(connection)
<- vector("list", ncol(crates))
stacks for (i in seq_along(stacks)) {
<- rev(crates[[i]])
stacks[[i]] <- stacks[[i]][!is.na(stacks[[i]])]
stacks[[i]] <- str_remove_all(stacks[[i]], r"([\[\]])")
stacks[[i]]
}
stacks
}
<- function(input_data) {
parse_instructions <- input_data[(which(input_data == "") + 1):length(input_data)]
instructions_vector <- str_match(instructions_vector, r"((\d+).*(\d+).*(\d+)$)")
number_matches <-
instructions apply(number_matches[, -1], MARGIN = 2, FUN = as.numeric) |>
as_tibble() |>
rename(number = V1, from = V2, to = V3)
instructions
}
<- function(input_data, num_stacks) {
part_1 <- parse_stacks(input_data, num_stacks)
stacks <- parse_instructions(input_data)
instructions <- execute_instructions(stacks, instructions)
stacks
read_crate_labels(stacks)
}
On the test data:
<- readLines("test_input.txt")
test_data part_1(test_data, 3)
[1] "CMZ"
That looks like what we want! Now let’s try it on the real data. I have looked at the input file and know that it has 9 stacks.
<- readLines("input.txt")
real_data part_1(real_data, 9)
[1] "ZWHVFWQWW"
Another gold star! This was a tricky one. Let’s see what part 2 has in store.
Part 2
As you watch the crane operator expertly rearrange the crates, you notice the process isn’t following your prediction.
Some mud was covering the writing on the side of the crane, and you quickly wipe it away. The crane isn’t a CrateMover 9000 - it’s a CrateMover 9001.
The CrateMover 9001 is notable for many new and exciting features: air conditioning, leather seats, an extra cup holder, and the ability to pick up and move multiple crates at once.
Again considering the example above, the crates begin in the same configuration:
[D]
[N] [C]
[Z] [M] [P]
1 2 3
Moving a single crate from stack 2 to stack 1 behaves the same as before:
[D]
[N] [C]
[Z] [M] [P]
1 2 3
However, the action of moving three crates from stack 1 to stack 3 means that those three moved crates stay in the same order, resulting in this new configuration:
[D]
[N]
[C] [Z]
[M] [P]
1 2 3
Next, as both crates are moved from stack 2 to stack 1, they retain their order as well:
[D]
[N]
[C] [Z]
[M] [P]
1 2 3
Finally, a single crate is still moved from stack 1 to stack 2, but now it’s crate C that gets moved:
[D]
[N]
[Z]
[M] [C] [P]
1 2 3
In this example, the CrateMover 9001 has put the crates in a totally different order:
MCD
.Before the rearrangement process finishes, update your simulation so that the Elves know where they should stand to be ready to unload the final supplies. After the rearrangement procedure completes, what crate ends up on top of each stack?
We’ll still need to get the crates and instructions out of the input data.
<- readLines("test_input.txt")
test_data <- parse_stacks(test_data, 3)
stacks <- parse_instructions(test_data) instructions
This time, we need to change the move()
and execute_instructions()
functions.
<- function(stacks, number, from, to) {
move_2 1<- length(stacks[[from]])
len_from 2<- stacks[[from]][(len_from - (number - 1)) : len_from]
moving 3<- c(
stacks[[to]]
stacks[[to]],
moving
)4<- stacks[[to]][!is.na(stacks[[to]])]
stacks[[to]] 5if (number == len_from) {
<- NA
stacks[[from]] else {
} 6<- stacks[[from]][1:(len_from - number)]
stacks[[from]]
}
stacks }
- 1
-
I make a placeholder variable for the length of the
from
stacks just to save typing and visual clarity - 2
- I make a vector holding the elements that will be moved
- 3
-
The new
to
stacks will be a concatenation of the old stack and the vector of crates being moved. - 4
-
If there was an
NA
in theto
stack (i.e. if it had been empty prior to this move), remove theNA
- 5
-
If I’m moving the entire stack, I need to “save its place” with an
NA
. Otherwise, assigning it justc()
will actually delete that element from thestacks
list. - 6
-
Otherwise, lop off the
number
of items from thefrom
stack
<- 1
i <- move(stacks, instructions$number[i], instructions$from[i], instructions$to[i])
stacks_1 stacks_1
[[1]]
[1] "Z" "N" "D"
[[2]]
[1] "M" "C"
[[3]]
[1] "P"
<- 2
i <- move(stacks_1, instructions$number[i], instructions$from[i], instructions$to[i])
stacks_2 stacks_2
[[1]]
character(0)
[[2]]
[1] "M" "C"
[[3]]
[1] "P" "D" "N" "Z"
<- 3
i <- move(stacks_2, instructions$number[i], instructions$from[i], instructions$to[i])
stacks_3 stacks_3
[[1]]
[1] "C" "M"
[[2]]
character(0)
[[3]]
[1] "P" "D" "N" "Z"
<- 4
i <- move(stacks_3, instructions$number[i], instructions$from[i], instructions$to[i])
stacks_4 stacks_4
[[1]]
[1] "C"
[[2]]
[1] "M"
[[3]]
[1] "P" "D" "N" "Z"
<- function(stacks, instructions) {
execute_instructions_2 for (i in seq_len(nrow(instructions))) {
<- move_2(
stacks
stacks,$number[i],
instructions$from[i],
instructions$to[i]
instructions
)
}
stacks
}
<- execute_instructions_2(stacks, instructions)
stacks_final read_crate_labels(stacks_final)
[1] "MCD"
I had a lot of troubleshooting because I mixed up the order of arguments to move()
in the execute_instructions()
function, but I won’t belabor the point. We got the right answer for the example. Now to put it all together.
<- function(input_data, num_stacks) {
part_2 <- parse_stacks(input_data, num_stacks)
stacks <- parse_instructions(input_data)
instructions <- execute_instructions_2(stacks, instructions)
stacks
read_crate_labels(stacks)
}
part_2(test_data, 3)
[1] "MCD"
Real data:
part_2(real_data, 9)
[1] "HZFZCCWWV"
Correct! This one was tough, but I really enjoyed it.
You can find all of my Advent of Code solutions on GitHub.