This commit is contained in:
Caleb Webber 2023-12-20 00:08:30 -05:00
parent d0b7ca24ef
commit 6bdcb771fb
3 changed files with 208 additions and 0 deletions

10
bench/day19_bench.exs Normal file
View file

@ -0,0 +1,10 @@
{_, puzzle_input} = File.read("./input/2023_19.txt")
{commands, _} = Mix.Tasks.Day19.parse(puzzle_input, &Mix.Tasks.Day19.build_range_comparator/1)
Benchee.run(%{
"day19_part2" => fn -> Mix.Tasks.Day19.solve_part_2(commands) end
})
#Name ips average deviation median 99th %
#day19_part2 1.47 K 681.65 μs ±25.77% 603.99 μs 1300.13 μs

180
lib/2023/day19.ex Normal file
View file

@ -0,0 +1,180 @@
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(commands, toys)
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)
|> RangeUtil.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

View file

@ -57,4 +57,22 @@ defmodule RangeUtil do
r2.first >= r1.first and r2.last >= r1.last -> Range.new(r2.first, r1.last)
end
end
@spec split_at(range :: Range.t(), value :: integer(), bound :: :lb | :ub) :: [Range.t()]
def split_at(range, value, bound) do
if value <= range.first or value >= range.last do
[range]
else
ls = min(range.first, value - 1)
le = min(range.last, value - 1)
rs = max(range.first, value + 1)
re = max(range.last, value + 1)
if bound == :lb do
[ls..value, rs..re]
else
[ls..le, value..re]
end
end
end
end