lud

lud

Advent of Code 2024 - Day 17

Gosh this one took me sooo much time.

At first I was trying to iterate each digit independently on the input A number to make digits change in the output. (iterating on a base-8 representation of the input). It looks likes it is kind of possible but I am not sure how.

So finally I just wen another way by trying more numbers. But it takes 3ms in the end so I guess it’s okay :smiley:

I wrote a big comment block if it can be useful to some !

https://github.com/lud/adventofcode/blob/main/lib/solutions/2024/day17.ex

Most Liked

sevenseacat

sevenseacat

Author of Ash Framework

Part 1 was pretty straightforward, part 2 is one of those reverse engineering nightmare puzzles D:

bjorng

bjorng

Erlang Core Team

I tried a few ways to solve part 2 before I found an approach that would terminate.

My solution seems to be similar to @lud’s, in that I construct possible values for the A register and discard values that don’t work. It’s a little bit different in that I don’t reverse the list of program digits.

https://github.com/bjorng/advent-of-code/blob/main/2024/day17/lib/day17.ex

antoine-duchenet

antoine-duchenet

Here are the main parts of my solution :

defmodule Y2024.D17 do
  use Day, input: "2024/17", part1: ~c"c", part2: ~c"c"

  @bits 3
  @mod 2 ** @bits

  # Because of the static A = A / 2^3 (truncated)
  @iteration_factor 2 ** 3

  defp part1(input) do
    {registers, program} = parse_input(input)

    %{
      rest: program,
      whole: program,
      registers: registers,
      output: []
    }
    |> run()
    |> Map.get(:output)
    |> Enum.join(",")
  end

  defp part2(input) do
    {registers, program} = parse_input(input)

    program
    |> Enum.count()
    |> backtrack(registers, program)
    |> Enum.min()
  end

  defp untrunc(n, factor), do: Range.new(n * factor, (n + 1) * factor - 1)

  defp backtrack(0, _, _), do: [0]

  defp backtrack(size, registers, program) do
    size
    |> Kernel.-(1)
    |> backtrack(registers, program)
    |> Enum.flat_map(&untrunc(&1, @iteration_factor))
    |> Enum.uniq()
    |> Enum.filter(fn a ->
      %{output: output} =
        run(%{
          rest: program,
          whole: program,
          registers: %{registers | a: a},
          output: []
        })

      output == Enum.take(program, -size)
    end)
  end

  defp run(%{rest: []} = state), do: state
  defp run(%{rest: [_]} = state), do: state

  defp run(%{rest: [op, operand | rest]} = state) do
    state
    |> Map.replace!(:rest, rest)
    |> opcode(op, operand)
    |> run()
  end

  defp opcode(state, 0, operand), do: adv(state, operand)
  defp opcode(state, 1, operand), do: bxl(state, operand)
  defp opcode(state, 2, operand), do: bst(state, operand)
  defp opcode(state, 3, operand), do: jnz(state, operand)
  defp opcode(state, 4, operand), do: bxc(state, operand)
  defp opcode(state, 5, operand), do: out(state, operand)
  defp opcode(state, 6, operand), do: bdv(state, operand)
  defp opcode(state, 7, operand), do: cdv(state, operand)

  defp adv(state, operand) do
    Map.update!(
      state,
      :registers,
      fn %{a: a} = registers -> %{registers | a: trunc(a / 2 ** combo(operand, registers))} end
    )
  end

  defp bxl(state, operand) do
    Map.update!(
      state,
      :registers,
      fn %{b: b} = registers -> %{registers | b: Bitwise.bxor(b, literal(operand))} end
    )
  end

  defp bst(state, operand) do
    Map.update!(
      state,
      :registers,
      &Map.replace!(&1, :b, rem(combo(operand, &1), @mod))
    )
  end

  defp jnz(%{registers: %{a: 0}} = state, _), do: state

  defp jnz(%{whole: whole} = state, operand) do
    Map.replace!(
      state,
      :rest,
      Enum.drop(whole, literal(operand))
    )
  end

  defp bxc(state, _) do
    Map.update!(
      state,
      :registers,
      fn %{b: b, c: c} = registers -> %{registers | b: Bitwise.bxor(b, c)} end
    )
  end

  defp out(%{registers: registers} = state, operand) do
    Map.update!(
      state,
      :output,
      fn output -> output ++ [rem(combo(operand, registers), @mod)] end
    )
  end

  defp bdv(state, operand) do
    Map.update!(
      state,
      :registers,
      fn %{a: a} = registers -> %{registers | b: trunc(a / 2 ** combo(operand, registers))} end
    )
  end

  defp cdv(state, operand) do
    Map.update!(
      state,
      :registers,
      fn %{a: a} = registers -> %{registers | c: trunc(a / 2 ** combo(operand, registers))} end
    )
  end

  defp literal(n), do: n

  defp combo(4, %{a: a}), do: a
  defp combo(5, %{b: b}), do: b
  defp combo(6, %{c: c}), do: c
  defp combo(n, _), do: literal(n)

  # Some parsing stuff...
end

For part 2, once you understand that the final 3, 0 means that the program restarts from the beginning while A != 0 and that A is independant of B and C, it opens many doors.

The most important part is the untrunc/2 function which takes advantage of the static division of A by 2^3 at every loop (for my input).

I had to keep track of every possible predecessor (Enum.filter, not Enum.find) because some outputs are related to multiple inputs and some outputs cannot exist with inputs in the predecessor..(predecessor + 7) range if only the lowest predecessor is kept. For example 0 and 1 inputs both output a 5 but the 4 output needs at least an 11 (which gives it very specific conditions of appearance that could be precluded by keeping only the lower predecessor).

At the end, the backtrack function takes 4ms to find 3 possible starting A (with the lowest being the solution).

Last Post!

ken-kost

ken-kost

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

  import Bitwise

  def parse(input, _part) do
    [registers, program] = String.split(Input.read!(input), "\n\n")
    [a, b, c] = registers |> String.split("\n") |> Enum.flat_map(&extract_numbers/1)
    {extract_numbers(program), %{a: a, b: b, c: c}}
  end

  def part_one({program, registers}) do
    program |> execute(registers, 0, []) |> Enum.join(",")
  end

  def part_two({expected, registers}) do
    solve(expected, registers, expected)
  end

  defp execute(program, registers, counter, output) do
    {opcode, literal} = {Enum.at(program, counter), Enum.at(program, counter + 1)}

    if is_nil(opcode) or is_nil(literal) do
      Enum.reverse(output)
    else
      operation = Map.get(opcodes(), opcode)
      combo = Map.get(combo_operands(), literal)
      result = operation.(combo.(registers), registers, literal, counter, output)
      {output, registers, counter} = result
      execute(program, registers, counter, output)
    end
  end

  defp solve(program, registers, expected) do
    # Looking at the program it can be seen that each output value
    # depends on the lower 10 bits of the value of register A.

    # Generate all possible A register values hat produce the first digit
    initial =
      Enum.map(0..1023, fn e ->
        {e, execute(program, Map.put(registers, :a, e), 0, [])}
      end)

    # Keep discarding the digits that don't match.
    # For each of the surviving elements extend the A register and
    # generate the next digit
    Enum.reduce(expected, {initial, 10}, fn digit, {as, shift} ->
      Enum.flat_map(as, fn {a, d} ->
        if hd(d) == digit do
          Enum.flat_map(0..7, fn value ->
            a_acc = value <<< shift ||| a
            registers = Map.put(registers, :a, a_acc >>> (shift - 7))
            output = execute(program, registers, 0, [])
            [{a_acc, output}]
          end)
        else
          []
        end
      end)
      |> then(&{&1, shift + 3})
    end)
    |> elem(0)
    |> Enum.min()
    |> elem(0)
  end

  defp combo_operands do
    %{
      0 => fn _ -> 0 end,
      1 => fn _ -> 1 end,
      2 => fn _ -> 2 end,
      3 => fn _ -> 3 end,
      4 => fn map -> Map.get(map, :a) end,
      5 => fn map -> Map.get(map, :b) end,
      6 => fn map -> Map.get(map, :c) end,
      7 => fn _map -> nil end
    }
  end

  defp opcodes do
    %{
      0 => fn
        combo, registers, _literal, counter, output ->
          registers = Map.update!(registers, :a, fn a -> trunc(a / :math.pow(2, combo)) end)
          {output, registers, counter + 2}
      end,
      1 => fn
        _combo, registers, literal, counter, output ->
          registers = Map.update!(registers, :b, fn b -> bxor(b, literal) end)
          {output, registers, counter + 2}
      end,
      2 => fn
        combo, registers, _literal, counter, output ->
          registers = Map.update!(registers, :b, fn _b -> rem(combo, 8) end)
          {output, registers, counter + 2}
      end,
      3 => fn
        _combo, registers, literal, counter, output ->
          case Map.get(registers, :a) do
            0 -> {output, registers, counter + 2}
            _a -> {output, registers, literal}
          end
      end,
      4 => fn
        _combo, registers, _literal, counter, output ->
          b = Map.get(registers, :b)
          c = Map.get(registers, :c)
          registers = Map.update!(registers, :b, fn _b -> bxor(b, c) end)
          {output, registers, counter + 2}
      end,
      5 => fn
        combo, registers, _literal, counter, output ->
          {[rem(combo, 8) | output], registers, counter + 2}
      end,
      6 => fn
        combo, registers, _literal, counter, output ->
          a = Map.get(registers, :a)
          registers = Map.update!(registers, :b, fn _b -> div(a, round(:math.pow(2, combo))) end)
          {output, registers, counter + 2}
      end,
      7 => fn
        combo, registers, _literal, counter, output ->
          a = Map.get(registers, :a)
          registers = Map.update!(registers, :c, fn _c -> div(a, round(:math.pow(2, combo))) end)
          {output, registers, counter + 2}
      end
    }
  end

  defp extract_numbers(string, regex \\ ~r/\d+(\.\d+)?/) do
    Enum.flat_map(Regex.scan(regex, string), &Enum.map(&1, fn e -> String.to_integer(e) end))
  end
end

I put functions into maps. :cowboy_hat_face: So I solved part 1 on my own because it was fun.
Used @bjorng shifting algorithm for part 2. I don’t think I would figure this one out on my own. :bowing_man: I did try to get the solution for part 2 with naive method (incrementing by 1) but that failed spectacularly. I even tried leaving the laptop running over night but forgot to turn off automatic sleep. :man_facepalming: Perhaps better even, my poor computer shouldn’t go through such treatment. :japanese_ogre:

Where Next?

Popular in Challenges Top

Aetherus
Finished Day 1 with Elixir :tada: Here’s my code: #!/usr/bin/env elixir defmodule Combination do @doc "Yields each combination of 2...
New
Aetherus
This topic is about Day 3 of the Advent of Code 2020 . Thanks to @egze, we have a private leaderboard: https://adventofcode.com/2020/le...
New
Aetherus
This topic is about Day 4 of the Advent of Code 2020 . Thanks to @egze, we have a private leaderboard: https://adventofcode.com/2020/le...
New
Aetherus
This topic is about Day 5 of the Advent of Code 2020 . Thanks to @egze, we have a private leaderboard: https://adventofcode.com/2020/le...
New
ehayun
I have 2 arrays: a1 can be any combination of value or nil like that a1 = [1,nil,3] and array 2 the same a2 = [4,2, nil] How do I com...
New
igorb
Today is a brute-force day: advent-of-code-2024/lib/advent_of_code2024/day6.ex at main · ibarakaiev/advent-of-code-2024 · GitHub Takes a...
New
lud
Hello everyone! This year is going to be shorter, but the difficulty will grow faster. Today I already feel that this is not standard “D...
New

Other popular topics Top

KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 36654 110
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
greenz1
I have a phoenix application from which a user can download multiple(5-6) files of size 1MB. I couldn’t find anything related to sending ...
New
sorentwo
Hello! tl;dr Announcing Oban, an Ecto based job processing library with a focus on reliability and historical observability. After spen...
985 44532 311
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New

We're in Beta

About us Mission Statement