Advent of Code 2022, Day 5: Supply Stacks

#ruby #advent of code 2022

Part A

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.

Part B

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 :)