Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Advent of Code - Day 11

elixir-livebook/day11.livemd

Advent of Code - Day 11

Mix.install([
  {:kino_aoc, "~> 0.1"},
  # {:benchee, "~> 1.0", only: :dev}
  {:kino_benchee, github: "akoutmos/kino_benchee", branch: "initial_release_prep"}
])

Introduction

Puzzle

{:ok, puzzle_input} =
  KinoAOC.download_puzzle("2023", "11", System.fetch_env!("LB_AOC_SESSION"))

Parser

Code - Parser

defmodule Parser do
  def parse(input) do
    rows =
      input
      |> String.split("\n", trim: true)

    {rows
     |> Enum.with_index(1)
     |> Enum.map(fn {line, y} ->
       line
       |> String.split("", trim: true)
       |> Enum.with_index(1)
       |> Enum.reduce([], fn {v, x}, acc ->
         if v == "#", do: [{x, y} | acc], else: acc
       end)
     end)
     |> List.flatten(), Enum.count(rows)}
  end
end

Tests - Parser

ExUnit.start(autorun: false)

defmodule ParserTest do
  use ExUnit.Case, async: true
  import Parser

  @input """
  ...#......
  .......#..
  #.........
  ..........
  ......#...
  .#........
  .........#
  ..........
  .......#..
  #...#.....
  """
  @expected []

  test "parse test" do
    actual = parse(@input)
    assert actual == @expected
  end
end

ExUnit.run()
defmodule GalaxyOperations do
  def expand_empty_space({map, n}, expansion_factor) do
    keys = MapSet.new(map)

    empty_columns =
      Enum.map(1..n, fn x ->
        unless Enum.any?(1..n, fn y ->
                 MapSet.member?(keys, {x, y})
               end),
               do: x
      end)
      |> Enum.reject(&is_nil/1)

    empty_rows =
      Enum.map(1..n, fn y ->
        unless Enum.any?(1..n, fn x ->
                 MapSet.member?(keys, {x, y})
               end),
               do: y
      end)
      |> Enum.reject(&is_nil/1)

    map
    |> Enum.map(fn {x, y} ->
      offset_x = Enum.count(empty_columns, fn col -> x > col end)
      offset_y = Enum.count(empty_rows, fn row -> y > row end)
      {x + offset_x * expansion_factor, y + offset_y * expansion_factor}
    end)
  end

  def find_pairs(map) do
    Enum.reduce(map, MapSet.new(), fn startg, acc ->
      Enum.reduce(map, acc, fn endg, acc ->
        cond do
          startg == endg -> acc
          MapSet.member?(acc, {endg, startg}) -> acc
          true -> MapSet.put(acc, {startg, endg})
        end
      end)
    end)
  end

  def sum_lengths([], acc), do: acc

  def sum_lengths([{{x1, y1}, {x2, y2}} | rest], acc) do
    sum_lengths(rest, acc + abs(x1 - x2) + abs(y1 - y2))
  end
end

Part One

Code - Part 1

defmodule PartOne do
  import GalaxyOperations

  def solve(input) do
    IO.puts("--- Part One ---")
    IO.puts("Result: #{run(input)}")
  end

  def run(input) do
    input
    |> Parser.parse()
    |> expand_empty_space(1)
    |> find_pairs()
    |> Enum.to_list()
    |> sum_lengths(0)
  end
end

Tests - Part 1

ExUnit.start(autorun: false)

defmodule PartOneTest do
  use ExUnit.Case, async: true
  import PartOne

  @input """
  ...#......
  .......#..
  #.........
  ..........
  ......#...
  .#........
  .........#
  ..........
  .......#..
  #...#.....
  """
  @expected 374

  test "part one" do
    actual = run(@input)
    assert actual == @expected
  end
end

ExUnit.run()

Solution - Part 1

PartOne.solve(puzzle_input)

Part Two

Code - Part 2

defmodule PartTwo do
  import GalaxyOperations

  def solve(input) do
    IO.puts("--- Part Two ---")
    IO.puts("Result: #{run(input, 1_000_000)}")
  end

  def run(input, expansion_factor) do
    input
    |> Parser.parse()
    |> expand_empty_space(expansion_factor - 1)
    |> find_pairs()
    |> Enum.to_list()
    |> sum_lengths(0)
  end
end

Tests - Part 2

ExUnit.start(autorun: false)

defmodule PartTwoTest do
  use ExUnit.Case, async: true
  import PartTwo

  @input """
  ...#......
  .......#..
  #.........
  ..........
  ......#...
  .#........
  .........#
  ..........
  .......#..
  #...#.....
  """

  test "part two 10 expand" do
    assert run(@input, 10) == 1030
  end

  test "part two 100 expand" do
    assert run(@input, 100) == 8410
  end
end

ExUnit.run()

Solution - Part 2

PartTwo.solve(puzzle_input)

Benchmarking

defmodule Benchmarks do
  def part1(input) do
    PartOne.run(input)
  end

  def part2(input) do
    PartTwo.run(input, 1_000_000)
  end
end

Benchee.run(
  %{
    "day11.part1" => &Benchmarks.part1/1,
    "day11.part2" => &Benchmarks.part2/1
  },
  inputs: %{
    "puzzle" => puzzle_input
  },
  time: 1,
  memory_time: 1,
  reduction_time: 1
)