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)