Powered by AppSignal & Oban Pro

d22

d22/d22.livemd

d22

# Mix.install([
#   {:memoize, "~> 1.4"}
# ])

Section

# Memoize.invalidate()
div(1, 2)
ExUnit.start()

defmodule D22 do
  def parse(input) do
    String.split(input, "\n", trim: true)
    |> Enum.map(&String.to_integer/1)
  end

  defp mix(value, transform) do
    rem(Bitwise.bxor(value, transform.(value)), 16_777_216)
  end

  def evolve(secret) do
    secret
    |> mix(&(&1 * 64))
    |> mix(&div(&1, 32))
    |> mix(&(&1 * 2048))
  end

  def generate(secret) do
    Stream.iterate(secret, &D22.evolve/1)
    |> Enum.take(2001)
  end

  def part1(secrets) do
    Enum.map(secrets, fn secret -> List.last(generate(secret)) end)
    |> Enum.sum()
  end

  def collate(secret) do
    prices = generate(secret) |> Enum.map(&rem(&1, 10))

    Enum.zip(prices, tl(prices))
    |> Enum.map(fn {prev, curr} -> {curr - prev, curr} end)
  end

  def sell(trigger, book), do: sell(trigger, book, trigger)
  defp sell(_, [], _), do: 0

  defp sell(trigger, [{actual, price} | book], [expected | rest]) do
    rest = if expected == actual, do: rest, else: trigger

    if Enum.empty?(rest) do
      price
    else
      sell(trigger, book, rest)
    end
  end

  # Keying by an integer instead of a list cuts runtime by about half.
  defp window_key([a, b, c, d]), do: (((a + 9) * 20 + (b + 9)) * 20 + (c + 9)) * 20 + (d + 9)

  def window(book), do: window(book, [], %{})

  defp window([{a, price} | book], [b, c, d | _], acc) do
    changes = [a, b, c, d]
    window(book, changes, Map.put_new(acc, window_key(changes), price))
  end

  defp window([], _, acc), do: acc
  defp window([{head, _} | book], changes, acc), do: window(book, [head | changes], acc)

  def part2(secrets) do
    Enum.map(secrets, fn secret -> window(collate(secret)) end)
    |> Enum.reduce(fn windowed, acc ->
      Enum.reduce(windowed, acc, fn {key, price}, acc ->
        Map.update(acc, key, price, &(&1 + price))
      end)
    end)
    |> Map.values()
    |> Enum.max()
  end

  use ExUnit.Case

  test "123" do
    assert [
             15_887_950,
             16_495_136,
             527_345,
             704_524,
             1_553_684,
             12_683_156,
             11_100_544,
             12_249_484,
             7_753_432,
             5_908_254
           ] == Stream.iterate(123, &D22.evolve/1) |> Stream.drop(1) |> Enum.take(10)
  end

  test "part1" do
    input = "1
10
100
2024
"
    assert 37_327_623 == D22.parse(input) |> D22.part1()
  end

  test "part2" do
    assert 6 == sell([-1, -1, 0, 2], collate(123))

    assert [7, 7, 0, 9] == Enum.map([1, 2, 3, 2024], &sell([-2, 1, -1, 3], collate(&1)))

    input = "1
2
3
2024
"
    assert 23 == D22.parse(input) |> D22.part2()
  end
end

ExUnit.run()
input = File.read!(__DIR__ <> "/input")
D22.parse(input) |> D22.part1()
D22.parse(input) |> D22.part2()