Advent of Code 2025 - Day 3

2025 Dec 03

Lobby

defmodule Joltage do
  def parse_line(s) do
    String.trim(s)
      |> to_charlist()
      |> Enum.map(fn ch ->
        if ch <= ?9 and ch >= ?0 do
          ch - ?0
        end
      end)
  end

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

  def max_prefix(bank) do
    [_last | rbank] = Enum.reverse(bank)
    Enum.max(rbank)
  end

  def max_joltage(bank) do
    max1 = max_prefix(bank)
    [_max1 | tail] = Enum.drop_while(bank, fn j -> j < max1 end)
    max1 * 10 + Enum.max(tail)
  end

  def sum_max_joltages(banks) do
    Enum.reduce(banks, 0, fn bank, sum ->
      sum + max_joltage(bank)
    end)
  end
end
test_banks = """
987654321111111
811111111111119
234234234234278
818181911112111
""" |> String.split("\n", trim: true)
  |> Joltage.parse()
  |> IO.inspect()
Enum.map(test_banks, &Joltage.max_joltage/1)
  |> IO.inspect(charlists: :as_lists)
Joltage.sum_max_joltages(test_banks)
input_banks = File.stream!(__DIR__ <> "/dec-03-input.txt")
  |> Joltage.parse()
Joltage.sum_max_joltages(input_banks)

Part Two

defmodule Joltage2 do
  def max_prefix(bank, n) do
    Enum.reverse(bank)
      |> Enum.drop(n - 1)
      |> Enum.max()
  end
  
  @batteries 12

  def max_joltage(bank, n \\ @batteries, acc \\ 0) do
    if n < 1 do
      acc
    else
      max1 = max_prefix(bank, n)
      [_max1 | tail] = Enum.drop_while(bank, fn j -> j < max1 end)
      max_joltage(tail, n - 1, acc * 10 + max1)
    end
  end

  def sum_max_joltages(banks) do
    Enum.reduce(banks, 0, fn bank, sum ->
      sum + max_joltage(bank)
    end)
  end
end
Enum.map(test_banks, &Joltage2.max_joltage/1)
  |> IO.inspect(charlists: :as_lists)
Joltage2.sum_max_joltages(test_banks)
Joltage2.sum_max_joltages(input_banks)
2 Likes

A bit overengineered but runs under 3ms on my machine so good enough I guess

defmodule AdventOfCode.Solutions.Y25.Day03 do
  alias AoC.Input

  def parse(input, _part) do
    input
    |> Input.stream!(trim: true)
    |> Enum.map(&Integer.digits(String.to_integer(&1)))
  end

  def part_one(banks) do
    Enum.sum_by(banks, &best_jolts_pair/1)
  end

  defp best_jolts_pair(digits) do
    {last, firsts} = List.pop_at(digits, -1)
    first = Enum.max(firsts)
    [^first | rest] = Enum.drop_while(firsts, &(&1 != first))
    second = Enum.max([last | rest])
    first * 10 + second
  end

  def part_two([first | _] = banks) do
    len = length(first)
    Enum.sum_by(banks, &best_jolts(&1, len))
  end

  defp best_jolts(bank, len) do
    init_candidate = {len, bank, 0}

    {_, _, jolts} =
      Enum.reduce(11..0//-1, init_candidate, fn need_remaining_digits, candidate ->
        {rest_len, bank_rest, jolts} = candidate
        best_candidate(bank_rest, rest_len, need_remaining_digits, jolts)
      end)

    jolts
  end

  defp best_candidate(bank, len, need_remaining_digits, jolts) do
    max_index_plus_one = len - need_remaining_digits
    {candidates, _rest} = Enum.split(bank, max_index_plus_one)

    {value, index} =
      candidates
      |> Enum.with_index()
      |> Enum.reduce({0, 0}, fn {value, index}, {best_val, best_index} ->
        case value do
          n when n > best_val -> {n, index}
          _ -> {best_val, best_index}
        end
      end)

    {_, rest} = Enum.split(bank, index + 1)
    rest_len = len - index - 1
    {rest_len, rest, jolts * 10 + value}
  end
end

2 Likes

My top-down dynamic programming solution for both parts (omitting the input parsing part):

defmodule AoC2025.Day3 do
  @spec solve([tuple()], pos_integer()) :: pos_integer()
  def solve(input, on_count) do
    Enum.sum_by(input, fn line ->
      dp(line, on_count - 1, 0, %{})[{on_count - 1, 0}]
    end)
  end

  @spec dp(tuple(), pow, index, memo) :: memo
        when memo: %{optional({pow, index}) => non_neg_integer()},
             pow: -1 | non_neg_integer(),
             index: non_neg_integer()
  defp dp(_line, -1, _i, memo) do
    memo
  end

  defp dp(line, _pow, i, memo) when i >= tuple_size(line) do
    memo
  end

  defp dp(line, pow, i, memo) do
    if memo[{pow, i}] do
      memo
    else
      curr = elem(line, i) * (10 ** pow)
      memo = dp(line, pow - 1, i + 1, memo)
      memo = dp(line, pow, i + 1, memo)

      cond do
        pow == 0 && !memo[{pow, i + 1}] ->
          Map.put(memo, {pow, i}, curr)

        pow == 0 ->
          Map.put(memo, {pow, i}, max(curr, memo[{pow, i + 1}]))

        !memo[{pow - 1, i + 1}] && !memo[{pow, i + 1}] ->
          memo

        !memo[{pow - 1, i + 1}] ->
          Map.put(memo, {pow, i}, memo[{pow, i + 1}])

        !memo[{pow, i + 1}] ->
          Map.put(memo, {pow, i}, curr + memo[{pow - 1, i + 1}])

        true ->
          Map.put(memo, {pow, i}, max(memo[{pow, i + 1}], curr + memo[{pow - 1, i + 1}]))
      end
    end
  end
end

Part 1

AoC2025.Day3.solve(input, 2)

Part 2

AoC2025.Day3.solve(input, 12)

By the way, input is like

[
  {9, 8, 7, 6, 5, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1},
  {8, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 9},
  {2, 3, 4, 2, 3, 4, 2, 3, 4, 2, 3, 4, 2, 7, 8},
  {8, 1, 8, 1, 8, 1, 9, 1, 1, 1, 1, 2, 1, 1, 1}
] 
1 Like

The algorithm is pretty straightforward: to take k largest batteries from a list of n, take the earliest largest digit among the first (n - k + 1), then iterate for all the digits after that one.

defmodule Y2025.Day03 do
  def digits(s) do
    s
    |> String.graphemes()
    |> Enum.map(&String.to_integer(&1))
  end

  def first_digit(digits, k) do
    n = length(digits)

    digits
    |> Enum.take(n - k + 1)
    |> Enum.with_index()
    |> Enum.max_by(fn {a, i} -> {a, -i} end)
  end

  def max_digits(digits, k) do
    k..1//-1
    |> Enum.reduce({digits, 0}, fn k, {digits, acc} ->
      {d, pos} = first_digit(digits, k)
      {digits |> Enum.drop(pos + 1), acc * 10 + d}
    end)
    |> elem(1)
  end

  def max_joltage(s, k \\ 2) do
    digits(s)
    |> max_digits(k)
  end

  def part1(s, k \\ 2) do
    s
    |> String.split("\n")
    |> Enum.map(&max_joltage(&1, k))
    |> Enum.sum()
  end

  def part2(s) do
    part1(s, 12)
  end
end
4 Likes

Brute force, after all.

  defmodule Day3 do
    @input "day3_1.input" |> File.read!() |> String.trim()

    @phase1 false

    if @phase1 do
      defp parse(<<d1::binary-size(1), d2::binary-size(1), rest::binary>>),
        do: do_parse_1(rest, {String.to_integer(d1), String.to_integer(d2)})

      defp do_parse("", {d1, d2}), do: d1 * 10 + d2

      defp do_parse(<<d::binary-size(1), rest::binary>>, {d1, d2}) do
        d = String.to_integer(d)

        acc =
          cond do
            d > d1 and rest != "" -> {d, -1}
            d > d2 -> {d1, d}
            true -> {d1, d2}
          end

        do_parse(rest, acc)
      end
    else
      import Aoc2025.H

      defp parse(<<d::binary-size(1), rest::binary>>),
        do: do_parse(rest, {String.to_integer(d), unquote_splicing(minus_ones(11))})

      defp do_parse("", digits),
        do: digits |> Tuple.to_list() |> Integer.undigits()

      defp do_parse(
             <<d::binary-size(1), rest::binary>>,
             {d1, d2, d3, d4, d5, d6, d7, d8, d9, d10, d11, d12}
           ) do
        d = String.to_integer(d)

        acc =
          cond do
            d > d1 and byte_size(rest) >= 11 -> {d, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}
            d > d2 and byte_size(rest) >= 10 -> {d1, d, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1}
            d > d3 and byte_size(rest) >= 9 -> {d1, d2, d, -1, -1, -1, -1, -1, -1, -1, -1, -1}
            d > d4 and byte_size(rest) >= 8 -> {d1, d2, d3, d, -1, -1, -1, -1, -1, -1, -1, -1}
            d > d5 and byte_size(rest) >= 7 -> {d1, d2, d3, d4, d, -1, -1, -1, -1, -1, -1, -1}
            d > d6 and byte_size(rest) >= 6 -> {d1, d2, d3, d4, d5, d, -1, -1, -1, -1, -1, -1}
            d > d7 and byte_size(rest) >= 5 -> {d1, d2, d3, d4, d5, d6, d, -1, -1, -1, -1, -1}
            d > d8 and byte_size(rest) >= 4 -> {d1, d2, d3, d4, d5, d6, d7, d, -1, -1, -1, -1}
            d > d9 and byte_size(rest) >= 3 -> {d1, d2, d3, d4, d5, d6, d7, d8, d, -1, -1, -1}
            d > d10 and byte_size(rest) >= 2 -> {d1, d2, d3, d4, d5, d6, d7, d8, d9, d, -1, -1}
            d > d11 and byte_size(rest) >= 1 -> {d1, d2, d3, d4, d5, d6, d7, d8, d9, d10, d, -1}
            d > d12 -> {d1, d2, d3, d4, d5, d6, d7, d8, d9, d10, d11, d}
            true -> {d1, d2, d3, d4, d5, d6, d7, d8, d9, d10, d11, d12}
          end

        do_parse(rest, acc)
      end
    end

    def calc(input \\ @input) do
      input
      |> String.split(["\s", "\n"], trim: true)
      |> Enum.sum_by(&parse/1)
    end
  end
1 Like

This one was a bit of fun!

Name                     ips        average  deviation         median         99th %
day 03, part 1        787.83        1.27 ms     ±3.95%        1.25 ms        1.42 ms
day 03, part 2        623.54        1.60 ms     ±4.19%        1.58 ms        1.79 ms
2 Likes

Parse

batteries =
  puzzle_input
  |> String.split("\n", trim: true)
  |> Enum.map(fn row ->
    row
    |> String.to_charlist()
    |> Enum.map(& &1 - ?0)
  end)

Implementation

defmodule Joltage do
  def make_largest(list, n) do
    to_remove = length(list) - n
    Enum.reduce(1..to_remove, list, fn _, acc -> make_larger(acc) end)
  end
  
  def make_larger([_]), do: []
  def make_larger([a, b | rest]) when a < b, do: [b | rest]
  def make_larger([b | rest]), do: [b | make_larger(rest)]
end

Part 1

Enum.sum_by(batteries, &Integer.undigits(Joltage.make_largest(&1, 2)))

Part 2

Enum.sum_by(batteries, &Integer.undigits(Joltage.make_largest(&1, 12)))

All thanks to simple observation that to make number larger you need to drop first digit that is smaller than the next one or last digit. Now apply it N times to be left with just required amount of digits and you are good to go.

10 Likes

Pretty much the same as most others.

Updated to simplify.

defmodule RAoc.Solutions.Y25.Day03 do
  alias AoC.Input

  def parse(input, _part) do
    Input.stream!(input, trim: true)
    |> Enum.map(fn line ->
      line
      |> String.split("", trim: true)
      |> Enum.map(&String.to_integer/1)
    end)
  end

  def part_one(problem) do
    problem
    |> Enum.sum_by(&highest_joltage(&1, 2))
  end

  def part_two(problem) do
    problem
    |> Enum.sum_by(&highest_joltage(&1, 12))
  end

  defp highest_joltage(list, num_batteries) do
    Enum.reduce(num_batteries..1//-1, {list, []}, fn on_battery, {list, digits} ->
      len = Enum.count(list)
      digit = Enum.take(list, len - (on_battery - 1)) |> Enum.max()
      {Enum.take(list, Enum.find_index(list, fn x -> x == digit end) + 1 - len), [digit | digits]}
    end)
    |> elem(1)
    |> Enum.reverse()
    |> Integer.undigits()
  end
end
1 Like

  def p1(input) do
    input
    |> parse_input
    |> Enum.map(&find_jolt_for(&1, [], 2))
    |> Enum.sum()
  end

  # 168617068915447
  def p2(input) do
    input
    |> parse_input
    |> Enum.map(&find_jolt_for(&1, [], 12))
    |> Enum.sum()
  end

  defp find_jolt_for(_batteries, acc, 0), do: Enum.sum(acc)

  defp find_jolt_for(batteries, acc, count) do
    {batteries_to_search, _} = Enum.split(batteries, length(batteries) - count + 1)

    {biggest_jolt, index} =
      batteries_to_search
      |> Enum.with_index()
      |> Enum.max_by(fn {v, _} -> v end)

    {_, remainder} = Enum.split(batteries, index + 1)
    jolt = 10 ** (count - 1) * biggest_jolt

    find_jolt_for(remainder, [jolt | acc], count - 1)
  end

  defp parse_input(input) do
    input
    |> String.split("\n", trim: true)
    |> Enum.map(fn line ->
      String.split(line, "", trim: true)
      |> Enum.map(&String.to_integer(&1))
    end)
  end

Not super happy with my solution today, fairly fiddly with the indexes and off-by-one’s. Will have to see in the topic what solutions work better :).

Enum.sum_by(batteries, &Integer.undigits(Joltage.make_largest(&1, 12)))

Oh! I didn’t know about this Integer.undigits, that helps!

2 Likes
defmodule Day3 do
  def file, do: Parser.read_file(3)
  def test, do: Parser.read_file("test")

  def parse(input) do
    Enum.map(input, fn batteries_bank ->
      batteries_bank
      |> String.graphemes()
      |> Enum.map(&String.to_integer/1)
    end)
  end

  def solve(input \\ file()) do
    input
    |> parse()
    |> Enum.map(&find_max(&1, 2))
    |> Enum.sum()
  end

  def solve_two(input \\ file()) do
    input
    |> parse()
    |> Enum.map(&find_max(&1, 12))
    |> Enum.sum()
  end

  def find_max(list, nb_digits) do
    list
    |> find_max_recursive(nb_digits)
    |> Integer.undigits()
  end

  def find_max_recursive(list, max_length, acc \\ [])
  def find_max_recursive(_list, 0, acc), do: acc

  def find_max_recursive(list, max_length, acc) do
    length = length(list)

    if length == max_length do
      acc ++ list
    else
      first =
        list
        |> Enum.take(length - max_length + 1)
        |> Enum.max()

      index = Enum.find_index(list, &(&1 == first))

      list
      |> Enum.split(index + 1)
      |> elem(1)
      |> find_max_recursive(max_length - 1, acc ++ [first])
    end
  end
end
1 Like

I learn some more Enum-methods when reading your solutions. This is my 3:rd day using Elixir solution :). Updated using `Integer.undigits` after seeing it here.

defmodule Advent2025Test do
  use ExUnit.Case

  def day3_data() do
    "day3.txt"
    |> File.stream!()
    |> Enum.map(fn line ->
      String.trim(line) |> String.graphemes() |> Enum.map(&String.to_integer/1)
    end)
  end

  def max_index([x]) do
    {x, []}
  end

  def max_index([x | rest]) do
    {rx, ri} = max_index(rest)

    if x >= rx do
      {x, rest}
    else
      {rx, ri}
    end
  end

  def max_nl(l, 0) do
    {x, i} = max_index(l)
    {[x], i}
  end

  def max_nl(l, n) do
    {elements, last} = Enum.split(l, -n)
    {x, i} = max_index(elements)
    {x2, rest} = max_nl(i ++ last, n - 1)
    {[x] ++ x2, rest}
  end

  def max_n(l, n) do
    {digits, _} = max_nl(l, n)
    Integer.undigits(digits)
  end

  test "day3_p1" do
    sum = day3_data() |> Enum.map(&max_n(&1, 1)) |> Enum.sum()
    IO.puts("answer p1 #{inspect(sum)}")
  end

  test "day3_p2" do
    sum = day3_data() |> Enum.map(&max_n(&1, 11)) |> Enum.sum()
    IO.puts("answer p2 #{inspect(sum)}")
  end
end
1 Like

Started with brute force. Added memoization when part 2 didn’t finish.

defmodule Day03 do
  def part1(input) do
    solve(input, 2)
  end

  def part2(input) do
    solve(input, 12)
  end

  defp solve(input, num_batteries) do
    parse(input)
    |> Enum.map(fn bank ->
      power = Integer.pow(10, num_batteries-1)
      {result, _} = find_max(bank, power, %{})
      result
    end)
    |> Enum.sum
  end

  defp find_max(_, 0, memo), do: {0, memo}
  defp find_max([], _mul, memo), do: {-10_000_000_000_000, memo}
  defp find_max([j | rest], mul, memo) do
    {amount1, memo} = memo_find_max(rest, div(mul, 10), memo)
    amount1 = j * mul + amount1
    {amount2, memo} = memo_find_max(rest, mul, memo)
    {max(amount1, amount2), memo}
  end

  defp memo_find_max(rest, mul, memo) do
    key = {mul,rest}
    case memo do
      %{^key => result} ->
        {result, memo}
      %{} ->
        {result, memo} = find_max(rest, mul, memo)
        memo = Map.put(memo, key, result)
        {result, memo}
    end
  end

  defp parse(input) do
    input
    |> Enum.map(fn line ->
      String.to_charlist(line)
      |> Enum.map(&(&1 - ?0))
    end)
  end
end

1 Like

I’d be happy to have a commented code. Your solution looks impressive but I don’t understand at all how it works! Is it a tree search with memoization ?

1 Like

My solution, after a first attempt doing a never ending search (I’ve forgotten about memoization: I should memorize memoization more often). I thought my nice optimizations would do it, but finally they didn’t.
I found a more direct way

defmodule AdventOfCode.Solution.Year2025.Day03 do
  def parse(input) do
    input
    |> String.split("\n", trim: true)
    |> Enum.map(fn bank ->
      bank |> to_charlist() |> Enum.map(&(&1 - ?0))
    end)
  end

  ### Part 1
  def largest_two(bank) do
    bank_wi = Enum.with_index(bank)
    Enum.max(for {n1, i1} <- bank_wi, {n2, i2} <- bank_wi, i1 < i2, do: n1 * 10 + n2)
  end

  def part1(input), do: parse(input) |> Enum.map(&largest_two/1) |> Enum.sum()

  ### Part 2
  # End of recursion: all switches are activated
  def search(_, cumul, -1), do: cumul

  def search(rest, cumul, n_switches) do
    # Consider the max number on the right (keeping enough numbers for all the switches that are still to activate)
    max_in_remaining = rest |> Enum.slice(0..(length(rest) - n_switches - 1)) |> Enum.max()
    # Drop the numbers to the right until (and including) this max
    {_, [_max_in_remaining | r]} = Enum.split_while(rest, &(&1 != max_in_remaining))
    # Loop recursively
    search(r, cumul + max_in_remaining * Integer.pow(10, n_switches), n_switches - 1)
  end

  def part2(input), do: parse(input) |> Enum.map(&search(&1, 0, 11)) |> Enum.sum()
end
2 Likes

Here we go.

Who knows?
How to do a ā€˜selective capture’?

The reducer passes 2 arguments to the anonymous function. Thought I could simply do (&2) but that is not allowed. &1 has to be used. The dirty trick is to (&2 || &1) when you are certain &2 will never be falsy but it is stretching the limits.

Could have gone with a simple fn but am wondering if someone knows a nice solid trick.

defmodule Aoc2025.Solutions.Y25.Day03 do
  alias AoC.Input

  def parse(input, _part) do
    Input.read!(input)
    |> String.trim()
    |> String.split("\n")
    |> Enum.map(&String.to_charlist/1)
  end

  def part_one(problem) do
    solve(problem, 2)
  end

  def part_two(problem) do
    solve(problem, 12)
  end

  def solve(problem, limit) do
    problem
    |> Stream.map(&keep_highest(&1, limit))
    |> Stream.map(&to_string/1)
    |> Stream.map(&String.to_integer/1)
    |> Enum.sum()
  end

  def keep_highest(bank, limit) do
      discard = length(bank) - limit
      Enum.reduce(1..discard, bank, &maximize/2)
  end

  def maximize(_reduction, bank), do: maximize(bank)
  def maximize([x, s]), do: [max(x, s)]
  def maximize([l, r | rest]) when l < r, do: [r | rest]
  def maximize([l, r | rest]), do: [l | maximize([r | rest])]
end

Edit 1: You can play ā€œspot the differencesā€ with @hauleth solution :slight_smile:

3 Likes

Yes, it is memoization. I don’t know what your definition of tree search is though, cuz there’s no tree in my implementation.

My memo stores the tuple {lights_need_to_turn_on, index} as the keys, and max_jolt_in_the_subarray_from_index_and_after as the values.

2 Likes

3 posts were split to a new topic: Split from Advent of Code 2025 - Day 3

#!/usr/bin/env elixir

# Advent of Code 2025. Day 3

defmodule M do
  def largest_joltage_2(bank) do
    bank
    |> String.codepoints()
    |> Enum.map(&String.to_integer/1)
    |> Enum.reduce({nil, nil}, fn
      d, {nil, nil} -> {d, nil}
      d, {d1, nil} -> {d1, d}
      d, {d1, d2} when d1*10+d > d1*10+d2 and d1*10+d > d2*10+d -> {d1, d}
      d, {d1, d2} when d2*10+d > d1*10+d2 -> {d2, d}
      _d, {d1, d2} -> {d1, d2}
    end)
    |> then(fn {d1, d2} -> d1*10+d2 end)
  end

  def largest_joltage_12(bank) do
    bank
    |> String.codepoints()
    |> Enum.map(&String.to_integer/1)
    |> Enum.reduce([], fn
      d, lst when length(lst) < 12 -> [d | lst]
      d, lst -> max_list(d, lst)
    end)
    |> lst_to_num()
  end

  defp max_list(d, lst) do
    Enum.max_by([
      lst,
      [d | List.delete_at(lst, 0)],
      [d | List.delete_at(lst, 1)],
      [d | List.delete_at(lst, 2)],
      [d | List.delete_at(lst, 3)],
      [d | List.delete_at(lst, 4)],
      [d | List.delete_at(lst, 5)],
      [d | List.delete_at(lst, 6)],
      [d | List.delete_at(lst, 7)],
      [d | List.delete_at(lst, 8)],
      [d | List.delete_at(lst, 9)],
      [d | List.delete_at(lst, 10)],
      [d | List.delete_at(lst, 11)],
    ], &lst_to_num/1)
  end
  defp lst_to_num(lst), do: lst |> Enum.reverse() |> Enum.reduce(0, fn d, total -> total*10+d end)
end

# Part 1
File.read!("../day03.txt")
|> String.split()
|> Enum.map(&M.largest_joltage_2/1)
|> Enum.sum()
|> IO.inspect(label: "Day 3. Part 1")

# Part 2
File.read!("../day03.txt")
|> String.split()
|> Enum.map(&M.largest_joltage_12/1)
|> Enum.sum()
|> IO.inspect(label: "Day 3. Part 2")
1 Like

Ooops forgot about Integer.undigits/2.

1 Like

This is really nice. I also today TIL learned Integer.undigits/2, although replacing a string round-trip with that didn’t make my solution faster :sweat_smile: