On Day 5 it is getting more interesting. Input is more complex and there is some potential for adding visualisations.
Before Elves expedition can depart supplies need to be unloaded from the ships. These supplies are stored in stacks of marked crates and there is a giant cargo crane capable of rearranging these crates.
Our input looks like this:
[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
The first part is the arrangement of crates in stacks. We have three stacks, first with crates N and Z, second with D, C and M, and third stack with just crate P.
The second part of input is a series of rearrangements that will be done by the crane operator. It says how many crates to move from one stack to another. Crates are moved one at a time, so if you need to move three crates, you need to pop crate from one stack, then push it to another stack and repeat it another two times.
The final setup will look like this:
[Z]
[N]
[D]
[C] [M] [P]
1 2 3
Our task is to output crate name at the top of each stack. In the above case it is CMZ
.
Let’s start with reading and parsing the input file:
data = File.readlines("5.txt", chomp: true)
split_line = 0
data.each_with_index do |line, index|
if line == ""
split_line = index
break
end
end
setup = data[0...(split_line - 1)]
moves = data[(split_line + 1)..]
STACKS_COUNT = data[split_line - 1].split(/\s+/).map(&:to_i).max
stacks = []
setup.reverse_each do |row|
STACKS_COUNT.times do |index|
offset = 1 + index * 4
value = row[offset]
if value != " "
stacks[index] ||= []
stacks[index].push(value)
end
end
end
We read data from file and scan for the empty line to split input into arrangement part and movement part. Arrangement goes into setup
array and movement goes into moves
array. The last (bottom) line of arrangement is scanned for numbers, and we select the largest one. It is the number of stacks.
Then we need to parse the arrangement input and put it into some arrays that will represent our stacks. I’m doing it from the bottom. The crate letters are positioned at columns 1, 5, 9 etc. so extracting it is easy. We can just calculate the appropriate offset 1 + index * 4
, index
is our current stack index. If we get space character it means there is no crate.
When we have our input data parsed, let’s create some code to print it back out, just to check if parsing code is working correctly.
def print(stacks)
max = stacks.map(&:size).max
(max - 1).downto(0) do |index|
puts stacks.map { |stack| stack[index] ? "[#{stack[index]}]" : " " }.join(" ")
end
puts (1..STACKS_COUNT).to_a.map { |index| " #{index} " }.join(" ")
end
This is how it looks for the real input file:
[B] [L] [J]
[B] [Q] [R] [D] [T]
[G] [H] [H] [M] [N] [F]
[J] [N] [D] [F] [J] [H] [B]
[Q] [F] [W] [S] [V] [N] [F] [N]
[W] [N] [H] [M] [L] [B] [R] [T] [Q]
[L] [T] [C] [R] [R] [J] [W] [Z] [L]
[S] [J] [S] [T] [T] [M] [D] [B] [H]
1 2 3 4 5 6 7 8 9
Pretty neat!
The only thing left is to apply the moves and output top crates from each stack.
def apply_move(stacks, size, from, to)
size.times do
crate = stacks[from - 1].pop
stacks[to - 1].push(crate)
end
end
def parse_move(move_str)
match = move_str.match(/move (\d+) from (\d+) to (\d+)/)
[match[1].to_i, match[2].to_i, match[3].to_i]
end
def get_config(stacks)
stacks.map { |stack| stack[-1] }.join("")
end
moves.each do |move|
apply_move(stacks, *parse_move(move))
end
puts get_config(stacks)
apply_move
simulates the crane move, it repeats the operation of popping a crate from one stack and pushing it on another stack. parse_move
is extracting move details using simple regular expression and get_config
is getting top crates from each stack.
The change in Part B allows the crane to pick all the crates at once, so instead of popping a crate and pushing it somewhere else multiple of times, we can now pop all crates in one move and then push all of the onto another stack.
This requires a change to apply_move
method.
def apply_move(stacks, size, from, to)
crates = stacks[from - 1].pop(size)
crates.each do |crate|
stacks[to - 1].push(crate)
end
end
Here is the full code for Part B:
data = File.readlines("5.txt", chomp: true)
split_line = 0
data.each_with_index do |line, index|
if line == ""
split_line = index
break
end
end
setup = data[0...(split_line - 1)]
moves = data[(split_line + 1)..]
STACKS_COUNT = data[split_line - 1].split(/\s+/).map(&:to_i).max
stacks = []
setup.reverse_each do |row|
STACKS_COUNT.times do |index|
offset = 1 + index * 4
value = row[offset]
if value != " "
stacks[index] ||= []
stacks[index].push(value)
end
end
end
def print(stacks)
max = stacks.map(&:size).max
(max - 1).downto(0) do |index|
puts stacks.map { |stack| stack[index] ? "[#{stack[index]}]" : " " }.join(" ")
end
puts (1..STACKS_COUNT).to_a.map { |index| " #{index} " }.join(" ")
end
def apply_move(stacks, size, from, to)
crates = stacks[from - 1].pop(size)
crates.each do |crate|
stacks[to - 1].push(crate)
end
end
def parse_move(move_str)
match = move_str.match(/move (\d+) from (\d+) to (\d+)/)
[match[1].to_i, match[2].to_i, match[3].to_i]
end
def get_config(stacks)
stacks.map { |stack| stack[-1] }.join("")
end
moves.each do |move|
apply_move(stacks, *parse_move(move))
end
print(stacks)
puts get_config(stacks)
FYI. I was hacking this code quickly to get the answer and didn’t pay too much attention to code quality. I am just wondering why I used a constant for stacks count :)