Day 7: Bridge Repair
Mix.install([
{:kino, "~> 0.14.0"}
])
Reading
- We’re doing calibrations for a rope bridge
- Looking fro the right operators to some equations
- Input Equations without any operators
- Equations are always evaluated left-to-right - NOT according to operator precedence
-
Possbile operators are
+
and*
- The equation holds if it results in the value before the colon
- Result Sum of all equation results that can be made to work with any operators
Input
equations =
Kino.FS.file_path("day7_input.bin")
|> File.stream!()
|> Enum.map(fn line ->
[result, numbers] = String.split(line, ":")
numbers = numbers
|> String.split()
|> Enum.map(fn num -> num |> String.trim() |> String.to_integer() end)
{String.to_integer(result), numbers}
end)
Implementation
- With the parsed input from above we can basically just test all combinations
-
We can write a generator that generates a set of operators for the length of the list of values:
-
+ +
-
* +
-
+ *
-
* *
-
- This grows exponentially, so it might be slow if we do it brute-force
Recursive stepping solution
While implementing the above idea, I thought about the following better solution:
- All numbers in my Input are positive
- Adding two positive numbers makes the result bigger
- Multiplying two positive numbers makes the result much bigger
-
Idea:
- Multiply both numbers
- If this is less than the final result, move on to next number
- If it’s more than the final result, add them instead
- If this is less than the final result, move on to next number
- Else go back one step and try the other operator
- This way, we go big early (using multiplications) and gradually make things smaller when needed
- This does fewer computations than generating all possible combinations and evaluating each
defmodule Evaluator do
@operands [:mul, :add]
def is_possibly_true?({result, [first | rest]}) do
do_calc(result, rest, first, @operands)
|> case do
:ok -> true
_ -> false
end
end
defp do_calc(target, [number | rest] = numbers, current, [operand | fallback]) do
case eval(current, number, operand) do
result when result > target -> do_calc(target, numbers, current, fallback)
result -> do_calc(target, rest, result, @operands)
end
|> then(fn
:ok -> :ok
:step_back -> do_calc(target, numbers, current, fallback)
end)
end
defp do_calc(_target, _numbers, _current, []) do
# No more operators to try, need to step back
:step_back
end
defp do_calc(target, [], current, _operators) when target == current, do: :ok
defp do_calc(_target, [], _current, _operators), do: :step_back
defp eval(a, b, :mul), do: a * b
defp eval(a, b, :add), do: a + b
end
total_calibration_result = equations
|> Enum.reduce(0, fn {value, _numbers} = equation, count ->
if Evaluator.is_possibly_true?(equation), do: count + value, else: count
end)
Part 2
-
We have a new
concat
operator which combines two digits:1 || 2 = 12
- Everything is still evaluated left-to-right, as before
- Output is again the sum of all possible equations results
- This number must obviously be higher thant the previous one
Idea
I can probably just extend my code above to also include the new operator and use the exact same rules.
The only interesting question is should there be a new order of operators? I’m gonna append it for now and see…
defmodule EvaluatorV2 do
@operands [:mul, :add, :concat]
def is_possibly_true?({result, [first | rest]}) do
do_calc(result, rest, first, @operands)
|> case do
:ok -> true
_ -> false
end
end
defp do_calc(target, [number | rest] = numbers, current, [operand | fallback]) do
case eval(current, number, operand) do
result when result > target -> do_calc(target, numbers, current, fallback)
result -> do_calc(target, rest, result, @operands)
end
|> then(fn
:ok -> :ok
:step_back -> do_calc(target, numbers, current, fallback)
end)
end
defp do_calc(_target, _numbers, _current, []) do
# No more operators to try, need to step back
:step_back
end
defp do_calc(target, [], current, _operators) when target == current, do: :ok
defp do_calc(_target, [], _current, _operators), do: :step_back
defp eval(a, b, :mul), do: a * b
defp eval(a, b, :add), do: a + b
defp eval(a, b, :concat) do
to_string(a) <> to_string(b)
|> String.to_integer()
end
end
p2_total_calibration_result = equations
|> Enum.reduce(0, fn {value, _numbers} = equation, count ->
if EvaluatorV2.is_possibly_true?(equation), do: count + value, else: count
end)