Advent of Code 2022, Day 21: Monkey Math

#ruby #advent of code 2022

Part A

On Day 21 we have input like this:

root: pppw + sjmn
dbpl: 5
cczh: sllz + lgvd
zczc: 2
ptdq: humn - dvpt
dvpt: 3
lfqf: 4
humn: 5
ljgn: 2
sjmn: drzm * dbpl
sllz: 4
pppw: cczh / lfqf
lgvd: ljgn * ptdq
drzm: hmdt - zczc
hmdt: 32

So we have variables that contain plain values or some operation using other variables. The task is to output the value of root.

We can write very elegant solution in Ruby using eval:

data = File.readlines("21.txt", chomp: true)

data.map do |line|
  name, expression = line.match(/^(.+?): (.+?)$/).captures
  eval("def #{name}; #{expression}; end")
end

puts root

We just read the input file and define new method for each variable and put the right side into it’s body.

So for the given example we will generate the following code:

def root; pppw + sjmn; end
def dbpl; 5; end
def cczh; sllz + lgvd; end
def zczc; 2; end
def ptdq; humn - dvpt; end
def dvpt; 3; end
def lfqf; 4; end
def humn; 5; end
def ljgn; 2; end
def sjmn; drzm * dbpl; end
def sllz; 4; end
def pppw; cczh / lfqf; end
def lgvd; ljgn * ptdq; end
def drzm; hmdt - zczc; end
def hmdt; 32; end

Then we just call root method and print out the output value.

Part B

Second part is more difficult. Our root variable should perform equality and our humn value is something we need to calculate so this equality test is true.

We can still use the approach from Part A, but now it will be more code. Approach I used is to define humn method to raise an special exception.

Now instead of just calling root method, we can first call it’s left part and check if it raises this special exception. From the example above our root is defined as def root; pppw + sjmn; end. So first we call pppw. If exception is raised it means humn is evaluated somewhere in pppw tree. If not, it must be evaluate on the right side, so sjmn in this case.

Since one side of the operation will not raise the exception, we will know it’s value. And we also know the operation, so using this we can calculate what should be the value of the other side to make it work.

Let’s use example input again. When we evaluate root, pppw will raise exception, but sjmn will just return 150. Because root operation is == and it must be true then pppw must also equal 150.

We can then try to evaluate pppw. It is defined as def pppw; cczh / lfqf; end. Again cczh will raise an exception, but lfqf will just return 4. So we know that cczh / 4 must equal 150, so cczh must equal 600. And we can continue like that until we reach humn variable.

Here is the full code:

require "debug"

# 9584437937672

class SpecialException < StandardError; end

data = File.readlines("21.txt", chomp: true)

@operations = {}

data.map do |line|
  name, expression = line.match(/^(.+?): (.+?)$/).captures

  if expression =~ /\d+/
    if name == "humn"
      eval("def #{name}; raise SpecialException; end")
    else
      eval("def #{name}; #{expression}; end")

      @operations[name] = expression
    end
  else
    left, op, right = expression.match(/(.+?) (\+|\-|\/|\*) (.+)/).captures

    if name == "root"
      op = "=="
    end

    eval("def #{name}; #{left} #{op} #{right}; end")

    @operations[name] = [left, op, right]
  end
end

def parse(node, expected)
  if node == "humn"
    return expected
  end

  known = nil
  value = nil
  left, op, right = @operations[node]

  begin
    value = eval(left)
  rescue SpecialException
    known = :right
    value = eval(right)
  end

  if known.nil?
    known = :left
    value = eval(left)
  end

  if op == "/" && known == :right
    # expected = left / right
    # left = expected * right
    parse(left, expected * value)
  elsif op == "/" && known == :left
    # expected = left / right
    # right = left / expected
    parse(right, value / expected)
  elsif op == "+" && known == :left
    # expected = left + right
    # right = expected - left
    parse(right, expected - value)
  elsif op == "+" && known == :right
    # expected = left + right
    # left = expected - right
    parse(left, expected - value)
  elsif op == "-" && known == :left
    # expected = left - right
    # right = left - expected
    parse(right, value - expected)
  elsif op == "-" && known == :right
    # expected = left - right
    # left = expected + right
    parse(left, expected + value)
  elsif op == "*" && known == :left
    # expected = left * right
    # right = expected / left
    parse(right, expected / value)
  elsif op == "*" && known == :right
    # expected = left * right
    # left = expected / right
    parse(left, expected / value)
  elsif op == "==" && known == :left
    parse(right, value)
  elsif op == "==" && known == :right
    parse(left, value)
  end
end

puts parse("root", true)