Files
Advent_of_code/src/2020/P16.py
David Doblas Jiménez 1352cd9394 Tidy up
2022-01-05 16:12:54 +01:00

246 lines
8.1 KiB
Python

# --- 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()