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

Advent of Code - Day 13

2023_day13.livemd

Advent of Code - Day 13

Mix.install([
  {:kino_aoc, "~> 0.1"}
])

Introduction

–> Content

Puzzle

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

Parser

Code - Parser

defmodule Parser do
  def parse(input) do
    String.split(input, "\n\n", trim: true)
    |> Enum.map(fn pattern ->
      String.split(pattern, "\n", trim: true)
      |> Enum.map(fn row -> String.split(row, "", trim: true) end)
    end)

    # |> Enum.chunk_every(2)
    # |> Enum.map(fn [first_pattern, second_pattern] ->
    #   # IO.inspect(first_pattern, label: :first_pattern)
    #   # IO.inspect(second_pattern, label: :second_pattern)
    #   [first_pattern, second_pattern]
    #   |> Enum.zip([:first, :second])
    #   |> Map.new(fn {pattern, name} -> {name, pattern} end)
    # end)
  end
end

Tests - Parser

ExUnit.start(autorun: false)

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

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

  #...##..#
  #....#..#
  ..##..###
  #####.##.
  #####.##.
  ..##..###
  #....#..#
  """

  @expected [
    [
      ["#", ".", "#", "#", ".", ".", "#", "#", "."],
      [".", ".", "#", ".", "#", "#", ".", "#", "."],
      ["#", "#", ".", ".", ".", ".", ".", ".", "#"],
      ["#", "#", ".", ".", ".", ".", ".", ".", "#"],
      [".", ".", "#", ".", "#", "#", ".", "#", "."],
      [".", ".", "#", "#", ".", ".", "#", "#", "."],
      ["#", ".", "#", ".", "#", "#", ".", "#", "."]
    ],
    [
      ["#", ".", ".", ".", "#", "#", ".", ".", "#"],
      ["#", ".", ".", ".", ".", "#", ".", ".", "#"],
      [".", ".", "#", "#", ".", ".", "#", "#", "#"],
      ["#", "#", "#", "#", "#", ".", "#", "#", "."],
      ["#", "#", "#", "#", "#", ".", "#", "#", "."],
      [".", ".", "#", "#", ".", ".", "#", "#", "#"],
      ["#", ".", ".", ".", ".", "#", ".", ".", "#"]
    ]
  ]

  describe "parse/1" do
    test "simple example" do
      assert parse(@input) == @expected
    end

    test "complex example" do
      raw_input = """
      #.##
      ..#.
      ##..

      ##..
      ..#.
      ..##

      #.#.
      #...
      #...

      ..##
      ####
      ####
      """

      expected = [
        [["#", ".", "#", "#"], [".", ".", "#", "."], ["#", "#", ".", "."]],
        [["#", "#", ".", "."], [".", ".", "#", "."], [".", ".", "#", "#"]],
        [["#", ".", "#", "."], ["#", ".", ".", "."], ["#", ".", ".", "."]],
        [[".", ".", "#", "#"], ["#", "#", "#", "#"], ["#", "#", "#", "#"]]
      ]

      assert parse(raw_input) == expected
    end

    test "example with only 1 input" do
      raw_input = """
      #.#.##.#.
      ..##..###
      #####.##.
      #####.##.
      ..##..###
      #....#..#
      """

      expected = [
        [
          ["#", ".", "#", ".", "#", "#", ".", "#", "."],
          [".", ".", "#", "#", ".", ".", "#", "#", "#"],
          ["#", "#", "#", "#", "#", ".", "#", "#", "."],
          ["#", "#", "#", "#", "#", ".", "#", "#", "."],
          [".", ".", "#", "#", ".", ".", "#", "#", "#"],
          ["#", ".", ".", ".", ".", "#", ".", ".", "#"]
        ]
      ]

      assert parse(raw_input) == expected
    end
  end
end

ExUnit.run()

Part One

Code - Part 1

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

  def run(input_string) when is_bitstring(input_string), do: run(Parser.parse(input_string))

  def run(inputs) do
    inputs
    |> Enum.reduce(0, fn input, sum ->
      sum + calculate_reflection_value(input)
    end)
  end

  def calculate_reflection_value(input) do
    (columns_left_of_vertical_reflection(input) || 0) +
      100 * (rows_above_horizontal_reflection(input) || 0)
  end

  def columns_left_of_vertical_reflection(pattern, excluding \\ nil) do
    Enum.find(1..(number_of_columns(pattern) - 1), fn offset ->
      distance_to_end = number_of_columns(pattern) - 1 - offset
      distance_to_start = offset - 1
      search_distance = min(distance_to_end, distance_to_start)

      Enum.all?(0..search_distance, fn inner_offset ->
        column(pattern, offset - inner_offset - 1) == column(pattern, offset + inner_offset)
      end) && offset != excluding
    end)
  end

  def rows_above_horizontal_reflection(pattern, excluding \\ nil) do
    Enum.find(1..(length(pattern) - 1), fn offset ->
      distance_to_end = length(pattern) - 1 - offset
      distance_to_start = offset - 1
      search_distance = min(distance_to_end, distance_to_start)

      Enum.all?(0..search_distance, fn inner_offset ->
        Enum.at(pattern, offset - inner_offset - 1) == Enum.at(pattern, offset + inner_offset)
      end) && offset * 100 != excluding
    end)
  end

  def number_of_columns(pattern) do
    pattern |> hd() |> length()
  end

  def column(pattern, idx) do
    Enum.map(0..(length(pattern) - 1), fn row_idx ->
      (Enum.at(pattern, row_idx) || [])
      |> Enum.at(idx)
    end)
  end
end

Tests - Part 1

ExUnit.start(autorun: false)

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

  @raw_input """
  #.##..##.
  ..#.##.#.
  ##......#
  ##......#
  ..#.##.#.
  ..##..##.
  #.#.##.#.

  #...##..#
  #....#..#
  ..##..###
  #####.##.
  #####.##.
  ..##..###
  #....#..#
  """

  @input Parser.parse(@raw_input)

  describe "run/1" do
    test "main example" do
      assert run(@raw_input) == 405
    end
  end

  describe "columns_left_of_vertical_reflection/1" do
    test "main example" do
      assert columns_left_of_vertical_reflection(Enum.at(@input, 0)) == 5
      assert columns_left_of_vertical_reflection(Enum.at(@input, 1)) == nil
    end

    test "now example with reflection more towards the left" do
      raw_input = """
      #.##.###.
      ##..##.#.
      #.##.#..#

      #########
      """

      assert columns_left_of_vertical_reflection(Parser.parse(raw_input) |> hd()) == 3
    end
  end

  describe "rows_above_horizontal_reflection/1" do
    test "main example" do
      assert rows_above_horizontal_reflection(Enum.at(@input, 0)) == nil
      assert rows_above_horizontal_reflection(Enum.at(@input, 1)) == 4
    end

    test "now example with reflection more towards the top" do
      raw_input = """
      #.#.##.#.
      ..##..###
      #####.##.
      #####.##.
      ..##..###
      #....#..#
      """

      assert rows_above_horizontal_reflection(Parser.parse(raw_input) |> hd()) == nil
    end
  end

  describe "number_of_columns/2" do
    test "main example" do
      assert number_of_columns(Enum.at(@input, 0)) == 9
    end
  end

  describe "column/2" do
    test "main example" do
      assert column(Enum.at(@input, 0), 0) == ["#", ".", "#", "#", ".", ".", "#"]
    end
  end
end

ExUnit.run()

Solution - Part 1

PartOne.solve(puzzle_input)

Part Two

Code - Part 2

defmodule PartTwo do
  require Integer, [:is_odd, :is_even]

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

  def run(input_string) when is_bitstring(input_string), do: run(Parser.parse(input_string))

  def run(inputs) do
    Enum.reduce(inputs, 0, fn input, sum ->
      regular_reflection_value = calculate_reflection_value(input)

      Enum.find_value(0..(length(input) - 1), fn row_idx ->
        Enum.find_value(0..(PartOne.number_of_columns(input) - 1), fn col_idx ->
          current_res =
            calculate_reflection_value(
              invert_at(input, row_idx, col_idx),
              regular_reflection_value
            )

          if current_res && current_res != regular_reflection_value do
            current_res
          else
            nil
          end
        end)
      end) + sum
    end)
  end

  def invert_at(input, row_idx, col_idx) do
    List.update_at(input, row_idx, fn row ->
      List.update_at(row, col_idx, fn cell -> invert(cell) end)
    end)
  end

  defp invert("."), do: "#"
  defp invert("#"), do: "."

  def calculate_reflection_value(input, excluding \\ nil) do
    cond do
      res = PartOne.rows_above_horizontal_reflection(input, excluding) ->
        res * 100

      res = PartOne.columns_left_of_vertical_reflection(input, excluding) ->
        res

      true ->
        nil
    end
  end
end

Tests - Part 2

ExUnit.start(autorun: false)

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

  @raw_input """
  #.##..##.
  ..#.##.#.
  ##......#
  ##......#
  ..#.##.#.
  ..##..##.
  #.#.##.#.

  #...##..#
  #....#..#
  ..##..###
  #####.##.
  #####.##.
  ..##..###
  #....#..#
  """

  describe "run/1" do
    test "main example" do
      assert run(@raw_input) == 400
    end

    test "example from input" do
      raw_input = """
      ###.#####.#
      ###.#####.#
      #..##.####.
      ####.#...#.
      #.....#..##
      .##.#..##.#
      ##.##.#...#
      ##.##.#...#
      .#..#..##.#
      #.....#..##
      ####.#...#.
      """

      # would be 100
      assert run(raw_input) == 700
    end
  end

  describe "invert_at" do
    test "example from input" do
      raw_input = """
      ###.#####.#
      ###.#####.#
      #..##.####.
      ####.#...#.
      #.....#..##
      .##.#..##.#
      ##.##.#...#
      ##.##.#...#
      .#..#..##.#
      #.....#..##
      ####.#...#.
      """

      input = raw_input |> Parser.parse() |> hd()

      assert invert_at(input, 0, 0) |> Enum.at(0) |> Enum.join("") == ".##.#####.#"
      assert invert_at(input, 0, 1) |> Enum.at(0) |> Enum.join("") == "#.#.#####.#"
      assert invert_at(input, 0, 3) |> Enum.at(0) |> Enum.join("") == "#########.#"

      assert invert_at(input, 1, 0) |> Enum.at(1) |> Enum.join("") == ".##.#####.#"
      assert invert_at(input, 1, 1) |> Enum.at(1) |> Enum.join("") == "#.#.#####.#"
      assert invert_at(input, 1, 3) |> Enum.at(1) |> Enum.join("") == "#########.#"

      assert invert_at(input, 10, 10) |> Enum.at(10) |> Enum.join("") == "####.#...##"
    end
  end

  describe "calculate_reflection_value/1" do
    test "main example 1" do
      raw_input = """
      ..##..##.
      ..#.##.#.
      ##......#
      ##......#
      ..#.##.#.
      ..##..##.
      #.#.##.#.
      """

      assert calculate_reflection_value(raw_input |> Parser.parse() |> hd()) == 300
    end

    test "main example 2" do
      raw_input = """
      #...##..#
      #...##..#
      ..##..###
      #####.##.
      #####.##.
      ..##..###
      #....#..#
      """

      assert calculate_reflection_value(raw_input |> Parser.parse() |> hd()) == 100
    end
  end
end

ExUnit.run()

Solution - Part 2

PartTwo.solve(puzzle_input)