Powered by AppSignal & Oban Pro

Day 3: Binary Diagnostic

2021/day03.livemd

Day 3: Binary Diagnostic

Setup

defmodule Setup do
  def get_input(prompt) do
    case IO.gets(prompt) do
      :eof -> ""
      line -> line <> get_input(prompt)
    end
  end

  def parse_input(report) do
    report
    |> String.split("\n", trim: true)
    |> Enum.map(&amp;String.codepoints/1)
  end

  def parse_sorted(report) do
    report
    |> String.split("\n", trim: true)
    |> Enum.sort()
    |> Enum.map(&amp;String.codepoints/1)
  end
end

report = Setup.get_input("input")

input = report |> Setup.parse_input()
sorted_input = report |> Setup.parse_sorted()

:ok
defmodule Diagnostics do
  def transpose(matrix) do
    matrix
    |> Enum.zip()
    |> Enum.map(&amp;Tuple.to_list/1)
  end

  def most_common_bits(readings) do
    readings
    |> transpose()
    |> Enum.map(&amp;Enum.frequencies/1)
    |> Enum.map(fn
      %{"0" => zeros, "1" => ones} when ones >= zeros -> "1"
      _ -> "0"
    end)
  end

  def flip(bits) do
    Enum.map(bits, fn
      "1" -> "0"
      "0" -> "1"
    end)
  end

  def gamma(most_common_bits) do
    most_common_bits
    |> Enum.join()
    |> String.to_integer(2)
  end

  def epsilon(most_common_bits) do
    most_common_bits
    |> flip()
    |> Enum.join()
    |> String.to_integer(2)
  end

  def power_consumption(readings) do
    mcb = most_common_bits(readings)

    gamma(mcb) * epsilon(mcb)
  end

  def oxygen_rating(readings) do
    rating(readings, &amp;Kernel.==/2)
  end

  def co2_rating(readings) do
    rating(readings, &amp;Kernel.!=/2)
  end

  defp rating(readings, fun) do
    size = readings |> List.first() |> Enum.count()

    Enum.reduce_while(0..(size - 1), readings, fn
      _, [reading] ->
        {:halt, [reading]}

      index, readings ->
        most_common_bit = most_common_bits(readings) |> Enum.at(index)

        {:cont, Enum.filter(readings, &amp;fun.(Enum.at(&amp;1, index), most_common_bit))}
    end)
    |> List.first()
    |> Enum.join()
    |> String.to_integer(2)
  end

  def life_support_rating(readings) do
    oxygen_rating(readings) * co2_rating(readings)
  end
end

Part1

Diagnostics.power_consumption(input)

Part2

Diagnostics.life_support_rating(input)

SortedSolve

defmodule SortedSolver do
  def solve(input) do
    oxygen(input) * co2(input)
  end

  def oxygen(input) do
    rating(input, &amp;most_commons/1)
  end

  def co2(input) do
    rating(input, &amp;least_commons/1)
  end

  defp rating(input, fun) do
    width = input |> hd() |> length()

    [rating] =
      Enum.reduce_while(0..(width - 1), input, fn
        _, [readings] ->
          {:halt, [readings]}

        i, readings ->
          {:cont, readings |> take_while_at(i) |> fun.()}
      end)

    rating
    |> Enum.join()
    |> String.to_integer(2)
  end

  defp most_commons({zeros, ones}) when length(ones) >= length(zeros), do: ones
  defp most_commons({zeros, _}), do: zeros

  defp least_commons({zeros, ones}) when length(ones) >= length(zeros), do: zeros
  defp least_commons({_, ones}), do: ones

  def take_while_at(input, index) do
    Enum.split_while(input, &amp;(Enum.at(&amp;1, index) == "0"))
  end
end

SortedSolver.solve(sorted_input)
nums =
  sorted_input
  |> Diagnostics.transpose()
  |> Enum.map(fn r -> Enum.join(r) |> String.to_integer(2) end)

num = hd(nums)

Enum.map(0..1000, fn n ->
  round(:math.pow(2, n))
end)
defmodule IndexSolver do
  def solve(input) do
    oxygen(input) * co2(input)
  end

  def oxygen(input) do
    rating(input, &amp;update_mc_boundaries/2)
  end

  def co2(input) do
    rating(input, &amp;update_lc_boundaries/2)
  end

  def rating(input, fun) do
    {i, _} =
      input
      |> Diagnostics.transpose()
      |> Enum.reduce_while({0, length(input) - 1}, fn
        col, {upper, lower} when lower - upper > 1 ->
          {:cont,
           col
           |> Enum.slice(upper..lower)
           |> Enum.find_index(&amp;(&amp;1 == "1"))
           |> fun.({upper, lower})}

        _col, boundaries ->
          {:halt, boundaries}
      end)

    input
    |> Enum.at(i)
    |> Enum.join()
    |> String.to_integer(2)
  end

  defp update_mc_boundaries(index, {upper, lower}) when index > div(lower - upper, 2),
    do: {upper, upper + index}

  defp update_mc_boundaries(index, {upper, lower}), do: {upper + index, lower}

  defp update_lc_boundaries(index, {upper, lower}) when index > div(lower - upper, 2),
    do: {upper + index, lower}

  defp update_lc_boundaries(index, {upper, _lower}), do: {upper, upper + index}
end
Enum.at(sorted_input, 368)

Tests

ExUnit.start()

defmodule Test do
  use ExUnit.Case

  @test_input """
  00100
  11110
  10110
  10111
  10101
  01111
  00111
  11100
  10000
  11001
  00010
  01010
  """

  setup do
    {:ok, input: Setup.parse_input(@test_input), sorted_input: Setup.parse_sorted(@test_input)}
  end

  test "parse input", %{input: input} do
    assert input == [
             ["0", "0", "1", "0", "0"],
             ["1", "1", "1", "1", "0"],
             ["1", "0", "1", "1", "0"],
             ["1", "0", "1", "1", "1"],
             ["1", "0", "1", "0", "1"],
             ["0", "1", "1", "1", "1"],
             ["0", "0", "1", "1", "1"],
             ["1", "1", "1", "0", "0"],
             ["1", "0", "0", "0", "0"],
             ["1", "1", "0", "0", "1"],
             ["0", "0", "0", "1", "0"],
             ["0", "1", "0", "1", "0"]
           ]
  end

  test "diagnostics", %{input: input} do
    mcb = Diagnostics.most_common_bits(input)

    assert Diagnostics.gamma(mcb) == 22
    assert Diagnostics.epsilon(mcb) == 9

    assert Diagnostics.power_consumption(input) == 198
  end

  test "life support rating", %{input: input} do
    assert Diagnostics.oxygen_rating(input) == 23
    assert Diagnostics.co2_rating(input) == 10

    assert Diagnostics.life_support_rating(input) == 230
  end

  test "SortedSolver", %{sorted_input: input} do
    assert SortedSolver.solve(input) == 230
  end

  test "IndexSolver", %{sorted_input: input} do
    assert IndexSolver.oxygen(input) == 23
    assert IndexSolver.co2(input) == 10
    assert IndexSolver.solve(input) == 230
  end
end

ExUnit.run()
Mix.install([:benchee])
Benchee.run(%{
  "Diagnostics" => fn -> Diagnostics.life_support_rating(input) end,
  "SortedSolve" => fn -> SortedSolver.solve(sorted_input) end,
  "IndexSolver" => fn -> IndexSolver.solve(sorted_input) end
})

:ok