Advent of Code 2024 - Day 7

This one was much easier for me than yesterday’s. Part 1 runs in 22ms and part 2 runs in ~3s.

defmodule BridgeRepair do
  def parse_input(input) do
     input
        |> String.trim()
        |> String.split("\n")
        |> Stream.map(fn l -> 
          [acc, nums] = String.split(l, ":")
          nums = nums |> String.trim() |> String.split(" ") |> Enum.map(&String.to_integer/1)
          {acc |> String.to_integer(), nums}
        end)
  end
  def combine_ints(acc, [], _), do: acc
  def combine_ints(acc, [i | rest], ops) do
    ops
      |> Enum.flat_map(fn op -> 
        acc |> Enum.map(fn j ->
          case op do
            :plus -> j + i
            :mul -> j * i
            :cat -> "#{j}#{i}" |> String.to_integer()
          end
        end)
      end)
    |> combine_ints(rest, ops)
  end

  def part_one_ops() do
    [:plus, :mul]
  end

  def part_two_ops() do
    [:cat | part_one_ops()]
  end
  
  def solve(input, part) do
    input 
     |> BridgeRepair.parse_input()
     |> Stream.filter(fn {sum, [i | rest]} -> 
        BridgeRepair.combine_ints(
          [i],
          rest, 
          (if part == :part1, do: part_one_ops(), else: part_two_ops())
        ) |> Enum.any?(&(&1 == sum)) 
      end)
    |> Stream.map(fn {i, _} -> i end)
    |> Enum.sum
  end
end

Edit small optimization on the concat operation:

:cat -> j*10**(i |> Integer.digits() |> Enum.count) + i

reduces part 2 runtime to 800ms.

2 Likes

I like recursion :slight_smile: Back in 1996-98, I did a lot of Scheme (MIT Scheme) and fell in love with that language, FP in general and tail recursion (and TCO of course). We had a teacher in Belgium who went to the USAs and was exposed to all of it. I had a really good time.

Excluding I/O, when I call C for the produced? function using a NIF, the code executes ± 3 times faster.

#!/usr/bin/env elixir

# AoC 2024. day 7.

###########################################################
# Part 1

defmodule Part1 do
  def produced?(test_value, numbers), do: produced?(test_value, numbers, nil)
  def produced?(test_value, [], value), do: value == test_value
  def produced?(test_value, [x | numbers], nil), do: produced?(test_value, numbers, x)
  def produced?(test_value, _numbers, value) when value > test_value, do: false
  def produced?(test_value, [x | numbers], value) do
    produced?(test_value, numbers, value*x) || produced?(test_value, numbers, value+x)
  end
end

File.stream!("../day07.txt")
|> Stream.map(fn line -> Regex.scan(~r/\d+/, line) |> Enum.map(fn [x] -> String.to_integer(x) end) end)
|> Enum.reduce(0, fn [test_value | numbers], sum ->
  if Part1.produced?(test_value, numbers), do: sum+test_value, else: sum
end)
|> IO.inspect(label: "Part 1")

###########################################################
# Part 2

defmodule Part2 do
  def produced?(test_value, numbers), do: produced?(test_value, numbers, nil)
  def produced?(test_value, [], value), do: value == test_value
  def produced?(test_value, [x | numbers], nil), do: produced?(test_value, numbers, x)
  def produced?(test_value, _numbers, value) when value > test_value, do: false
  def produced?(test_value, [x | numbers], value) do
    produced?(test_value, numbers, value*x) ||
    produced?(test_value, numbers, value+x) ||
    produced?(test_value, numbers, value*10**ndigits(x)+x)
  end
  @spec ndigits(pos_integer()) :: pos_integer()
  defp ndigits(x), do: trunc(:math.floor(:math.log(x)/:math.log(10))+1)
end

File.stream!("../day07.txt")
|> Stream.map(fn line -> Regex.scan(~r/\d+/, line) |> Enum.map(fn [x] -> String.to_integer(x) end) end)
|> Enum.reduce(0, fn [test_value | numbers], sum ->
  if Part2.produced?(test_value, numbers), do: sum+test_value, else: sum
end)
|> IO.inspect(label: "Part 2")
2 Likes

My original version ran in 0.9 seconds for both parts. Since that was a little bit slow for my taste I optimized the concatenation of integers. My first version did it like so:

String.to_integer(Integer.to_string(a) <> Integer.to_string(b))

I rewrote that to operate directly on the integers. That reduced the time to 0.1 seconds for both parts.

The version shown here includes additional refactoring to share the solutions for parts 1 and 2.

My solution is to inverse the calculation from right to left, for example:

3267: 81 40 27

I try

3267: 27 40 81  (initial)
  3240 (= 3267 - 27): 40 81
    3200 (= 3240 - 40): 81
      3119 (= 3200 - 81):  => false
    81 (= 3140 / 40): 81
      0 (= 81 - 81):  => true

I accidentally deleted my code ToT

And for the inverse of concatenation, it’s just

defp truncate(a, b) when a < b, do: nil

defp truncate(a, 0), do: a

defp truncate(a, b) when rem(a, 10) != rem(b, 10), do: nil

defp truncate(a, b), do: truncate(div(a, 10), div(b, 10))

1 Like

When you measure time execution, do you take I/O into consideration or not (time taken to read the file) ? I wanted to avoid it but since I am using Stream I/O and CPU are interleaved.

1 Like

I rewrote my solution.

defmodule AoC2024.Day07 do
  def part_1(input) do
    input
    |> Enum.filter(fn {result, rev_operands} ->
      solvable_1?(result, rev_operands)
    end)
    |> Enum.map(&elem(&1, 0))
    |> Enum.sum()
  end

  def part_2(input) do
    input
    |> Enum.filter(fn {result, rev_operands} ->
      solvable_2?(result, rev_operands)
    end)
    |> Enum.map(&elem(&1, 0))
    |> Enum.sum()
  end

  defp solvable_1?(target, []) do
    target == 0
  end

  defp solvable_1?(target, [h | t]) do
    solvable_1?(target - h, t) or
    (rem(target, h) == 0 and solvable_1?(div(target, h), t))
  end

  defp solvable_2?(target, []) do
    target == 0
  end
  
  defp solvable_2?(target, [h | t]) do
    solvable_2?(target - h, t) or
    (rem(target, h) == 0 and solvable_2?(div(target, h), t)) or
    ((truncated = truncate(target, h)) && solvable_2?(truncated, t))
  end

  defp truncate(a, b) when a < b, do: false

  defp truncate(a, 0), do: a

  defp truncate(a, b) when rem(a, 10) != rem(b, 10), do: false

  defp truncate(a, b) do
    truncate(div(a, 10), div(b, 10))
  end
end
2 Likes

Much easier than yesterday! I still went through a few iterations before landing on this one:

This takes about 250ms to run on my machine for part 2.

My aim for this year is to have each solution run in less than a second - we’ll see how far I get lol

3 Likes

I use the time reported by mix test.

Running ExUnit with seed: 794416, max_cases: 16

....
Finished in 0.1 seconds (0.00s async, 0.1s sync)
4 tests, 0 failures

So that includes all I/O, as well as the time for running the examples.

2 Likes

20 lines

3 Likes

Relatively simple day!

defmodule AOC.Y2024.Day7 do
  @moduledoc false

  use AOC.Solution

  @impl true
  def load_data() do
    Data.load_day(2024, 7)
    |> Enum.map(fn line -> String.split(line, ":") end)
    |> Enum.map(fn [test_value, numbers] ->
      {String.to_integer(test_value),
       numbers |> String.split(" ", trim: true) |> Enum.map(&String.to_integer/1)}
    end)
  end

  @impl true
  def part_one(data) do
    data
    |> Enum.filter(fn {test_value, numbers} -> form_test_value?(numbers, test_value) end)
    |> General.map_sum(fn {test_value, _} -> test_value end)
  end

  @impl true
  def part_two(data) do
    data
    |> Enum.filter(fn {test_value, numbers} -> form_test_value?(numbers, test_value, true) end)
    |> General.map_sum(fn {test_value, _} -> test_value end)
  end

  defp form_test_value?(numbers, test_value, has_concat \\ false)
  defp form_test_value?([acc | _], test_value, _) when acc > test_value, do: false
  defp form_test_value?([test_value], test_value, _), do: true
  defp form_test_value?([_], _, _), do: false

  defp form_test_value?([a | [b | rest]], test_value, has_concat) do
    initial =
      form_test_value?([a * b | rest], test_value, has_concat) or
        form_test_value?([a + b | rest], test_value, has_concat)

    if has_concat do
      initial or form_test_value?([String.to_integer("#{a}#{b}") | rest], test_value, has_concat)
    else
      initial
    end
  end
end
1 Like

This was much easier than yesterday!

And I’m always proud of myself when I manage to write recursive code (mostly because I don’t need recursive code very often in my daily job)

Part1

defmodule Advent.Y2024.Day07.Part1 do
  def run(puzzle) do
    puzzle
    |> parse()
    |> Enum.filter(fn {result, nums} -> eval(result, nums) end)
    |> Enum.map(&elem(&1, 0))
    |> Enum.sum()
  end

  def parse(puzzle) do
    puzzle
    |> String.split("\n")
    |> Enum.map(fn equation ->
      [result, numbers] = String.split(equation, ": ")
      numbers = numbers |> String.split(" ") |> Enum.map(&String.to_integer/1)
      {String.to_integer(result), numbers}
    end)
  end

  def eval(result, numbers, total \\ nil)
  def eval(result, [], total), do: total == result
  def eval(result, [number | tail], nil), do: eval(result, tail, number)
  def eval(result, _numbers, total) when total > result, do: false

  def eval(result, [number | tail], total) do
    eval(result, tail, total + number) or eval(result, tail, total * number)
  end
end

Part2

defmodule Advent.Y2024.Day07.Part2 do
  alias Advent.Y2024.Day07.Part1

  def run(puzzle) do
    puzzle
    |> Part1.parse()
    |> Task.async_stream(fn {result, nums} -> {eval(result, nums), result} end)
    |> Stream.map(fn {:ok, output} -> output end)
    |> Stream.filter(fn {eval, _result} -> eval end)
    |> Stream.map(fn {_eval, result} -> result end)
    |> Enum.sum()
  end

  def eval(result, numbers, acc \\ nil)
  def eval(result, [], acc), do: acc == result
  def eval(result, [number | tail], nil), do: eval(result, tail, number)
  def eval(result, _numbers, acc) when acc > result, do: false

  def eval(result, [number | tail], acc) do
    eval(result, tail, acc + number) or
      eval(result, tail, acc * number) or
      eval(result, tail, concat(acc, number))
  end

  defp concat(a, b) when b < 10, do: a * 10 + b
  defp concat(a, b) when b < 100, do: a * 100 + b
  defp concat(a, b) when b < 1000, do: a * 1000 + b
end

It takes about 10ms for part1 and 40ms for part2
edit: using Task.async_stream and part 2 is now running in 20ms

2 Likes

Here’s mine for today, much easier than yesterday, with some simple optimisations got it down to.

Solution for 2024 day 7
part_one: 12553187650171 in 6.33ms
part_two: 96779702119491 in 71.4ms

Not at all happy with the conditional logic/code duplication, so might take some time to tidy that up later, but it works at least!


    defmodule Aoc2024.Solutions.Y24.Day07 do
      alias AoC.Input

      def parse(input, _part) do
        Input.read!(input)
        |> String.split("\n", trim: true)
        |> Enum.map(fn ip ->
          [total | options] = String.split(ip, " ")
          [total | _] = String.split(total, ":")
          {total |> String.to_integer(), options |> Enum.map(&String.to_integer/1)}
        end)
      end

      def part_one(problem) do
        problem
        |> split_into_chunks()
        |> Task.async_stream(&do_calculations/1)
        |> merge_results_stream()
        |> Enum.filter(fn {_total, valid} ->
          valid
        end)
        |> Enum.reduce(0, fn {total, _}, acc ->
          acc + total
        end)
      end

      def do_calculations(problem) do
        problem
        |> Enum.map(fn {total, [current | rest]} ->
          {total, do_calculation(total, rest, current)}
        end)
      end

      def do_calculation(total, [], current) do
        [current == total]
      end

      def do_calculation(total, [option | rest], current) do
        multi_valid = check_multi_calculation(total, option, current)
        add_valid = check_plus_calculation(total, option, current)

        cond do
          multi_valid and add_valid ->
            [
              do_calculation(total, rest, current * option),
              do_calculation(total, rest, current + option)
            ]
            |> List.flatten()
            |> Enum.filter(& &1)
            |> List.first(false)

          multi_valid ->
            [do_calculation(total, rest, current * option)]
            |> List.flatten()
            |> Enum.filter(& &1)
            |> List.first(false)

          add_valid ->
            [do_calculation(total, rest, current + option)]
            |> List.flatten()
            |> Enum.filter(& &1)
            |> List.first(false)

          true ->
            false
        end
      end

      def check_multi_calculation(total, option, current)
          when option * current <= total do
        true
      end

      def check_multi_calculation(_total, _options, _current), do: false

      def check_plus_calculation(total, option, current)
          when option + current <= total do
        true
      end

      def check_plus_calculation(_total, _options, _current), do: false

      def check_concat_calculation(total, option, current) do
        concat_number = current * trunc(:math.pow(10, trunc(:math.log10(option)) + 1)) + option

        if concat_number <= total do
          {true, concat_number}
        else
          {false, 0}
        end
      end

      def part_two(problem) do
        problem
        |> split_into_chunks()
        # |> do_calculations_part_2()
        |> Task.async_stream(fn options ->
          do_calculations_part_2(options)
        end)
        |> merge_results_stream()
        |> Enum.filter(fn {_total, valid} ->
          valid
        end)
        |> Enum.reduce(0, fn {total, _}, acc ->
          acc + total
        end)
      end

      def do_calculations_part_2(problem) do
        problem
        |> Enum.map(fn {total, [current | rest]} ->
          {total, do_calculation_part_2(total, rest, current)}
        end)
      end

      defp split_into_chunks(options) do
        workers = :erlang.system_info(:schedulers_online)
        options_count = Enum.count(options)
        options_per_chunk = :erlang.ceil(options_count / workers)

        Enum.chunk_every(options, options_per_chunk)
      end

      defp merge_results_stream(results_stream) do
        Enum.reduce(results_stream, [], fn {:ok, worker_result}, acc ->
          acc ++ worker_result
        end)
      end

      def do_calculation_part_2(total, [], current) do
        [current == total]
      end

      def do_calculation_part_2(total, [option | rest], current) do
        multi_valid = check_multi_calculation(total, option, current)
        add_valid = check_plus_calculation(total, option, current)
        {concat_valid, concat_value} = check_concat_calculation(total, option, current)

        cond do
          multi_valid and add_valid and concat_valid ->
            [
              do_calculation_part_2(total, rest, current * option),
              do_calculation_part_2(total, rest, current + option),
              do_calculation_part_2(total, rest, concat_value)
            ]
            |> List.flatten()
            |> Enum.filter(& &1)
            |> List.first(false)

          multi_valid and concat_valid ->
            [
              do_calculation_part_2(total, rest, current * option),
              do_calculation_part_2(total, rest, concat_value)
            ]
            |> List.flatten()
            |> Enum.filter(& &1)
            |> List.first(false)

          multi_valid and add_valid ->
            [
              do_calculation_part_2(total, rest, current * option),
              do_calculation_part_2(total, rest, current + option)
            ]
            |> List.flatten()
            |> Enum.filter(& &1)
            |> List.first(false)

          add_valid and concat_valid ->
            [
              do_calculation_part_2(total, rest, current + option),
              do_calculation_part_2(total, rest, concat_value)
            ]
            |> List.flatten()
            |> Enum.filter(& &1)
            |> List.first(false)

          multi_valid ->
            [do_calculation_part_2(total, rest, current * option)]
            |> List.flatten()
            |> Enum.filter(& &1)
            |> List.first(false)

          add_valid ->
            [do_calculation_part_2(total, rest, current + option)]
            |> List.flatten()
            |> Enum.filter(& &1)
            |> List.first(false)

          concat_valid ->
            [do_calculation_part_2(total, rest, concat_value)]
            |> List.flatten()
            |> Enum.filter(& &1)
            |> List.first(false)

          true ->
            false
        end
      end
    end

Did some refactoring and Task work so I got it down to sub 100ms

defmodule AOC.Y2024.Day7 do
  @moduledoc false

  use AOC.Solution

  @impl true
  def load_data() do
    Data.load_day(2024, 7)
    |> Enum.map(fn line -> String.split(line, ": ") end)
    |> Enum.map(fn [test_value, numbers] ->
      {String.to_integer(test_value),
       numbers |> String.split(" ") |> Enum.map(&String.to_integer/1)}
    end)
  end

  @impl true
  def part_one(data) do
    solve(data, false)
  end

  @impl true
  def part_two(data) do
    solve(data, true)
  end

  defp solve(data, has_concat) do
    data
    |> Enum.chunk_every(20)
    |> Task.async_stream(fn chunk ->
      chunk
      |> Enum.filter(fn {test_value, numbers} ->
        form_test_value?(numbers, test_value, has_concat)
      end)
      |> General.map_sum(fn {test_value, _} -> test_value end)
    end)
    |> Enum.reduce(0, fn {:ok, res}, acc -> acc + res end)
  end

  defp form_test_value?([acc | _], test_value, _) when acc > test_value, do: false
  defp form_test_value?([test_value], test_value, _), do: true
  defp form_test_value?([_], _, _), do: false

  defp form_test_value?([a | [b | rest]], test_value, false) do
    form_test_value?([a * b | rest], test_value, false) or
      form_test_value?([a + b | rest], test_value, false)
  end

  defp form_test_value?([a | [b | rest]], test_value, true) do
    form_test_value?([a * b | rest], test_value, true) or
      form_test_value?([a + b | rest], test_value, true) or
      form_test_value?([String.to_integer("#{a}#{b}") | rest], test_value, true)
  end
end

Slow and naive, using a stream of all possible combinations of operators:

defmodule AdventOfCode.Solutions.Y24.Day07 do
  alias AdventOfCode.Combinations
  alias AoC.Input

  def parse(input, _part) do
    Input.stream!(input, trim: true) |> Enum.map(&parse_line/1)
  end

  defp parse_line(line) do
    [result, operation] = String.split(line, ":")

    operands =
      operation
      |> String.split(" ", trim: true)
      |> Enum.map(&String.to_integer/1)

    {String.to_integer(result), operands}
  end

  def part_one(problem) do
    solve(problem, [:*, :+])
  end

  def part_two(problem) do
    solve(problem, [:*, :+, :||])
  end

  defp solve(problem, operators) do
    problem
    |> Enum.filter(&can_be_computed?(&1, operators))
    |> Enum.map(&elem(&1, 0))
    |> Enum.sum()
  end

  defp can_be_computed?({result, operands}, operators) do
    operators_combins = Combinations.of(operators, length(operands) - 1)
    Enum.any?(operators_combins, fn c -> result == compute(operands, c) end)
  end

  defp compute([h | operands], operators) do
    Enum.reduce(Enum.zip(operators, operands), h, fn
      {:+, n}, acc -> acc + n
      {:*, n}, acc -> acc * n
      {:||, n}, acc -> cat(acc, n)
    end)
  end

  defp cat(a, b) do
    Integer.undigits(Integer.digits(a) ++ Integer.digits(b))
  end
end

Edit 1

Final optimised version for me, pretty happy with this.

    defmodule Aoc2024.Solutions.Y24.Day07 do
      alias AoC.Input

      def parse(input, _part) do
        Input.read!(input)
        |> String.split("\n", trim: true)
        |> Enum.map(fn ip ->
          [total | options] = String.split(ip, " ")
          [total | _] = String.split(total, ":")
          {total |> String.to_integer(), options |> Enum.map(&String.to_integer/1)}
        end)
      end

      def part_one(problem) do
        do_work(problem, :part_one)
      end

      def part_two(problem) do
        do_work(problem, :part_two)
      end

      def do_work(problem, part) do
        problem
        |> split_into_chunks()
        |> Task.async_stream(fn options ->
          do_calculations(options, part)
        end)
        |> merge_results_stream()
        |> Enum.filter(fn {_total, valid} ->
          valid
        end)
        |> Enum.reduce(0, fn {total, _}, acc ->
          acc + total
        end)
      end

      def do_calculations(problem, part) do
        problem
        |> Enum.map(fn {total, [current | rest]} ->
          {total, do_calculation(total, rest, current, part)}
        end)
      end

      def do_calculation(total, _, current, _part) when current > total do
        false
      end

      def do_calculation(total, [], current, _part) do
        current == total
      end

      def do_calculation(total, [option | rest], current, part) do
        case part do
          :part_one ->
            do_calculation(total, rest, current * option, part) ||
              do_calculation(total, rest, current + option, part)

          :part_two ->
            concat_value = get_concat_calculation(option, current)

            do_calculation(total, rest, current * option, part) ||
              do_calculation(total, rest, current + option, part) ||
              do_calculation(total, rest, concat_value, part)
        end
      end

      def get_concat_calculation(option, current) do
        current * trunc(:math.pow(10, trunc(:math.log10(option)) + 1)) + option
      end

      defp split_into_chunks(options) do
        workers = :erlang.system_info(:schedulers_online)
        options_count = Enum.count(options)
        options_per_chunk = :erlang.ceil(options_count / workers)

        Enum.chunk_every(options, options_per_chunk)
      end

      defp merge_results_stream(results_stream) do
        Enum.reduce(results_stream, [], fn {:ok, worker_result}, acc ->
          acc ++ worker_result
        end)
      end
    end

Original

Slightly updated version removing the lists to accumulate results, realised I could just return a bool, doh! Still struggle sometimes with following the recursion login, more Coffee needed.

Solution for 2024 day 7
part_one: 12553187650171 in 4.05ms
part_two: 96779702119491 in 36.17ms

    defmodule Aoc2024.Solutions.Y24.Day07 do
      alias AoC.Input

      def parse(input, _part) do
        Input.read!(input)
        |> String.split("\n", trim: true)
        |> Enum.map(fn ip ->
          [total | options] = String.split(ip, " ")
          [total | _] = String.split(total, ":")
          {total |> String.to_integer(), options |> Enum.map(&String.to_integer/1)}
        end)
      end

      def part_one(problem) do
        problem
        |> split_into_chunks()
        |> Task.async_stream(&do_calculations/1)
        |> merge_results_stream()
        |> Enum.filter(fn {_total, valid} ->
          valid
        end)
        |> Enum.reduce(0, fn {total, _}, acc ->
          acc + total
        end)
      end

      def do_calculations(problem) do
        problem
        |> Enum.map(fn {total, [current | rest]} ->
          {total, do_calculation(total, rest, current)}
        end)
      end

      def do_calculation(total, [], current) do
        current == total
      end

      def do_calculation(total, [option | rest], current) do
        multi_valid = check_multi_calculation(total, option, current)
        add_valid = check_plus_calculation(total, option, current)

        cond do
          multi_valid and add_valid ->
            do_calculation(total, rest, current * option) ||
              do_calculation(total, rest, current + option)

          # |> List.flatten()
          # |> Enum.filter(& &1)
          # |> List.first(false)

          multi_valid ->
            do_calculation(total, rest, current * option)

          # |> List.flatten()
          # |> Enum.filter(& &1)
          # |> List.first(false)

          add_valid ->
            do_calculation(total, rest, current + option)

          # |> List.flatten()
          # |> Enum.filter(& &1)
          # |> List.first(false)

          true ->
            false
        end
      end

      def check_multi_calculation(total, option, current)
          when option * current <= total do
        true
      end

      def check_multi_calculation(_total, _options, _current), do: false

      def check_plus_calculation(total, option, current)
          when option + current <= total do
        true
      end

      def check_plus_calculation(_total, _options, _current), do: false

      def check_concat_calculation(total, option, current) do
        concat_number = current * trunc(:math.pow(10, trunc(:math.log10(option)) + 1)) + option

        if concat_number <= total do
          {true, concat_number}
        else
          {false, 0}
        end
      end

      def part_two(problem) do
        problem
        |> split_into_chunks()
        # |> do_calculations_part_2()
        |> Task.async_stream(fn options ->
          do_calculations_part_2(options)
        end)
        |> merge_results_stream()
        |> Enum.filter(fn {_total, valid} ->
          valid
        end)
        |> Enum.reduce(0, fn {total, _}, acc ->
          acc + total
        end)
      end

      def do_calculations_part_2(problem) do
        problem
        |> Enum.map(fn {total, [current | rest]} ->
          {total, do_calculation_part_2(total, rest, current)}
        end)
      end

      defp split_into_chunks(options) do
        workers = :erlang.system_info(:schedulers_online)
        options_count = Enum.count(options)
        options_per_chunk = :erlang.ceil(options_count / workers)

        Enum.chunk_every(options, options_per_chunk)
      end

      defp merge_results_stream(results_stream) do
        Enum.reduce(results_stream, [], fn {:ok, worker_result}, acc ->
          acc ++ worker_result
        end)
      end

      def do_calculation_part_2(total, [], current) do
        current == total
      end

      def do_calculation_part_2(total, [option | rest], current) do
        multi_valid = check_multi_calculation(total, option, current)
        add_valid = check_plus_calculation(total, option, current)
        {concat_valid, concat_value} = check_concat_calculation(total, option, current)

        cond do
          multi_valid and add_valid and concat_valid ->
            do_calculation_part_2(total, rest, current * option) ||
              do_calculation_part_2(total, rest, current + option) ||
              do_calculation_part_2(total, rest, concat_value)

          multi_valid and concat_valid ->
            do_calculation_part_2(total, rest, current * option) ||
              do_calculation_part_2(total, rest, concat_value)

          multi_valid and add_valid ->
            do_calculation_part_2(total, rest, current * option) ||
              do_calculation_part_2(total, rest, current + option)

          add_valid and concat_valid ->
            do_calculation_part_2(total, rest, current + option) ||
              do_calculation_part_2(total, rest, concat_value)

          multi_valid ->
            do_calculation_part_2(total, rest, current * option)

          add_valid ->
            do_calculation_part_2(total, rest, current + option)

          concat_valid ->
            do_calculation_part_2(total, rest, concat_value)

          true ->
            false
        end
      end
    end

This is the first time I’m contributing with my solution here. I mostly want to say how I misrepresented the second part. For some reason, I assumed that concatenation would have precedence over the other operators. That complicated things quite a bit and only after implementing the solution, I realized that it didn’t match the example.

Anyway, here’s the (much simpler) working code:

defmodule AOC2024.Day7 do
  def sum_valid(input, opts \\ [concat: false]) do
    input
    |> String.split("\n", trim: true)
    |> Enum.map(&parse_equation/1)
    |> Enum.filter(&valid?(&1, opts[:concat]))
    |> Enum.map(fn {result, _numbers} -> result end)
    |> Enum.sum()
  end
  
  def parse_equation(text) do
    [result, number_text] = String.split(text, ": ")
    numbers =
      number_text
      |> String.split()
      |> Enum.map(&String.to_integer/1)
    
    {String.to_integer(result), numbers}
  end

  def valid?({result, [h | t]}, concat) do
    valid?(t, result, h, concat)
  end

  defp valid?([], result, current, _), do: result == current
  defp valid?(_, result, current, _) when current > result, do: false
  defp valid?([h | t], result, current, concat) do
    valid?(t, result, current + h, concat) or
      valid?(t, result, current * h, concat) or
      concat and valid?(t, result, concatenate(current, h), concat)
  end

  defp concatenate(a, b) do
    exponent = Integer.digits(b) |> length()
    10 ** exponent * a + b
  end
end

Me too! I don’t know why, the text is clear enough :smiley:

Anyway without using streams and using the 10/100/1000 integer shifting I now have a 40ms solution which is good enough :slight_smile:

I’m amazed at the integer shifting for implementing the concatenation operator, and how much of a difference it makes.

Now mine runs in like 65ms!

(I don’t use streams but I always reach for Task.async_stream whenever I see a process that needs to be done for lots of different things)

2 Likes

Nothing fancy today, my solution looks like some others :

defmodule Y2024.D07 do
  use Day, input: "2024/07", part1: ~c"l", part2: ~c"l"

  defp part1(input), do: partX(input, false)
  defp part2(input), do: partX(input, true)

  defp partX(input, concat?) do
    input
    |> Enum.map(&parse_line/1)
    |> Enum.filter(&works?(&1, concat?))
    |> Enum.map(&elem(&1, 0))
    |> Enum.sum()
  end

  defp works?({r, [a, b | t]}, concat?) do
    works?({r, [a + b | t]}, concat?) or
      works?({r, [a * b | t]}, concat?) or
      (concat? and works?({r, [concat(a, b) | t]}, concat?))
  end

  defp works?({r, [r]}, _), do: true
  defp works?(_, _), do: false

  defp concat(a, b) do
    b
    |> Integer.digits()
    |> Enum.count()
    |> then(&(10 ** &1))
    |> Kernel.*(a)
    |> Kernel.+(b)
  end

  # defp parse_line...
end

To get the shift, b |> Integer.digits() |> Enum.count() give slightly faster results than (b |> :math.log10() |> floor()) + 1.

I updated my solution with Task.async_stream to reach 20ms on part 2 :zap: