diff --git a/day16/README b/day16/README index 9f18249..bbdfd31 100644 --- a/day16/README +++ b/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 diff --git a/day16/day16part1.exs b/day16/day16part1.exs index 5b2104e..5f3e2ab 100644 --- a/day16/day16part1.exs +++ b/day16/day16part1.exs @@ -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() diff --git a/day16/day16part2.exs b/day16/day16part2.exs index eb354bb..02b0f3a 100644 --- a/day16/day16part2.exs +++ b/day16/day16part2.exs @@ -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()