sevenseacat

sevenseacat

Author of Ash Framework

Advent of Code 2025 - Day 10

Well some of us wanted a difficulty spike - and today we got one :sweat_smile:

https://github.com/sevenseacat/advent_of_code/blob/main/lib/y2025/day10.ex

I don’t think there’s any way to solve part 2 in the “naive” way (eg. with a breadth-first search). There’s no way to reduce the search space enough.

The brainwave is that each set of buttons/target joltage can be modelled as a set of simultaneous equations. Of course they’re not nice simultaneous equations, because there’s more variables (buttons) than equations (output values), and you need to add constraints for non-negative and whole numbers….

Name                     ips        average  deviation         median         99th %
day 10, part 1         78.99       12.66 ms     ±3.07%       12.63 ms       13.76 ms
day 10, part 2          6.90      144.82 ms     ±6.17%      141.61 ms      175.05 ms

First Post! Switch mode

antoine-duchenet

antoine-duchenet

I didn’t find any way to solve it with a BFS or some smart DFS, BUT: for those who balk at implementing a equations solver (like me, I was not in the mood today), it is solvable with a genetic programming / iterative reparation approach, the hard part being to get enough guaranties that the solution is the actual minimum.

It is pretty random, and maybe not very elegant, but at least it did the job with my input (and took a lot more time to run than an equation solver would :laughing: ).

Most Liked

hauleth

hauleth

Even with this PRs it doesn’t work, as the solver always used downloaded HiGHS binary instead of using user-provided one. In addition to that HiGHS 1.9.0 (which is downloaded) has some bug, which causes wrong result for my input, and if I change version to 1.12.0 it fails, as it requires additional external libraries (OpenBLAS IIRC). I have forked Dantzig to remove the HiGHS downloader (as it is working improperly anyway) in general and make it more into “CPLEX library” that can be then used with any solver and move solvers downloading and management to separate libs. Something like Nx or Ecto is doing. That way it can use Z3, HiGHS or anything else that can ingest this particular file format (and potentially implement other export formats as well).

@mudasobwa I was also thinking about implementing SMT-LIB generator in Elixir, so it would provide a way to use any SMT solver with Elixir without worrying about compiling binaries and stuff.

mudasobwa

mudasobwa

Creator of Cure

There is no interface, Cure invokes z3 via Erlang port https://github.com/am-kantox/cure-lang/blob/main/src%2Fsmt%2Fcure_smt_process.erl

antoine-duchenet

antoine-duchenet

Yes, my solution does not use any solver library:

defmodule Y2025.D10 do
  use Day, input: "2025/10", part1: ~c"l", part2: ~c"l"

  defp part2(input) do
    lines = input |> parse_input() |> Enum.with_index()

    lines
    |> Map.new(fn {_, idx} -> {idx, 1000} end)
    |> Stream.unfold(fn old_map ->
      new_map =
        Enum.reduce(lines, old_map, fn {l, idx}, acc ->
          maybe_new_min = maybe_min_presses(l)
          Map.update!(acc, idx, &min(&1, maybe_new_min))
        end)

      old_sum = old_map |> Map.values() |> Enum.sum()
      new_sum = new_map |> Map.values() |> Enum.sum()

      {min(old_sum, new_sum), new_map}
    end)
    |> Stream.map(&IO.inspect/1)
    |> Stream.transform({nil, 100, 100}, &until_stable/2)
    |> Enum.take(1)
    |> hd()
  end

  defp until_stable(value, {last_value, requirement, count}) do
    case {value == last_value, count} do
      {true, 0} ->
        {:halt, {value, requirement, count - 1}}

      {true, 1} ->
        {[value], {value, requirement, count - 1}}

      {true, _} ->
        {[], {value, requirement, count - 1}}

      {false, _} ->
        {[], {value, requirement, requirement - 1}}
    end
  end

  defp maybe_min_presses(%{lights: _, buttons: buttons, requirements: requirements}) do
    target_map = index(requirements)

    repair(buttons, target_map)
  end

  defp repair(buttons, target_map) do
    [initial_candidate] =
      buttons
      |> Enum.sort_by(&Enum.count/1, :desc)
      |> Enum.take(1)

    target_sum =
      target_map
      |> Map.values()
      |> Enum.sum()

    initial_presses = div(target_sum, length(initial_candidate))

    buttons_map =
      buttons
      |> Enum.map(&{&1, if(&1 == initial_candidate, do: initial_presses, else: 0)})
      |> Map.new()

    initial_map =
      target_map
      |> Map.keys()
      |> Enum.map(&if Enum.member?(initial_candidate, &1), do: initial_presses, else: 0)
      |> index()

    repair_rec(initial_map, target_map, buttons_map)
  end

  defp repair_rec(target_map, target_map, buttons_map) do
    buttons_map
    |> Map.values()
    |> Enum.sum()
  end

  defp repair_rec(current_map, target_map, buttons_map) do
    candidates =
      buttons_map
      |> Map.keys()
      |> Enum.map(&{score(&1, current_map, target_map), &1})
      |> Enum.sort_by(&elem(&1, 0), :desc)

    add = Enum.take(candidates, Enum.random(1..3))
    sub = Enum.take(candidates, -Enum.random(1..3))

    mutations =
      if Enum.random(0..3) == 0 do
        candidates
        |> Enum.shuffle()
        |> Enum.take(Enum.random(1..3))
      else
        []
      end

    {next_map, new_buttons_map} =
      add
      |> Enum.concat(sub)
      |> Enum.concat(mutations)
      |> Enum.reduce({current_map, buttons_map}, &apply_button/2)

    repair_rec(next_map, target_map, new_buttons_map)
  end

  defp apply_button({score, button}, {current_map, buttons_map}) do
    dir =
      cond do
        score > 0 and Map.get(buttons_map, button) > 0 and Enum.random(0..49) == 0 -> -1
        score > 0 -> 1
        score < 0 and Map.get(buttons_map, button) > 0 -> -1
        true -> 0
      end

    new_map =
      Enum.reduce(button, current_map, fn idx, acc ->
        Map.update!(acc, idx, &(&1 + dir))
      end)

    new_buttons_map = Map.update!(buttons_map, button, fn count -> count + dir end)

    {new_map, new_buttons_map}
  end

  defp score(button, current_map, target_map) do
    Enum.reduce(button, 0, fn idx, acc ->
      c = Map.fetch!(current_map, idx)
      t = Map.fetch!(target_map, idx)

      cond do
        c < t ->
          acc + Math.sqrt(t - c)

        c > t ->
          acc - Math.sqrt(c - t)

        true ->
          acc
      end
    end)
  end

  defp index(lights) do
    lights
    |> Enum.with_index()
    |> Enum.reduce(%{}, fn {light, idx}, acc -> Map.put(acc, idx, light) end)
  end

  defp parse_input(input), do: Enum.map(input, &parse_line/1)

  defp parse_line(line) do
    [lights | rest] =
      ~r/[\[\(\{]([\d\.\#,]+)[\}\)\]]/
      |> Regex.scan(line)
      |> Enum.map(&Enum.at(&1, 1))

    {buttons, [requirements]} =
      rest
      |> Enum.map(
        &(&1
          |> Utils.splitrim(",")
          |> Enum.map(fn num_str -> String.to_integer(num_str) end))
      )
      |> Enum.split(-1)

    %{lights: Utils.splitrim(lights, ""), buttons: buttons, requirements: requirements}
  end
end

It’s basically a recursive reparation approach with genetic programming inspiration. Those parameters converged “pretty quickly” :

  • A single iteration takes around ~1 second, it processes all the machines
  • We wait for the minimum (sum) to be stable for 100 iterations
  • Each iteration “repairs” each machine until it reaches the target numeric counters state, keeping track of the button presses count
  • The initial state (numeric counters and presses count) is filled with a very rough approximation, it is reset on each reparation
  • Each reparation step presses between 1 and 3 buttons, prioritized by a score (how relevant each button is to converge)
  • Each reparation step unpresses between 1 and 3 buttons, prioritized by a score (how irrelevant each button is to converge)
  • There is 25% chance that 1 to 3 additional buttons (mutations) get triggered, pressed or unpressed depending on their relevance to converge
  • There is a slight 2% chance that any button that should be pressed would be unpressed (only if it can be unpressed, it helps to converge quicker by jumping out of rabbit holes caused by the very basic / poor scoring method)

It took ~5min20sec to give the answer for my input, but I suspect it may vary a lot :grin:

Last Post!

KeithFrost

KeithFrost

I became obsessed with this problem after it took me a whole day to get a solution to Part 2. I have now implemented three different solutions: one, an exceedingly clever one (I can say this, because I didn’t think of the algorithm) which extends the Part 1 solution by finding all of the ways (with 0 or 1 presses of each button) to match the least significant bits of the desired joltages, and then recursing on the now-even joltages divided by two, until the desired joltages are all zero. I’ll post that one here, because it is the shortest self-contained solution (the other two are a long self-contained Elixir solution which implements Bareiss’s algorithm for reduction of integer matrices and then searches values of the remaining free variables for solutions, and another one that composes Prolog code that invokes SWI Prolog’s CLPQ solver).

defmodule Machine do
  defstruct target: 0, buttons: %{}, joltages: %{}, parity_map: %{}

  def parse_target(target_str) do
    to_charlist(target_str)
      |> Enum.with_index()
      |> Enum.reduce(0, fn {ch, i}, t ->
        case ch do
          ?. -> 
            t
          ?# ->
            Bitwise.bor(t, Bitwise.bsl(1, i))
        end
      end)
  end

  def parse_buttons(buttons_str) do
    Regex.scan(~r"\(([\d,]+)\)\s*", buttons_str)
      |> Enum.map(fn [_match, button_str] ->
        String.split(button_str, ",")
          |> Enum.map(&String.to_integer/1)
        end)
      |> Enum.with_index(fn l, i -> {i, MapSet.new(l)} end)
      |> Map.new()
  end

  def parse_joltages(joltages_str) do
    String.split(joltages_str, ",")
      |> Enum.map(&String.to_integer/1)
      |> Enum.with_index(fn jolt, i -> {i, jolt} end)
      |> Map.new()
  end
  
  def parse_line(line) do
    case Regex.run(~r"\[([.#]+)\]\s*([^{]+)\s*{([^}]+)}", line) do
      [_match, target_str, buttons_str, joltages_str] ->
        %__MODULE__{
          target: parse_target(target_str),
          buttons: parse_buttons(String.trim(buttons_str)),
          joltages: parse_joltages(joltages_str)
        }
    end
  end

  def parse(lines) do
    Enum.map(lines, &parse_line/1)
      |> Enum.map(&compute_parity_map/1)
  end

  def joltage_parity(joltages) do
    Enum.reduce(joltages, 0, fn {j, v}, parity ->
      Bitwise.bor(parity, Bitwise.bsl(Bitwise.band(v, 1), j))
    end)
  end
  
  def count_bpushes(bpushed, acc \\ 0) do
    if bpushed == 0 do
      acc
    else
      bpushed = Bitwise.band(bpushed, bpushed - 1)
      count_bpushes(bpushed, acc + 1)
    end
  end

  def compute_parity_map(machine = %__MODULE__{buttons: buttons}) do
    bpush_options = 0..(Bitwise.bsl(1, map_size(buttons)) - 1)
    parity_map = Enum.reduce(bpush_options, %{}, fn bpushed, map ->
      delta_jolts = Enum.reduce(buttons, %{}, fn {i, set}, djolts ->
        if Bitwise.band(1, Bitwise.bsr(bpushed, i)) == 1 do
          Enum.reduce(set, djolts, fn j, djolts ->
            Map.update(djolts, j, 1, fn v -> v + 1 end)
          end)
        else
          djolts
        end
      end)
      parity = joltage_parity(delta_jolts)
      push_count = count_bpushes(bpushed)
      push = {push_count, bpushed, delta_jolts}
      Map.update(map, parity, [push], fn pushes -> [push | pushes] end)
    end)
    %__MODULE__{machine | parity_map: parity_map}
  end

  def solve1(%__MODULE__{target: target, parity_map: parity_map}) do
    parity_map[target]
      |> Enum.map(fn {push_count, _bpushed, _delta_jolts} -> push_count end)
      |> Enum.min()
  end

  def reduced_joltages(joltages, delta_jolts) do
    Enum.reduce(delta_jolts, joltages, fn {j, dv}, joltages ->
      Map.update!(joltages, j, fn v -> v - dv end)
    end)
  end

  def solve2(mach = %__MODULE__{joltages: joltages, parity_map: parity_map}) do
    if Enum.all?(joltages, fn {_j, v} -> v == 0 end) do
      0
    else
      target = joltage_parity(joltages)
      push_options = Map.get(parity_map, target, [])
      Enum.map(push_options, fn {push_count, _bpushed, delta_jolts} ->
        rjoltages = reduced_joltages(joltages, delta_jolts)
        {push_count, rjoltages}
      end)
        |> Enum.reject(fn {_count, rjoltages} ->
            Enum.any?(rjoltages, fn {j, v} ->
              if Bitwise.band(v, 1) == 1, do: raise("odd joltage #{v} at #{j}")
              v < 0
            end)
          end)
        |> Enum.map(fn {count, rjoltages} ->
            rjoltages = Enum.map(rjoltages, fn {j, v} -> {j, Bitwise.bsr(v, 1)} end)
              |> Map.new()
            count2 = solve2(%__MODULE__{mach | joltages: rjoltages})
            if count2 != nil do
              count + Bitwise.bsl(count2, 1)
            end
          end)
        |> Enum.min(fn -> nil end)
    end
  end
end

Where Next?

Trending in Challenges Top

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New
type1fool
I just stumbled on a newly redesigned elixir-lang.org. :tada: It looks like @Software_Mansion did the work, and I think it is generally a...
New
akoutmos
@hugobarauna and I (Alex Koutmos) have been hard at work on writing a book on Nerves that takes you from simply blinking LEDs to building...
New

We're in Beta

About us Mission Statement