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.

8 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