Powered by AppSignal & Oban Pro

Advent of code day 10

2025/livebooks/day-10.livemd

Advent of code day 10

Mix.install(
  [
    {:kino, "~> 0.5.0"},
    {:dantzig, github: "hauleth/dantzig", ref: "use-proper-binary"}
  ],
  config: [
    # we can always use the original branch if we apply this patch https://github.com/tmbb/dantzig/pull/4
    # might be needed to use the previous hack in my commits to override the path.
    dantzig: [
      highs_binary_path:  "/usr/local/bin/highs" # this branch has fixes for integers. Original one
      # fails to round it correct and since we cannot have 0.5 buttons we must use it. 
    ]
  ]
)

Setup input

example = Kino.Input.textarea("Please paste your input example:")
defmodule Combinations do
  def combinations(_, 0), do: [[]]
  def combinations([], _), do: []
  
  def combinations([h | t], r) do
    (for combo <- combinations(t, r - 1), do: [h | combo]) ++ combinations(t, r)
  end
end
lines =
  example
  |> Kino.Input.read()
  |> String.split("\n", trim: true)
  |> Enum.map(fn line ->
    [target | rest] = String.split(line, " ", trim: true)
    [j | buttons] = Enum.reverse(rest)

    j =
      String.trim(j, "{")
      |> String.trim("}")
      |> String.split(",", trim: true)
      |> Enum.map(&amp;String.to_integer/1)

    buttons =
      Enum.reverse(buttons)
      |> Enum.map(fn button ->
        button
        |> String.trim_leading("(")
        |> String.trim_trailing(")")
        |> String.split(",")
        |> Enum.map(&amp;String.to_integer/1)
        |> Enum.into(MapSet.new())
      end)

    target =
      target
      |> String.trim_leading("[")
      |> String.trim_trailing("]")
      |> String.split("", trim: true)
      |> Enum.with_index()
      |> Enum.filter(fn {v, _i} -> v == "#" end)
      |> Enum.map(fn {_, i} -> i end)
      |> Enum.into(MapSet.new())

    {target, buttons, j}
  end)

Part 01

total =
  Enum.reduce(lines, 0, fn {target, buttons, _}, acc ->
    result = Enum.reduce_while(1..length(buttons), 0, fn count, _inner_acc ->
      found = Enum.reduce_while(Combinations.combinations(buttons, count), nil, fn attempt, _light ->
        lights = Enum.reduce(attempt, MapSet.new(), fn button, lights ->
          MapSet.symmetric_difference(lights, button)
        end)
        
        if MapSet.equal?(lights, target) do
          {:halt, count}
        else
          {:cont, nil}
        end
      end)
      
      if found != nil do
        {:halt, found}
      else
        {:cont, 0}
      end
    end)
    
    acc + result
  end)

Part 02

defmodule JoltageOptimizer do
  alias Dantzig.Polynomial
  alias Dantzig.Constraint
  alias Dantzig.Problem
  alias Dantzig.Solution

  def solve_machine(buttons, targets) do
    p = Problem.new(direction: :minimize)

    # Create one variable per button
    {p, vars} =
      Enum.reduce(0..(length(buttons) - 1), {p, []}, fn i, {prob, vars_acc} ->
        {prob, var} = Problem.new_variable(prob, "v#{i}", min: 0, type: :integer)
        {prob, vars_acc ++ [var]}
      end)

    # For each counter (joltage requirement), add constraint
    p =
      targets
      |> Enum.with_index()
      |> Enum.reduce(p, fn {target, counter_idx}, prob ->
        # Find which buttons affect this counter
        affecting_button_indices =
          buttons
          |> Enum.with_index()
          |> Enum.filter(fn {button, _} -> counter_idx in button end)
          |> Enum.map(fn {_, button_idx} -> button_idx end)

        case affecting_button_indices do
          [] ->
            prob

          indices ->
            # Sum the variables for buttons that affect this counter
            button_vars = Enum.map(indices, fn idx -> Enum.at(vars, idx) end)
            poly = Polynomial.sum(button_vars)
            Problem.add_constraint(prob, Constraint.new(poly, :==, target))
        end
      end)

    # Minimize sum of all button presses
    p = Problem.increment_objective(p, Polynomial.sum(vars))

    # Solve
    {:ok, s} = Dantzig.HiGHS.solve(p)

    # Return total button presses
    Enum.sum_by(vars, &amp;Solution.evaluate(s, &amp;1)) |> round()
  end
end

lines =
  Enum.map(lines, fn {_, buttons, joltages} ->
    buttons = buttons |> Enum.map(fn b -> MapSet.to_list(b) end)

    {buttons, joltages}
  end)

total =
  lines
  |> Enum.reduce(0, fn {buttons, targets}, acc ->
    result = JoltageOptimizer.solve_machine(buttons, targets)
    acc + result
  end)

total