Tidy Day 16

This commit is contained in:
Adam Millerchip 2020-12-16 23:57:57 +09:00
parent 3cff76a05b
commit 343d2ef962
3 changed files with 91 additions and 98 deletions

View file

@ -9,47 +9,44 @@ $ elixir day16part1.exs
Thoughts: Thoughts:
Slightly complex input to parse this time. Most of the solution is parsing.
The actual logic: Use Enum.any?(rules) to check if a ticket satisfies any of the rules.
Filter those results by only those that are not valid.
Flat map the filtered tickets, to get a flat list of fields.
Sum the result.
+--------+ +--------+
| Part 2 | | Part 2 |
+--------+ +--------+
$ elixir day16part2.exs $ elixir day16part2.exs
Idenfitied columns: %{ 1001849322119
"arrival location" => 18,
"arrival platform" => 12,
"arrival station" => 10,
"arrival track" => 7,
"class" => 15,
"departure date" => 11,
"departure location" => 2,
"departure platform" => 13,
"departure station" => 14,
"departure time" => 1,
"departure track" => 9,
"duration" => 0,
"price" => 5,
"route" => 16,
"row" => 19,
"seat" => 17,
"train" => 4,
"type" => 3,
"wagon" => 6,
"zone" => 8
}
Answer: 1001849322119
Thoughts: Thoughts:
More complicated as I initially thought, because most columns are valid for more than
one rule. Interestingly no column is valid for the *same* number of rules, which makes me
think this is a manifestation of some maths problem I don't know about.
Anyway, solve it by:
* Transposing the tickets into lists of "columns"
* Match each column against the rules it satisfies
* Starting with the column that only matches one rule, mark that column as solved.
* Continue for subsequent rules, removing the solved columns from the set of rules it
satisfies as we go.
I'm sure there is a more efficient way to handle this rather than the building up n lists
and then for each entry removing that from the remaining lists.
+------------------+ +------------------+
| Overall Thoughts | | Overall Thoughts |
+------------------+ +------------------+
Initial version. Will tidy up later.
Spent a bit too long on silly mistakes in this one. Think I need to consider my development Spent a bit too long on silly mistakes in this one. Think I need to consider my development
process to help avoid making errors. process to help avoid making errors, especially when dealing with multiple related data structures
/ representation of those data structures.
Also, I'm finding these daily puzzles a bit too distracting from work. I think after today Also, I'm finding these daily puzzles a bit too distracting from work. I think after today
I'm going to relegate them to the weekend. Doing them in the evenings has too much potential I'm going to relegate them to the weekend. Doing them in the evenings has too much potential

View file

@ -1,27 +1,32 @@
defmodule Day16Part1 do defmodule Day16Part1 do
def run do def run do
{rules, _my_ticket, tickets} = {rules, tickets} = File.read!("input") |> parse_input()
File.read!("input")
|> parse_input()
Enum.flat_map(tickets, &invalid_fields(&1, rules)) Enum.flat_map(tickets, &find_invalid_fields(&1, rules))
|> Enum.sum() |> Enum.sum()
|> IO.puts() |> IO.puts()
end end
def find_invalid_fields(ticket, rules) do
Enum.reject(ticket, fn field ->
Enum.any?(rules, fn {_name, range1, range2} -> field in range1 || field in range2 end)
end)
end
def parse_input(input) do def parse_input(input) do
[rules, [_, my_ticket | _], [_ | nearby_tickets]] = [rules, _, [_ | nearby_tickets]] =
input input
|> String.split("\n\n", trim: true) |> String.split("\n\n", trim: true)
|> Enum.map(&String.split(&1, "\n", trim: true)) |> Enum.map(&String.split(&1, "\n", trim: true))
rules = Enum.map(rules, &parse_rule/1) rules = Enum.map(rules, &parse_rule/1)
[my_ticket | nearby_tickets] = nearby_tickets =
[my_ticket | nearby_tickets] for ticket <- nearby_tickets do
|> Enum.map(fn ticket -> String.split(ticket, ",") |> Enum.map(&String.to_integer/1) end) ticket |> String.split(",") |> Enum.map(&String.to_integer/1)
end
{rules, my_ticket, nearby_tickets} {rules, nearby_tickets}
end end
def parse_rule(rule) do def parse_rule(rule) do
@ -31,12 +36,6 @@ defmodule Day16Part1 do
[a, b, c, d] = Enum.map(ranges, &String.to_integer/1) [a, b, c, d] = Enum.map(ranges, &String.to_integer/1)
{name, a..b, c..d} {name, a..b, c..d}
end end
def invalid_fields(ticket, rules) do
Enum.reject(ticket, fn field ->
Enum.any?(rules, fn {_name, range1, range2} -> field in range1 || field in range2 end)
end)
end
end end
Day16Part1.run() Day16Part1.run()

View file

@ -1,17 +1,66 @@
defmodule Day16Part2 do defmodule Day16Part2 do
def run do def run do
{rules, my_ticket, tickets} = {rules, my_ticket, tickets} = File.read!("input") |> parse_input()
File.read!("input")
|> parse_input()
tickets tickets
|> Enum.filter(&valid?(&1, rules)) |> Enum.filter(&valid?(&1, rules))
|> transpose_tickets() |> transpose_tickets()
|> identify_columns(rules) |> identify_columns(rules)
|> IO.inspect(label: "Idenfitied columns", charlists: :as_lists)
|> Enum.filter(&match?({"departure" <> _, _idx}, &1)) |> Enum.filter(&match?({"departure" <> _, _idx}, &1))
|> Enum.reduce(1, fn {_name, idx}, acc -> my_ticket[idx] * acc end) |> Enum.reduce(1, fn {_name, idx}, acc -> my_ticket[idx] * acc end)
|> IO.inspect(label: "Answer") |> IO.puts()
end
def valid?(ticket, rules) do
Enum.all?(ticket, fn field ->
Enum.any?(rules, fn {_name, {range1, range2}} -> field in range1 || field in range2 end)
end)
end
def transpose_tickets(tickets), do: transpose_tickets(tickets, [], 0, length(hd(tickets)))
def transpose_tickets(_tickets, columns, stop, stop), do: columns
def transpose_tickets(tickets, columns, field_index, stop) do
{remaining_columns, column} = next_column(tickets)
transpose_tickets(remaining_columns, [{field_index, column} | columns], field_index + 1, stop)
end
def next_column(tickets) do
Enum.map_reduce(tickets, [], fn [field | rest], column -> {rest, [field | column]} end)
end
# Probably overcomplicated this.
# First find which columns satisfy which rules.
# The number of valid fields for each column is unique, which doesn't seem like a coincidence.
# Then allocate the columns to fields starting from the column with the smallest number
# of valid columns - need to filter out the known values for the rest of the columns.
# It's the final filtering part that makes me think I'm missing a trick.
def identify_columns(transposed_tickets, rules) do
{field_map, by_valid_count} =
Enum.reduce(transposed_tickets, {%{}, %{}}, fn {id, column}, {field_map, by_valid_count} ->
names = names_for(column, rules)
valid_count = length(names)
{Map.put(field_map, id, names), Map.put(by_valid_count, valid_count, id)}
end)
{identified, _seen} =
by_valid_count
|> Enum.sort(fn {k1, _}, {k2, _} -> k1 <= k2 end)
|> Enum.reduce({%{}, MapSet.new()}, fn {_, col}, {identified, seen} ->
field = field_map[col] |> MapSet.new() |> MapSet.difference(seen) |> Enum.at(0)
{Map.put(identified, field, col), MapSet.put(seen, field)}
end)
identified
end
def names_for(column, rules) do
Enum.filter(rules, fn {_field, {range1, range2}} ->
Enum.all?(column, fn val -> val in range1 || val in range2 end)
end)
|> Enum.map(&elem(&1, 0))
end end
def parse_input(input) do def parse_input(input) do
@ -38,58 +87,6 @@ defmodule Day16Part2 do
[a, b, c, d] = Enum.map(ranges, &String.to_integer/1) [a, b, c, d] = Enum.map(ranges, &String.to_integer/1)
{name, {a..b, c..d}} {name, {a..b, c..d}}
end end
def valid?(ticket, rules) do
Enum.all?(ticket, fn field ->
Enum.any?(rules, fn {_name, {range1, range2}} -> field in range1 || field in range2 end)
end)
end
def transpose_tickets(tickets), do: transpose_tickets(tickets, [], 0, length(hd(tickets)))
def transpose_tickets(_tickets, columns, stop, stop), do: columns
def transpose_tickets(tickets, columns, field_index, stop) do
{remaining_columns, column} = next_column(tickets)
transpose_tickets(remaining_columns, [{field_index, column} | columns], field_index + 1, stop)
end
def next_column(tickets) do
Enum.map_reduce(tickets, [], fn [field | rest], column -> {rest, [field | column]} end)
end
def identify_column(column, rules) do
Enum.filter(rules, fn {_field, {range1, range2}} ->
Enum.all?(column, fn val -> val in range1 || val in range2 end)
end)
|> Enum.map(&elem(&1, 0))
end
# Probably overcomplicated this.
# First find which columns satisfy which rules.
# The number of valid fields for each column is unique, which doesn't seem like a coincidence.
# Then allocate the columns to fields starting from the column with the smallest number
# of valid columns - need to filter out the known values for the rest of the columns.
# It's the final filtering part that makes me think I'm missing a trick.
def identify_columns(transposed_tickets, rules) do
{field_map, by_valid_count} =
Enum.reduce(transposed_tickets, {%{}, %{}}, fn {id, column}, {field_map, by_valid_count} ->
names = identify_column(column, rules)
valid_count = length(names)
{Map.put(field_map, id, names), Map.put(by_valid_count, valid_count, id)}
end)
{identified, _seen} =
by_valid_count
|> Enum.sort(fn {k1, _}, {k2, _} -> k1 <= k2 end)
|> Enum.reduce({%{}, MapSet.new()}, fn {_, col}, {identified, seen} ->
field = field_map[col] |> MapSet.new() |> MapSet.difference(seen) |> Enum.at(0)
{Map.put(identified, field, col), MapSet.put(seen, field)}
end)
identified
end
end end
Day16Part2.run() Day16Part2.run()