Tidy Day 16
This commit is contained in:
parent
3cff76a05b
commit
343d2ef962
3 changed files with 91 additions and 98 deletions
49
day16/README
49
day16/README
|
@ -9,47 +9,44 @@ $ elixir day16part1.exs
|
|||
|
||||
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 |
|
||||
+--------+
|
||||
|
||||
$ elixir day16part2.exs
|
||||
Idenfitied columns: %{
|
||||
"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
|
||||
1001849322119
|
||||
|
||||
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 |
|
||||
+------------------+
|
||||
|
||||
Initial version. Will tidy up later.
|
||||
|
||||
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
|
||||
I'm going to relegate them to the weekend. Doing them in the evenings has too much potential
|
||||
|
|
|
@ -1,27 +1,32 @@
|
|||
defmodule Day16Part1 do
|
||||
def run do
|
||||
{rules, _my_ticket, tickets} =
|
||||
File.read!("input")
|
||||
|> parse_input()
|
||||
{rules, tickets} = File.read!("input") |> parse_input()
|
||||
|
||||
Enum.flat_map(tickets, &invalid_fields(&1, rules))
|
||||
Enum.flat_map(tickets, &find_invalid_fields(&1, rules))
|
||||
|> Enum.sum()
|
||||
|> IO.puts()
|
||||
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
|
||||
[rules, [_, my_ticket | _], [_ | nearby_tickets]] =
|
||||
[rules, _, [_ | nearby_tickets]] =
|
||||
input
|
||||
|> String.split("\n\n", trim: true)
|
||||
|> Enum.map(&String.split(&1, "\n", trim: true))
|
||||
|
||||
rules = Enum.map(rules, &parse_rule/1)
|
||||
|
||||
[my_ticket | nearby_tickets] =
|
||||
[my_ticket | nearby_tickets]
|
||||
|> Enum.map(fn ticket -> String.split(ticket, ",") |> Enum.map(&String.to_integer/1) end)
|
||||
nearby_tickets =
|
||||
for ticket <- nearby_tickets do
|
||||
ticket |> String.split(",") |> Enum.map(&String.to_integer/1)
|
||||
end
|
||||
|
||||
{rules, my_ticket, nearby_tickets}
|
||||
{rules, nearby_tickets}
|
||||
end
|
||||
|
||||
def parse_rule(rule) do
|
||||
|
@ -31,12 +36,6 @@ defmodule Day16Part1 do
|
|||
[a, b, c, d] = Enum.map(ranges, &String.to_integer/1)
|
||||
{name, a..b, c..d}
|
||||
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
|
||||
|
||||
Day16Part1.run()
|
||||
|
|
|
@ -1,17 +1,66 @@
|
|||
defmodule Day16Part2 do
|
||||
def run do
|
||||
{rules, my_ticket, tickets} =
|
||||
File.read!("input")
|
||||
|> parse_input()
|
||||
{rules, my_ticket, tickets} = File.read!("input") |> parse_input()
|
||||
|
||||
tickets
|
||||
|> Enum.filter(&valid?(&1, rules))
|
||||
|> transpose_tickets()
|
||||
|> identify_columns(rules)
|
||||
|> IO.inspect(label: "Idenfitied columns", charlists: :as_lists)
|
||||
|> Enum.filter(&match?({"departure" <> _, _idx}, &1))
|
||||
|> 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
|
||||
|
||||
def parse_input(input) do
|
||||
|
@ -38,58 +87,6 @@ defmodule Day16Part2 do
|
|||
[a, b, c, d] = Enum.map(ranges, &String.to_integer/1)
|
||||
{name, {a..b, c..d}}
|
||||
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
|
||||
|
||||
Day16Part2.run()
|
||||
|
|
Loading…
Reference in a new issue