84 lines
3.2 KiB
Python
84 lines
3.2 KiB
Python
import re
|
|
from collections import defaultdict
|
|
|
|
from solutions import BaseSolution
|
|
|
|
|
|
class Solution(BaseSolution):
|
|
input_file = "16.txt"
|
|
|
|
def __str__(self):
|
|
return "Day 16: Ticket Translation"
|
|
|
|
def parse_input(self, data):
|
|
def get_ranges(line):
|
|
label = line.split(":")[0]
|
|
r1a, r1b, r2a, r2b = re.findall(rxranges, line.strip())
|
|
return (label, range(int(r1a), int(r1b) + 1), range(int(r2a), int(r2b) + 1))
|
|
|
|
rxranges = r"([0-9]+)"
|
|
ranges, golden_ticket, other_tickets = data.split("\n\n")
|
|
ranges = [*map(get_ranges, ranges.splitlines())]
|
|
golden_ticket = [*map(int, golden_ticket.splitlines()[1].split(","))]
|
|
other_tickets = [
|
|
[*map(int, ticket.split(","))] for ticket in other_tickets.splitlines()[1:]
|
|
]
|
|
return (ranges, golden_ticket, other_tickets)
|
|
|
|
def get_invalid_values(self, ranges, ticket):
|
|
def not_in_any_range(x):
|
|
return all(x not in r1 and x not in r2 for _, r1, r2 in ranges)
|
|
|
|
return [*filter(not_in_any_range, ticket)]
|
|
|
|
def solve(self, puzzle_input):
|
|
ranges, _, tickets = puzzle_input
|
|
error_rate = 0
|
|
valid_tickets = []
|
|
for ticket in tickets:
|
|
error_rate += sum(self.get_invalid_values(ranges, ticket))
|
|
return error_rate
|
|
|
|
def solve_again(self, puzzle_input):
|
|
ranges, golden_ticket, tickets = puzzle_input
|
|
valid_tickets = []
|
|
for ticket in tickets:
|
|
if len(self.get_invalid_values(ranges, ticket)) == 0:
|
|
valid_tickets.append(ticket)
|
|
sorted_ranges = self.get_positions(ranges, valid_tickets)
|
|
factors = [i for i, r in enumerate(sorted_ranges) if "departure" in r]
|
|
m = 1
|
|
for f in factors:
|
|
m *= golden_ticket[f]
|
|
return m
|
|
|
|
def get_positions(self, ranges, valid_tickets):
|
|
columns = [*zip(*valid_tickets)]
|
|
matches = defaultdict(lambda: set())
|
|
# Step 1: find all possible range matches for columns
|
|
# Example: {'row': {0, 1, 2}, 'class': {1, 2}, 'seat': {2}}
|
|
for e, column in enumerate(columns):
|
|
for column_name, r1, r2 in ranges:
|
|
if all(x in r1 or x in r2 for x in column):
|
|
matches[column_name].add(e)
|
|
# Step 2: collect all columns with only one matching range, and subtract
|
|
# these ranges from the columns having multiple ranges. Repeat until all
|
|
# columns only have one matching range.
|
|
# Example:
|
|
# #1: {'row': {0, 1, 2}, 'class': {1, 2}, 'seat': {2}}
|
|
# #2: {'row': {0, 1}, 'class': {1}, 'seat': {2}}
|
|
# #3: {'row': {0}, 'class': {1}, 'seat': {2}}
|
|
while any(len(v) > 1 for _k, v in matches.items()):
|
|
singles = set({}).union(*[v for _k, v in matches.items() if len(v) == 1])
|
|
for m in matches:
|
|
if len(matches[m]) > 1:
|
|
matches[m] = matches[m] - singles
|
|
# step 3: Sort the matches list and return a list only containing the
|
|
# column names.
|
|
final = sorted([(k, v.pop()) for k, v in matches.items()], key=lambda kv: kv[1])
|
|
return [k for k, _v in final]
|
|
|
|
|
|
if __name__ == "__main__":
|
|
solution = Solution()
|
|
solution.show_results()
|