# --- Day 16: Ticket Translation --- # As you're walking to yet another connecting flight, you realize that one of # the legs of your re-routed trip coming up is on a high-speed train. However, # the train ticket you were given is in a language you don't understand. You # should probably figure out what it says before you get to the train station # after the next flight. # Unfortunately, you can't actually read the words on the ticket. You can, # however, read the numbers, and so you figure out the fields these tickets # must have and the valid ranges for values in those fields. # You collect the rules for ticket fields, the numbers on your ticket, and the # numbers on other nearby tickets for the same train service (via the airport # security cameras) together into a single document you can reference (your # puzzle input). # The rules for ticket fields specify a list of fields that exist somewhere on # the ticket and the valid ranges of values for each field. For example, a rule # like class: 1-3 or 5-7 means that one of the fields in every ticket is named # class and can be any value in the ranges 1-3 or 5-7 (inclusive, such that 3 # and 5 are both valid in this field, but 4 is not). # Each ticket is represented by a single line of comma-separated values. The # values are the numbers on the ticket in the order they appear; every ticket # has the same format. For example, consider this ticket: # .--------------------------------------------------------. # | ????: 101 ?????: 102 ??????????: 103 ???: 104 | # | | # | ??: 301 ??: 302 ???????: 303 ??????? | # | ??: 401 ??: 402 ???? ????: 403 ????????? | # '--------------------------------------------------------' # Here, ? represents text in a language you don't understand. This ticket might # be represented as 101,102,103,104,301,302,303,401,402,403; of course, the # actual train tickets you're looking at are much more complicated. In any # case, you've extracted just the numbers in such a way that the first number # is always the same specific field, the second number is always a different # specific field, and so on - you just don't know what each position actually # means! # Start by determining which tickets are completely invalid; these are tickets # that contain values which aren't valid for any field. Ignore your ticket for # now. # For example, suppose you have the following notes: # class: 1-3 or 5-7 # row: 6-11 or 33-44 # seat: 13-40 or 45-46 # your ticket: # 7,1,14 # nearby tickets: # 7,3,47 # 40,4,50 # 55,2,20 # 38,6,12 # It doesn't matter which position corresponds to which field; you can identify # invalid nearby tickets by considering only whether tickets contain values # that are not valid for any field. In this example, the values on the first # nearby ticket are all valid for at least one field. This is not true of the # other three nearby tickets: the values 4, 55, and 12 are are not valid for # any field. Adding together all of the invalid values produces your ticket # scanning error rate: 4 + 55 + 12 = 71. # Consider the validity of the nearby tickets you scanned. What is your ticket # scanning error rate? from typing import Set with open("files/P16.txt", "r") as f: rules_raw, my_ticket, nearby_tickets = [ f.split("\n") for f in f.read().strip().split("\n\n") ] def part_1() -> None: rules = [] for rule in rules_raw: name = rule.split(": ")[0] valid_numbers = set() for range_numbers in rule.split(": ")[1].split(" or "): numbers = [int(number) for number in range_numbers.split("-")] first, last = range(*numbers).start, range(*numbers).stop list_numbers = list(range(first, last + 1)) for number in list_numbers: valid_numbers.add(number) rules.append([name, valid_numbers]) # Create a single set with all rules all_rules = set() for rule in rules: all_rules |= rule[1] all_tickets = [list(map(int, o.split(","))) for o in nearby_tickets[1:]] ticket_scanning_error_rate = sum( [ ticket for ticket_numbers in all_tickets for ticket in ticket_numbers if ticket not in all_rules ] ) print(f"The ticket scanning error rate is {ticket_scanning_error_rate}") # --- Part Two --- # Now that you've identified which tickets contain invalid values, discard # those tickets entirely. Use the remaining valid tickets to determine which # field is which. # Using the valid ranges for each field, determine what order the fields appear # on the tickets. The order is consistent between all tickets: if seat is the # third field, it is the third field on every ticket, including your ticket. # For example, suppose you have the following notes: # class: 0-1 or 4-19 # row: 0-5 or 8-19 # seat: 0-13 or 16-19 # your ticket: # 11,12,13 # nearby tickets: # 3,9,18 # 15,1,5 # 5,14,9 # Based on the nearby tickets in the above example, the first position must be # row, the second position must be class, and the third position must be seat; # you can conclude that in your ticket, class is 12, row is 11, and seat is 13. # Once you work out which field is which, look for the six fields on your # ticket that start with the word departure. What do you get if you multiply # those six values together? def from_part_1(): # from part_1 rules = [] for rule in rules_raw: name = rule.split(": ")[0] valid_numbers = set() for range_numbers in rule.split(": ")[1].split(" or "): numbers = [int(number) for number in range_numbers.split("-")] first, last = range(*numbers).start, range(*numbers).stop list_numbers = list(range(first, last + 1)) for number in list_numbers: valid_numbers.add(number) rules.append([name, valid_numbers]) all_rules = set() for rule in rules: all_rules |= rule[1] return rules, all_rules def remove_invalid(all_rules): valid_tickets = [] for line in nearby_tickets[1:]: valid = True for ticket in [int(t) for t in line.split(",")]: if ticket not in all_rules: valid = False break if valid: valid_tickets.append(line) return valid_tickets def check_validity(rules, ticket_values): # check validity of sets possibles = [] for c in rules: possibles.append([c[0], []]) for idx, ticket in enumerate(ticket_values): valid = True for value in ticket: if value not in c[1]: valid = False break if valid: possibles[-1][1].append(idx) return possibles def get_ordered_fields(sorted_possibles): # get each field and its respective order ordered_fields = [] for idx, (field, values) in enumerate(sorted_possibles): # first field if len(values) == 1: ordered_fields.append([field, values[0]]) else: value = [ v for v in values if v not in sorted_possibles[idx - 1][1] ][0] ordered_fields.append([field, value]) return ordered_fields def part_2() -> None: rules, all_rules = from_part_1() valid_tickets = remove_invalid(all_rules) # collect 20 sets of all respective values ticket_values = [set() for x in range(len(rules))] for line in valid_tickets: index = 0 for element in line.split(","): ticket_values[index].add(int(element)) index += 1 possibles = check_validity(rules, ticket_values) # sort possibles by the length of values each set holds sorted_possibles = sorted(possibles, key=lambda l: (len(l[1]), l)) sorted_fields = get_ordered_fields(sorted_possibles) # parse "my ticket" myticket = [ int(v) for elements in my_ticket[1:] for v in elements.split(",") ] res = 1 for (field, idx) in sorted_fields: if field.startswith("departure"): res *= myticket[idx] print(f"The final result is {res}") if __name__ == "__main__": part_1() part_2()