Day 4
Setup
Mix.install([
{:kino, "~> 0.4.1"},
{:nimble_parsec, "~> 1.0"}
])
input = Kino.Input.textarea("Problem input")
defmodule Parser do
import NimbleParsec
bingo_numbers =
repeat(
integer(min: 1)
|> optional(ignore(string(",")))
)
|> reduce({List, :wrap, []})
|> ignore(string("\n"))
board_row =
times(
repeat(ignore(string(" ")))
|> integer(min: 1)
|> repeat(ignore(string(" "))),
5
)
|> optional(ignore(string("\n")))
board =
times(
board_row,
5
)
|> reduce({List, :wrap, []})
boards =
repeat(
board
|> ignore(optional(string("\n")))
)
defparsec(
:parse,
bingo_numbers
|> ignore(string("\n"))
|> concat(boards)
)
end
parsed_input =
input
|> Kino.Input.read()
|> Parser.parse()
|> elem(1)
[bingo_numbers | boards] = parsed_input
Part 1
defmodule Solver do
def board_solutions(board_size \\ 5) do
# each row
# each column
Enum.reduce((board_size - 1)..0, [], fn row, solutions ->
[
Enum.reduce(
(row * board_size)..(board_size * (row + 1) - 1),
MapSet.new(),
&MapSet.put(&2, &1)
)
| solutions
]
end) ++
Enum.reduce((board_size - 1)..0, [], fn col, solutions ->
[
Enum.reduce(
col..(board_size * board_size - 1)//board_size,
MapSet.new(),
&MapSet.put(&2, &1)
)
| solutions
]
end)
end
def board_won?(board, numbers) do
marked_positions =
numbers
|> Stream.map(&Enum.find_index(board, fn x -> x == &1 end))
|> MapSet.new()
board_solutions()
|> Enum.any?(&MapSet.subset?(&1, marked_positions))
end
def solve(boards, all_numbers) do
initial_sums =
boards
|> Stream.map(&Enum.sum/1)
|> Enum.to_list()
Enum.reduce_while(all_numbers, [[] | initial_sums], fn
draw, [drawn_numbers | unmarked_sums] ->
boards_containing_draw =
boards
|> Stream.map(fn b -> draw in b end)
acc_numbers = [draw | drawn_numbers]
acc_sums =
Stream.zip(unmarked_sums, boards_containing_draw)
|> Stream.map(fn
{s, true} -> s - draw
{s, false} -> s
end)
result =
Stream.zip(boards, unmarked_sums)
|> Stream.filter(fn {b, _s} -> board_won?(b, acc_numbers) end)
|> Enum.map(fn {_b, s} -> draw * (s - draw) end)
if length(result) == 0 do
{:cont, [acc_numbers | acc_sums]}
else
{:halt, List.first(result)}
end
end)
end
end
Solver.solve(boards, bingo_numbers)
Part 2
defmodule Solver2 do
def board_solutions(board_size \\ 5) do
Enum.reduce((board_size - 1)..0, [], fn row, solutions ->
[
Enum.reduce(
(row * board_size)..(board_size * (row + 1) - 1),
MapSet.new(),
&MapSet.put(&2, &1)
)
| solutions
]
end) ++
Enum.reduce((board_size - 1)..0, [], fn col, solutions ->
[
Enum.reduce(
col..(board_size * board_size - 1)//board_size,
MapSet.new(),
&MapSet.put(&2, &1)
)
| solutions
]
end)
end
def board_won?(board, numbers) do
marked_positions =
numbers
|> Stream.map(&Enum.find_index(board, fn x -> x == &1 end))
|> MapSet.new()
board_solutions()
|> Enum.any?(&MapSet.subset?(&1, marked_positions))
end
def solve(boards, all_numbers) do
initial_sums =
boards
|> Stream.map(&Enum.sum/1)
|> Enum.to_list()
Enum.reduce_while(all_numbers, [[[], []] | initial_sums], fn
draw, [[drawn_numbers, solved_boards] | unmarked_sums] ->
boards_containing_draw =
boards
|> Stream.map(fn b -> draw in b end)
acc_numbers = [draw | drawn_numbers]
acc_sums =
Stream.zip(unmarked_sums, boards_containing_draw)
|> Stream.map(fn
{s, true} -> s - draw
{s, false} -> s
end)
acc_solved_boards =
Stream.zip(boards, Stream.iterate(0, &(&1 + 1)))
|> Stream.filter(fn {b, _i} -> board_won?(b, acc_numbers) end)
|> Stream.filter(fn {_b, i} -> i not in solved_boards end)
|> Enum.map(fn t -> elem(t, 1) end)
if length(solved_boards) + length(acc_solved_boards) == length(boards) do
last_board = List.first(acc_solved_boards)
last_board_unmarked_sum = Enum.at(acc_sums, last_board)
{:halt, draw * last_board_unmarked_sum}
else
{:cont, [[acc_numbers, acc_solved_boards ++ solved_boards] | acc_sums]}
end
end)
end
end
Solver2.solve(boards, bingo_numbers)