Watch
1
0
Fork
You've already forked advent-of-code
0
advent-of-code/2020/day19/day19.ex
2021-12-04 21:20:57 -08:00

245 lines
5.9 KiB
Elixir

defmodule RBNF do
def build_grammar(rules) do
rules
|> Enum.into(%{}, fn [name, rbnf] ->
{
name,
make_rule(rbnf)
}
end)
end
def make_rule(str) do
{:alt,
String.split(str, " | ")
|> Enum.map(fn text ->
seq =
String.split(text, " ")
|> Enum.map(fn chars ->
cond do
String.match?(chars, ~r/^\d+$/) -> {:ref, chars}
String.match?(chars, ~r/^".*"$/) -> {:lit, String.slice(chars, 1..-2)}
end
end)
{:seq, seq}
end)}
end
# Returns {matched_text, remaining_text}
def parse(grammar, rule, text) do
case rule do
{:lit, char} ->
if String.at(text, 0) == char do
String.split_at(text, 1)
else
{nil, text}
end
{:ref, name} ->
parse(grammar, Map.fetch!(grammar, name), text)
{:seq, rules} ->
{matches, remaining} =
Enum.map_reduce(rules, text, fn rule, buffer ->
if buffer == nil do
# An earlier match failed -> bail out
{nil, nil}
else
case parse(grammar, rule, buffer) do
{nil, _} -> {nil, nil}
{matched, rest} -> {matched, rest}
end
end
end)
if Enum.all?(matches) do
{Enum.join(matches), remaining}
else
{nil, text}
end
{:alt, rules} ->
res =
Enum.reduce_while(rules, text, fn rule, _ ->
case parse(grammar, rule, text) do
{nil, _} -> {:cont, nil}
{matched, rest} -> {:halt, {matched, rest}}
end
end)
case res do
# Nothing matched
nil -> {nil, text}
{matched, rest} -> {matched, rest}
end
{:plus, repeated} ->
case parse(grammar, repeated, text) do
{nil, _} -> {nil, text}
{matched, rest} -> star_iter(grammar, repeated, matched, rest)
end
{:paired, r1, r2} ->
{count, prefix, rest} = count_iter(grammar, r1, "", 0, text)
if count == 0 do
{nil, text}
else
new_rule = {:seq, List.duplicate(r2, count)}
case parse(grammar, new_rule, rest) do
{nil, _} -> {nil, text}
{suffix, remaining} -> {prefix <> suffix, remaining}
end
end
{:left_sided, left, right} ->
{count, prefix, rest} = count_iter(grammar, left, "", 0, text)
if count < 2 do
# No extra lefts to match with
{nil, text}
else
new_rule = {:at_most, right, count - 1}
case parse(grammar, new_rule, rest) do
{nil, _} -> {nil, text}
{suffix, remaining} -> {prefix <> suffix, remaining}
end
end
{:at_most, r, count} ->
new_rule = {:alt, Enum.map(count..1, &{:seq, List.duplicate(r, &1)})}
parse(grammar, new_rule, text)
end
end
defp star_iter(grammar, rule, prefix, text) do
case parse(grammar, rule, text) do
{nil, _} -> {prefix, text}
{matched, rest} -> star_iter(grammar, rule, prefix <> matched, rest)
end
end
defp count_iter(grammar, rule, prefix, count, text) do
case parse(grammar, rule, text) do
{nil, _} -> {count, prefix, text}
{matched, rest} -> count_iter(grammar, rule, prefix <> matched, count + 1, rest)
end
end
end
defmodule Day19 do
defp read_input do
Path.expand('input', Path.dirname(__ENV__.file))
|> File.read!()
end
defp test_input do
"""
42: 9 14 | 10 1
9: 14 27 | 1 26
10: 23 14 | 28 1
1: "a"
11: 42 31
5: 1 14 | 15 1
19: 14 1 | 14 14
12: 24 14 | 19 1
16: 15 1 | 14 14
31: 14 17 | 1 13
6: 14 14 | 1 14
2: 1 24 | 14 4
0: 8 11
13: 14 3 | 1 12
15: 1 | 14
17: 14 2 | 1 7
23: 25 1 | 22 14
28: 16 1
4: 1 1
20: 14 14 | 1 15
3: 5 14 | 16 1
27: 1 6 | 14 18
14: "b"
21: 14 1 | 1 14
25: 1 1 | 1 14
22: 14 14
8: 42
26: 14 22 | 1 20
18: 15 15
7: 14 5 | 1 21
24: 14 1
abbbbbabbbaaaababbaabbbbabababbbabbbbbbabaaaa
bbabbbbaabaabba
babbbbaabbbbbabbbbbbaabaaabaaa
aaabbbbbbaaaabaababaabababbabaaabbababababaaa
bbbbbbbaaaabbbbaaabbabaaa
bbbababbbbaaaaaaaabbababaaababaabab
ababaaaaaabaaab
ababaaaaabbbaba
baabbaaaabbaaaababbaababb
abbbbabbbbaaaababbbbbbaaaababb
aaaaabbaabaaaaababaa
aaaabbaaaabbaaa
aaaabbaabbaaaaaaabbbabbbaaabbaabaaa
babaaabbbaaabaababbaabababaaab
aabbbbbaabbbaaaaaabbbbbababaaaaabbaaabba
"""
end
defp parse_input(text) do
[rules, messages] = String.split(text, "\n\n", trim: true)
{parse_rules(rules), parse_messages(messages)}
end
defp parse_rules(text) do
String.split(text, "\n", trim: true)
|> Enum.map(&String.split(&1, ": "))
end
defp parse_messages(text) do
String.split(text, "\n", trim: true)
end
def part1 do
{rules, messages} = read_input() |> parse_input()
grammar = RBNF.build_grammar(rules)
Enum.count(messages, fn msg ->
case RBNF.parse(grammar, {:ref, "0"}, msg) do
{_, ""} -> true
_ -> false
end
end)
end
def part2 do
{rules, messages} = read_input() |> parse_input()
grammar =
RBNF.build_grammar(rules)
# 8: 42 | 42 8 = 42+
|> Map.put("8", {:plus, {:ref, "42"}})
# 11: 42 31 | 42 11 31 = same number of 42 and 31 (at least one pair)
|> Map.put(
"11",
{:paired, {:ref, "42"}, {:ref, "31"}}
)
# 0: 8 11 = 42+ (matched 42 31) = 42 42{N} 31{1,N-1} = 42{M} 31{1,M-1}
|> Map.put(
"0",
{:left_sided, {:ref, "42"}, {:ref, "31"}}
)
Enum.count(messages, fn msg ->
case RBNF.parse(grammar, {:ref, "0"}, msg) do
{_, ""} -> true
_ -> false
end
end)
end
end
Day19.part1() |> IO.inspect()
Day19.part2() |> IO.inspect()