advent_of_code/elixir/lib/Y2023/day19.ex
2024-11-15 21:03:49 -05:00

180 lines
4.4 KiB
Elixir

defmodule Mix.Tasks.Day19 do
def run(args) do
[part | _] = args
{_, puzzle_input} = File.read("./input/2023_19.txt")
part = if part == "1", do: :part1, else: :part2
{commands, toys} = parse(puzzle_input, case part do
:part1 -> &build_comparator/1
:part2 -> &build_range_comparator/1
end)
cond do
part == :part1 -> solve_part_1(toys, commands)
part == :part2 -> solve_part_2(commands)
end |> IO.puts()
end
def build_comparator(map) do
%{"comp" => comp, "facet" => facet, "target" => target, "thresh" => thresh} = map
if comp == "" do
fn _ -> target end
else
comp =
if comp == ">" do
fn a, b -> a > b end
else
comp == "<"
fn a, b -> a < b end
end
thresh = String.to_integer(thresh)
fn toy ->
if toy |> Map.get(facet) |> comp.(thresh) do
target
else
nil
end
end
end
end
def build_range_comparator(map) do
%{"comp" => comp, "facet" => facet, "target" => target, "thresh" => thresh} = map
if comp == "" do
fn toy -> [{toy, target}] end
else
thresh = String.to_integer(thresh)
fn toy ->
toy
|> Map.get(facet)
|> AoC.Util.Range.split_at(thresh, if(comp == ">", do: :lb, else: :ub))
|> Enum.map(fn new_range ->
if comp == ">" and new_range.first > thresh do
{new_range, target}
else
if comp == "<" and new_range.last < thresh do
{new_range, target}
else
{new_range, nil}
end
end
end)
|> Enum.map(fn {new_range, target} ->
{toy |> Map.put(facet, new_range), target}
end)
end
end
end
def parse(puzzle_input, comparator_builder) do
[commands, toys] = puzzle_input |> String.split("\n\n")
commands =
commands
|> String.split("\n")
|> Enum.map(fn l ->
[label, inner | _] = String.split(l, ["{", "}"])
inner = String.split(inner, ",")
steps =
inner
|> Enum.map(fn i ->
r = ~r/(((?P<facet>[xmas])(?P<comp>[<>])(?P<thresh>\d+):)?(?P<target>\w+),?)/
Regex.named_captures(r, i) |> comparator_builder.()
end)
{label, steps}
end)
|> Map.new()
toys =
toys
|> String.split("\n")
|> Stream.reject(&(&1 == ""))
|> Enum.map(fn t ->
[_, u, _] = String.split(t, ["{", "}"])
u
|> String.split(",")
|> Stream.map(fn f -> f |> String.split("=") end)
|> Enum.map(fn [f, t] ->
{f, String.to_integer(t)}
end)
|> Map.new()
end)
{commands, toys}
end
def map_toy(toy, label, map) do
if label === "A" or label === "R" do
label
else
map
|> Map.get(label)
|> Enum.reduce_while(
nil,
fn f, _ ->
dest = f.(toy)
{if(is_nil(dest), do: :cont, else: :halt), dest}
end
)
|> then(fn dest -> map_toy(toy, dest, map) end)
end
end
def map_toy_range(toy_range, label, commands) do
if label === "A" or label === "R" do
[{toy_range, label}]
else
commands
|> Map.get(label)
|> Enum.reduce_while(
[{toy_range, nil}],
fn f, acc ->
new =
acc
|> Enum.flat_map(fn {range, target} ->
if is_nil(target) do
f.(range)
else
[{range, target}]
end
end)
{
if(Enum.all?(new, fn {_, dest} -> !is_nil(dest) end), do: :halt, else: :cont),
new
}
end
)
|> Enum.flat_map(fn {r, t} -> map_toy_range(r, t, commands) end)
end
end
def solve_part_1(toys, commands) do
toys
|> Enum.filter(fn t -> map_toy(t, "in", commands) === "A" end)
|> Enum.map(fn t ->
t |> Map.values() |> Enum.sum()
end)
|> Enum.sum()
end
def solve_part_2(commands) do
toy_range = %{"x" => 1..4000, "m" => 1..4000, "a" => 1..4000, "s" => 1..4000}
map_toy_range(toy_range, "in", commands)
|> Stream.filter(fn {_, r} -> r === "A" end)
|> Stream.map(&elem(&1, 0))
|> Stream.map(fn r ->
Map.values(r) |> Enum.reduce(1, fn rr, acc -> acc * Range.size(rr) end)
end)
|> Enum.sum()
end
end