diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..5e0864dbf43df3ae4c635e0b9408d0df08ca94e0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,145 @@ +# Project-specific +ImportData.py +apikeys.json + +# Mac file finder metadata +.DS_Store +# Windows file metadata +._* +# Thumbnail image caches +Thumbs.db +ethumbs.db +# MS Office temporary file +~* +# Emacs backup file +*~ + +# Common +[Bb]in/ +[Bb]uild/ +[Oo]bj/ +[Oo]ut/ +[Tt]mp/ +[Xx]86/ +[Ii][Aa]32/ +[Xx]64/ +[Xx]86_64/ +[Xx]86-64/ +[Aa]rm +[Aa]32 +[Tt]32 +[Aa]64 +[Aa]rch32 +[Aa]rch64 +*.tmp +*.bak +*.bk +*.swp + +# Miscellaneous +*.gcno + +# C files +*.o +*.out + +# Java files +*.class +javadoc/ + +# Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties + +# Python files +*.pyc +*.pyo +__pycache__/ + +# Swift files +*.hmap +*.ipa +*.dSYM.zip +*.dSYM + +# JetBrains (IntelliJ IDEA, PyCharm, etc) files +.idea/ +cmake-build-*/ +*.iml +*.iws +*.ipr +venv/ + +# Eclipse files +.settings/ +.project +.classpath +.buildpath +.loadpath +.factorypath +local.properties + +# Visual Studio / VS Code files +.vs*/ +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile +*.psess +*.vsp +*.vspx + +# Netbeans files +nbproject/private/ +build/ +nbbuild/ +dist/ +nbdist/ +nbactions.xml +nb-configuration.xml + +# Xcode files +*.xcodeproj/ +xcuserdata/ +.build/ + diff --git a/2020/A201901.py b/2020/A201901.py new file mode 100644 index 0000000000000000000000000000000000000000..3631376b448d7dab0db909966478e9f7b2aa0d50 --- /dev/null +++ b/2020/A201901.py @@ -0,0 +1,52 @@ +from typing import List + +from ImportData import import_data + +day: int = 1 + +sample_data: List[str] = [ + '100756' +] + +data_structure: type = List[int] + + +def parse_data(data: List[str]) -> data_structure: + return list(map(lambda m: int(m), data)) + + +def compute_required_fuel(module_mass: int) -> int: + return module_mass // 3 - 2 + + +def part1(data: data_structure) -> int: + return sum(map(lambda m: compute_required_fuel(m), data)) + + +def part2(data: data_structure) -> int: + next_fuel_demand: int = part1(data) + total_fuel_demand: int = 0 + while next_fuel_demand > 0: + total_fuel_demand += next_fuel_demand + next_fuel_demand = compute_required_fuel(next_fuel_demand) + return total_fuel_demand + + +def adjusted_compute_required_fuel(module_mass: int) -> int: + next_fuel_demand: int = compute_required_fuel(module_mass) + total_fuel_demand: int = 0 + while next_fuel_demand > 0: + total_fuel_demand += next_fuel_demand + next_fuel_demand = compute_required_fuel(next_fuel_demand) + return total_fuel_demand + + +def part2a(data:data_structure) -> int: + return sum(map(lambda m: adjusted_compute_required_fuel(m), data)) + + +if __name__ == '__main__': + production_ready = True + raw_data = import_data(day, 2019) if production_ready else sample_data + print(part1(parse_data(raw_data))) + print(part2a(parse_data(raw_data))) diff --git a/2020/A201902.py b/2020/A201902.py new file mode 100644 index 0000000000000000000000000000000000000000..f1ab8cd7357c9cfe7f63bc93eb10cc8491b8b066 --- /dev/null +++ b/2020/A201902.py @@ -0,0 +1,48 @@ +from typing import List + +from A2019_guidance_computer import compute +from ImportData import import_data + +day: int = 2 + +sample_data: List[str] = [ + '1,9,10,3,2,3,11,0,99,30,40,50' +] + +data_structure: type = List[int] + + +def parse_data(data: List[str]) -> data_structure: + return list(map(lambda x: int(x), data[0].split(','))) + + +def part1(data: data_structure) -> int: + memory: List[int] = data.copy() + memory[1] = 12 + memory[2] = 2 + memory, _ = compute(memory) + return memory[0] + + +def part2(data: data_structure) -> int: + running: bool = True + noun: int = -1 + verb: int = -1 + while running and noun < 99: + noun += 1 + verb = -1 + while running and verb < 99: + verb += 1 + memory: List[int] = data.copy() + memory[1] = noun + memory[2] = verb + memory, _ = compute(memory, []) + running = memory[0] != 19690720 + return 100 * noun + verb + + +if __name__ == '__main__': + production_ready = True + raw_data = import_data(day, 2019) if production_ready else sample_data + print(part1(parse_data(raw_data))) + print(part2(parse_data(raw_data))) diff --git a/2020/A201903.py b/2020/A201903.py new file mode 100644 index 0000000000000000000000000000000000000000..dfdf308a1e9745dbf7072213f9abb81e2263fd57 --- /dev/null +++ b/2020/A201903.py @@ -0,0 +1,132 @@ +from typing import List, Tuple, Set, Dict + +from ImportData import import_data + +day: int = 3 + +sample_data1: List[str] = [ + 'R8,U5,L5,D3', + 'U7,R6,D4,L4' +] + +sample_data: List[str] = [ + 'R75,D30,R83,U83,L12,D49,R71,U7,L72', + 'U62,R66,U55,R34,D71,R55,D58,R83' +] + +data_structure: type = Tuple[List[Tuple[str, int]], List[Tuple[str, int]]] + + +def parse_data(data: List[str]) -> data_structure: + raw_first_wire: List[str] = data[0].split(',') + raw_second_wire: List[str] = data[1].split(',') + first_wire: List[Tuple[str, int]] = [] + second_wire: List[Tuple[str, int]] = [] + for path in raw_first_wire: + first_wire.append((path[0], int(path[1:]))) + for path in raw_second_wire: + second_wire.append((path[0], int(path[1:]))) + return first_wire, second_wire + + +ordered_pair: type = Tuple[int, int] + + +def trace_path1(initial_position: ordered_pair, direction, distance) -> Tuple[Set[ordered_pair], ordered_pair]: + points: Set[ordered_pair] = {initial_position} + current_position: ordered_pair = (initial_position[0], initial_position[1]) + distance_travelled = 0 + while distance_travelled < distance: + distance_travelled += 1 + if direction == 'U': + current_position = (current_position[0], current_position[1] + 1) + elif direction == 'D': + current_position = (current_position[0], current_position[1] - 1) + elif direction == 'R': + current_position = (current_position[0] + 1, current_position[1]) + elif direction == 'L': + current_position = (current_position[0] - 1, current_position[1]) + else: + raise RuntimeError(f'Unexpected direction: {direction}') + points.add(current_position) + return points, current_position + + +def part1(data: data_structure) -> int: + current_position: ordered_pair = (0, 0) + first_wire_positions: Set[ordered_pair] = {current_position} + second_wire_positions: Set[ordered_pair] = {current_position} + for path in data[0]: + new_positions, current_position = trace_path1(current_position, path[0], path[1]) + first_wire_positions = first_wire_positions.union(new_positions) + current_position = (0, 0) + for path in data[1]: + new_positions, current_position = trace_path1(current_position, path[0], path[1]) + second_wire_positions = second_wire_positions.union(new_positions) + intersections: List[ordered_pair] = sorted(first_wire_positions.intersection(second_wire_positions), + key=lambda point: abs(point[0]) + abs(point[1])) + return abs(intersections[1][0]) + abs(intersections[1][1]) + + +modified_ordered_pair: type = Tuple[int, int, int] + + +def trace_path2(initial_position: modified_ordered_pair, direction: str, distance: int) -> List[modified_ordered_pair]: + points: List[modified_ordered_pair] = [] # here we'll assume the initial position is already in the master list + current_position: modified_ordered_pair = (initial_position[0], initial_position[1], initial_position[2]) + distance_traveled = 0 + while distance_traveled < distance: + distance_traveled += 1 + if direction == 'U': + current_position = (current_position[0], current_position[1] + 1, current_position[2] + 1) + elif direction == 'D': + current_position = (current_position[0], current_position[1] - 1, current_position[2] + 1) + elif direction == 'R': + current_position = (current_position[0] + 1, current_position[1], current_position[2] + 1) + elif direction == 'L': + current_position = (current_position[0] - 1, current_position[1], current_position[2] + 1) + else: + raise RuntimeError(f'Unexpected direction: {direction}') + points.append(current_position) + return points + + +def part2(data: data_structure) -> int: + current_position: modified_ordered_pair = (0, 0, 0) + first_wire_positions: List[modified_ordered_pair] = [] # intentionally leaving central port off list + second_wire_positions: List[modified_ordered_pair] = [] # (initially) + for path in data[0]: + new_positions: List[modified_ordered_pair] = trace_path2(current_position, path[0], path[1]) + current_position = new_positions[-1] + first_wire_positions.extend(new_positions) + current_position = (0, 0, 0) + for path in data[1]: + new_positions: List[modified_ordered_pair] = trace_path2(current_position, path[0], path[1]) + current_position = new_positions[-1] + second_wire_positions.extend(new_positions) + first_wire_map: Dict[ordered_pair, int] = {} + second_wire_map: Dict[ordered_pair, int] = {} + for point in first_wire_positions: + coordinates: ordered_pair = (point[0], point[1]) + distance = point[2] + if coordinates not in first_wire_map or distance < first_wire_map[coordinates]: + first_wire_map[coordinates] = distance + for point in second_wire_positions: + coordinates: ordered_pair = (point[0], point[1]) + distance = point[2] + if coordinates not in second_wire_map or distance < second_wire_map[coordinates]: + second_wire_map[coordinates] = distance + intersections: Set[ordered_pair] = set(first_wire_map.keys()).intersection(second_wire_map.keys()) + minimum_distance: int = 2 ** 30 + for point in intersections: + combined_distance: int = first_wire_map[point] + second_wire_map[point] + if combined_distance < minimum_distance: + minimum_distance = combined_distance + return minimum_distance + + +if __name__ == '__main__': + production_ready = True + raw_data = import_data(day, 2019) if production_ready else sample_data + print(part1(parse_data(raw_data))) + print(part2(parse_data(raw_data))) diff --git a/2020/A201904.py b/2020/A201904.py new file mode 100644 index 0000000000000000000000000000000000000000..d88cce52999b85e4a7736e6c6c35e9ce9f01c6c2 --- /dev/null +++ b/2020/A201904.py @@ -0,0 +1,44 @@ +from typing import List, Tuple, Set + +data_structure: type = Tuple[int, int] + + +def parse_data(data: List[str]) -> data_structure: + bounds: List[str] = data[0].split('-') + return int(bounds[0]), int(bounds[1]) + + +def meets_criteria(value: int, bounds: Tuple[int, int], extra_constraint: bool = False) -> bool: + satisfies: bool = 100000 <= value <= 999999 # 6-digit + satisfies &= bounds[0] <= value <= bounds[1] # within range + digits: List[int] = [10] + working_number: int = value + has_matching_adjacent_digits: bool = False + for i in range(6): + digits.append(working_number % 10) + working_number = working_number // 10 + satisfies &= digits[i + 1] <= digits[i] # non-strictly monotonically increasing digits from left-to-right + has_matching_adjacent_digits |= digits[i + 1] == digits[i] + satisfies &= has_matching_adjacent_digits # has at least one pair of identical digits + if extra_constraint and satisfies: + unique_digits: Set[int] = set(digits) + has_pair_of_matching_digits: bool = False + for digit in unique_digits: + has_pair_of_matching_digits |= digits.count(digit) == 2 + satisfies &= has_pair_of_matching_digits + return satisfies + + +def part1(data: data_structure) -> int: + return len([password for password in range(data[0], data[1] + 1) if meets_criteria(password, (data[0], data[1]))]) + + +def part2(data: data_structure) -> int: + return len([password for password in range(data[0], data[1] + 1) + if meets_criteria(password, (data[0], data[1]), True)]) + + +if __name__ == '__main__': + raw_data = ['178416-676461'] + print(part1(parse_data(raw_data))) + print(part2(parse_data(raw_data))) diff --git a/2020/A201905.py b/2020/A201905.py new file mode 100644 index 0000000000000000000000000000000000000000..82c545324c97a626e7b98cb936883de173725147 --- /dev/null +++ b/2020/A201905.py @@ -0,0 +1,37 @@ +from typing import List + +from A2019_guidance_computer import compute +from ImportData import import_data + +day: int = 5 + +sample_data: List[str] = [ + +] + +data_structure: type = List[int] + + +def parse_data(data: List[str]) -> data_structure: + return list(map(lambda x: int(x), data[0].split(','))) + + +def part1(data: data_structure) -> int: + memory: List[int] = data.copy() + inputs: List[int] = [1] + memory, outputs = compute(memory, inputs) + return outputs[-1] + + +def part2(data: data_structure) -> int: + memory: List[int] = data.copy() + inputs: List[int] = [5] + memory, outputs = compute(memory, inputs) + return outputs[-1] + + +if __name__ == '__main__': + production_ready = True + raw_data = import_data(day, 2019) if production_ready else sample_data + print(part1(parse_data(raw_data))) + print(part2(parse_data(raw_data))) diff --git a/2020/A201906.py b/2020/A201906.py new file mode 100644 index 0000000000000000000000000000000000000000..89b9e3289a218d2435c435589b480a932344901b --- /dev/null +++ b/2020/A201906.py @@ -0,0 +1,63 @@ +from typing import List, Dict, Optional, Set + +from ImportData import import_data + +day: int = 6 + +sample_data: List[str] = [ + 'COM)B', + 'B)C', + 'C)D', + 'D)E', + 'E)F', + 'B)G', + 'G)H', + 'D)I', + 'E)J', + 'J)K', + 'K)L', +] + +data_structure: type = Dict[str, Optional[str]] # object's name, parent object's name + + +def parse_data(data: List[str]) -> data_structure: + orbit_map = {'COM': None} + for orbit in data: + bodies = orbit.split(')') + orbit_map[bodies[1]] = bodies[0] + return orbit_map + + +def part1(data: data_structure) -> int: + orbit_count: int = 0 + for body in data: + parent_body = data[body] + while parent_body is not None: + orbit_count += 1 + parent_body = data[parent_body] + return orbit_count + + +def part2(data: data_structure) -> int: + my_parent_bodies: List[str] = [data['YOU']] + santa_parent_bodies: List[str] = [data['SAN']] + parent_body = data[my_parent_bodies[0]] + while parent_body is not None: + my_parent_bodies.append(parent_body) + parent_body = data[parent_body] + parent_body = data[santa_parent_bodies[0]] + while parent_body is not None: + santa_parent_bodies.append(parent_body) + parent_body = data[parent_body] + common_parents: Set[str] = set(my_parent_bodies).intersection(santa_parent_bodies) + return min(map(lambda body: my_parent_bodies.index(body) + santa_parent_bodies.index(body), common_parents)) + + +if __name__ == '__main__': + production_ready = True + raw_data = import_data(day, 2019) if production_ready else sample_data + print(part1(parse_data(raw_data))) + if not production_ready: + raw_data.extend(['K)YOU','I)SAN']) + print(part2(parse_data(raw_data))) diff --git a/2020/A201907.py b/2020/A201907.py new file mode 100644 index 0000000000000000000000000000000000000000..84fac2617cf018e36b6c5156e6e345bc4521c7ca --- /dev/null +++ b/2020/A201907.py @@ -0,0 +1,76 @@ +from itertools import permutations +from typing import List, Tuple, Iterator + +from A2019_guidance_computer import compute +from ImportData import import_data + +day: int = 7 + +sample_data1: List[str] = ['3,15,3,16,1002,16,10,16,1,16,15,15,4,15,99,0,0'] +sample_data2: List[str] = ['3,23,3,24,1002,24,10,24,1002,23,-1,23,101,5,23,23,1,24,23,23,4,23,99,0,0'] +sample_data3: List[str] = [ + '3,31,3,32,1002,32,10,32,1001,31,-2,31,1007,31,0,33,1002,33,7,33,1,33,31,31,1,32,31,31,4,31,99,0,0,0' +] +sample_data4: List[str] = [ + '3,26,1001,26,-4,26,3,27,1002,27,2,27,1,27,26,27,4,27,1001,28,-1,28,1005,28,6,99,0,0,5' +] +sample_data5: List[str] = [ + '3,52,1001,52,-5,52,3,53,1,52,56,54,1007,54,5,55,1005,55,26,1001,54,-5,54,1105,1,12,1,53,54,53,1008,54,0,55,1001,55,1,55,2,53,55,53,4,53,1001,56,-1,56,1005,56,6,99,0,0,0,0,10' +] + +data_structure: type = List[int] + + +def parse_data(data: List[str]) -> data_structure: + return list(map(lambda x: int(x), data[0].split(','))) + + +def part1(data: data_structure) -> int: + phase_permutations: Iterator[Tuple[int, ...]] = permutations(range(5)) + final_output: int = -1 + # noinspection PyUnusedLocal + outputs: List[int] = [] + for phases in phase_permutations: + # print(phases, end='--<') + amp_input: int = 0 + for i in range(5): + # print(amp_input, end=',') + memory: List[int] = data.copy() + _, outputs = compute(memory, [phases[i], amp_input]) + amp_input = outputs[-1] + # print(f'{amp_input}>') + final_output = max(final_output, outputs[-1]) + return final_output + + +def part2(data: data_structure) -> int: + phase_permutations: Iterator[Tuple[int, ...]] = permutations(range(5, 10)) + # phase_permutations = [(9, 8, 7, 6, 5)] + final_output: int = -1 + # noinspection PyUnusedLocal + outputs: List[int] = [] + for phases in phase_permutations: + memories = [data.copy() for _ in range(5)] + running: bool = True + amp_input: int = 0 + i: int = 0 + while running: + memories[i], outputs = compute(memories[i], [phases[i], amp_input]) + amp_input = outputs[-1] + if i == 4: + print(f'{phases}: {amp_input}') + if memories[i][-2:] != [2 ** 30, 99] and i == 4: + running = False + else: + i = (i + 1) % 5 + final_output = max(final_output, outputs[-1]) + return final_output + + +if __name__ == '__main__': + production_ready = False + raw_data = import_data(day, 2019) if production_ready else sample_data3 + print(part1(parse_data(raw_data))) + if not production_ready: + raw_data = sample_data5 + print(part2(parse_data(raw_data))) diff --git a/2020/A2019_guidance_computer.py b/2020/A2019_guidance_computer.py new file mode 100644 index 0000000000000000000000000000000000000000..bbe10bcc579f00af4b87993de249dd4e0fa0b641 --- /dev/null +++ b/2020/A2019_guidance_computer.py @@ -0,0 +1,67 @@ +from typing import Tuple, List, Optional + +maximum_number_of_parameters: int = 3 + + +def execute_instruction(instruction_pointer: int, opcode: int, parameters: List[int], inputs: List[int]) \ + -> Tuple[int, Optional[int], Optional[int]]: # new instruction pointer, value, output + if opcode == 1: # addition + return instruction_pointer + 4, parameters[0] + parameters[1], None + elif opcode == 2: # multiplication + return instruction_pointer + 4, parameters[0] * parameters[1], None + elif opcode == 3: # input + if len(inputs) > 0: + return instruction_pointer + 2, inputs.pop(0), None + else: + # cue context switch + return -1, instruction_pointer, None + elif opcode == 4: # output + return instruction_pointer + 2, None, parameters[0] + elif opcode == 5: # jump-if-true + new_instruction_pointer: int = parameters[1] if parameters[0] != 0 else instruction_pointer + 3 + return new_instruction_pointer, None, None + elif opcode == 6: # jump-if-false + new_instruction_pointer: int = parameters[1] if parameters[0] == 0 else instruction_pointer + 3 + return new_instruction_pointer, None, None + elif opcode == 7: # less-than + value: int = 1 if parameters[0] < parameters[1] else 0 + return instruction_pointer + 4, value, None + elif opcode == 8: # equals + value: int = 1 if parameters[0] == parameters[1] else 0 + return instruction_pointer + 4, value, None + else: + raise RuntimeError(f'Illegal instruction: {opcode} {parameters}') + + +def compute(memory: List[int], inputs: List[int] = ()) -> Tuple[List[int], List[int]]: # memory, output + instruction_pointer: int = 0 + value: int = 2 ** 30 + if memory[-2:] == [value, 99]: # restore from context switch + instruction_pointer = memory[-3] + memory = memory[:-3] + # noinspection PyUnusedLocal + output: Optional[int] = None + outputs: List[int] = [] + while memory[instruction_pointer] != 99: + opcode: int = memory[instruction_pointer] % 100 + modes: List[int] = list(map(lambda m: int(m), reversed(str(memory[instruction_pointer] // 100) + .zfill(maximum_number_of_parameters)))) + raw_parameters: List[int] = memory[instruction_pointer + 1: instruction_pointer + maximum_number_of_parameters] + parameters: List[int] = [-1 for _ in range(maximum_number_of_parameters - 1)] + for i in range(maximum_number_of_parameters - 1): + if modes[i] == 0: # position mode + try: + parameters[i] = memory[raw_parameters[i]] + except IndexError: # might happen for instructions with fewer parameters than the maximum + pass + if modes[i] == 1: # immediate mode + parameters[i] = raw_parameters[i] + instruction_pointer, value, output = execute_instruction(instruction_pointer, opcode, parameters, inputs) + if instruction_pointer == -1: # context switch + memory = memory + [value, 2 ** 30, 99] + elif value is not None: + target: int = memory[instruction_pointer - 1] + memory[target] = value + if output is not None: + outputs.append(output) + return memory, outputs diff --git a/2020/Day10.py b/2020/Day10.py new file mode 100644 index 0000000000000000000000000000000000000000..86bc9794632f635130f1471bbc974339cd8dced5 --- /dev/null +++ b/2020/Day10.py @@ -0,0 +1,90 @@ +from typing import List, Tuple + +from ImportData import import_data + +day: int = 10 + +sample_data: List[str] = [ + '28', + '33', + '18', + '42', + '31', + '14', + '46', + '20', + '48', + '47', + '24', + '23', + '49', + '45', + '19', + '38', + '39', + '11', + '1', + '32', + '25', + '35', + '8', + '17', + '7', + '9', + '4', + '2', + '34', + '10', + '3', +] + +data_structure: type = List[int] + + +def parse_data(data: List[str]) -> data_structure: + jolt_list = list(map(lambda j: int(j), data)) + jolt_list.sort() + jolt_list.insert(0, 0) + jolt_list.append(jolt_list[-1] + 3) + return jolt_list + + +def count_differences(data: List[int]) -> Tuple[int]: + differences: List[int] = [0, 0, 0, 0] + for i in range(len(data) - 1): + difference: int = data[i + 1] - data[i] + if difference == 0 or difference > 3: + raise ValueError(f'Difference between {data[i]} and {data[i + 1]} is not in-range.') + differences[difference] += 1 + return tuple(differences) + + +def part1(data: data_structure) -> int: + differences: Tuple[int] = count_differences(data) + return differences[1] * differences[3] + + +def part2(data: data_structure) -> int: + extended_data: List[int] = data.copy() + too_great = data[-1] + 27 # 3^3 -- clearly too great + extended_data.extend([too_great, too_great, too_great]) + counts: List[int] = [0] * len(data) + bsf_queue: List[int] = [0] + while len(bsf_queue) > 0: + node: int = bsf_queue.pop(0) + print(f'popped: {node}({extended_data[node]})\tqueue length: {len(bsf_queue)}', end='') + counts[node] += 1 + for i in range(1, 4): + if 0 < extended_data[node + i] - extended_data[node] <= 3: + bsf_queue.append(node + i) + print(f'\tadding: {node+i}({extended_data[node+i]})', end='') + print('',flush=True) + return counts[-1] + + +if __name__ == '__main__': + production_ready = True + raw_data = import_data(day) if production_ready else sample_data + parsed_data = parse_data(raw_data) + print(part1(parsed_data)) + print(part2(parsed_data)) diff --git a/2020/Day10a.py b/2020/Day10a.py new file mode 100644 index 0000000000000000000000000000000000000000..12be87a694e7d089756abf9ed00faa3fb6ccfb7f --- /dev/null +++ b/2020/Day10a.py @@ -0,0 +1,36 @@ +from typing import List, Tuple + +import Day10 +from ImportData import import_data + +day: int = Day10.day + +sample_data: List[str] = Day10.sample_data + +data_structure: type = Day10.data_structure + + +def parse_data(data: List[str]) -> data_structure: + return Day10.parse_data(data) + + +def part1(data: data_structure) -> int: + return Day10.part1(data) + + +def part2(data: data_structure) -> int: + ways_to_get_here: List[int] = [0] * len(data) + ways_to_get_here[0] = 1 + for i in range(1, len(data)): + for j in range(1, min(i, 3) + 1): + if 0 < data[i] - data[i - j] <= 3: + ways_to_get_here[i] += ways_to_get_here[i - j] + return ways_to_get_here[-1] + + +if __name__ == '__main__': + production_ready = True + raw_data = import_data(day) if production_ready else sample_data + parsed_data = parse_data(raw_data) + print(part1(parsed_data)) + print(part2(parsed_data)) diff --git a/2020/Day11.py b/2020/Day11.py new file mode 100644 index 0000000000000000000000000000000000000000..67edadee4e1f8b190066b3044d90007a58149704 --- /dev/null +++ b/2020/Day11.py @@ -0,0 +1,233 @@ +from typing import List, Tuple, Callable + +from ImportData import import_data + +day: int = 11 + +# noinspection SpellCheckingInspection +sample_data: List[str] = [ + 'L.LL.LL.LL', + 'LLLLLLL.LL', + 'L.L.L..L..', + 'LLLL.LL.LL', + 'L.LL.LL.LL', + 'L.LLLLL.LL', + '..L.L.....', + 'LLLLLLLLLL', + 'L.LLLLLL.L', + 'L.LLLLL.LL' +] + +data_structure: type = List[List["Position"]] + + +class Position: + def __init__(self, encoding: str): + self.is_floor: bool = encoding == '.' + self.is_occupied: bool = encoding == '#' + self.next_occupied: bool = False + self.has_changed_state: bool = True + + def stage_update(self, neighbors: Tuple["Position"], maximum_allowable_neighbors) -> None: + if not self.is_occupied and all(not neighbor.is_occupied for neighbor in neighbors): + self.next_occupied = True + if self.is_occupied and \ + sum(map(lambda neighbor: 1 if neighbor.is_occupied else 0, neighbors)) >= maximum_allowable_neighbors: + self.next_occupied = False + if self.is_floor: + self.next_occupied = False + + def commit_update(self) -> None: + self.has_changed_state = self.is_occupied != self.next_occupied + self.is_occupied = self.next_occupied + + def __repr__(self) -> str: + if self.is_floor: + return '.' + return '#' if self.is_occupied else 'L' + + +def parse_data(data: List[str]) -> data_structure: + seating_chart: List[List[Position]] = [] + for line in data: + row: List[Position] = list(map(lambda c: Position(c), line)) + seating_chart.append(row) + return seating_chart + + +def check_for_changes(positions: List[List[Position]]) -> bool: + changes_exist: bool = False + for row in positions: + changes_exist |= any(seat.has_changed_state for seat in row) + return changes_exist + + +def count_occupied_seats(positions: List[List[Position]]) -> int: + occupied_seats: int = 0 + for row in positions: + occupied_seats += sum(map(lambda seat: 1 if seat.is_occupied else 0, row)) + return occupied_seats + + +def display_seating_chart(positions: List[List[Position]]) -> None: + for row in positions: + for seat in row: + print(seat, end='') + print() + print() + + +def process_day11(data: data_structure, + neighbor_function: Callable[[data_structure, int, int], Tuple[Position]], + maximum_allowable_neighbors) -> int: + # display_seating_chart(data) + changes_exist = check_for_changes(data) + while changes_exist: + for y in range(len(data)): + for x in range(len(data[0])): + seat = data[y][x] + neighbors: Tuple[Position] = neighbor_function(data, y, x) + seat.stage_update(neighbors, maximum_allowable_neighbors) + for row in data: + for seat in row: + seat.commit_update() + # display_seating_chart(data) + changes_exist = check_for_changes(data) + return count_occupied_seats(data) + + +def find_part1_neighbors(data: List[List[Position]], y: int, x: int) -> Tuple[Position]: + minimum_x: int = 0 + minimum_y: int = 0 + maximum_y = len(data) - 1 + maximum_x = len(data[0]) - 1 + neighbors: List[Position] = [] + if x > minimum_x: + neighbors.append(data[y][x - 1]) + if x < maximum_x: + neighbors.append(data[y][x + 1]) + if y > minimum_y: + neighbors.append(data[y - 1][x]) + if y < maximum_y: + neighbors.append(data[y + 1][x]) + if x > minimum_x and y > minimum_y: + neighbors.append(data[y - 1][x - 1]) + if x > minimum_x and y < maximum_y: + neighbors.append(data[y + 1][x - 1]) + if x < maximum_x and y > minimum_y: + neighbors.append(data[y - 1][x + 1]) + if x < maximum_x and y < maximum_y: + neighbors.append(data[y + 1][x + 1]) + return tuple(neighbors) + + +def part1(data: data_structure) -> int: + return process_day11(data, find_part1_neighbors, 4) + + +def find_part2_neighbors(data: List[List[Position]], y: int, x: int) -> Tuple[Position]: + neighbors: List[Position] = [] + # right + i: int = y + j: int = x + 1 + still_looking: bool = j < len(data[i]) + while still_looking: + if data[i][j].is_floor: + j += 1 + still_looking = j < len(data[i]) + else: + neighbors.append(data[i][j]) + still_looking = False + # left + i = y + j = x - 1 + still_looking = j >= 0 + while still_looking: + if data[i][j].is_floor: + j -= 1 + still_looking = j >= 0 + else: + neighbors.append(data[i][j]) + still_looking = False + # down + i = y + 1 + j = x + still_looking = i < len(data) + while still_looking: + if data[i][j].is_floor: + i += 1 + still_looking = i < len(data) + else: + neighbors.append(data[i][j]) + still_looking = False + # up + i = y - 1 + j = x + still_looking = i >= 0 + while still_looking: + if data[i][j].is_floor: + i -= 1 + still_looking = i >= 0 + else: + neighbors.append(data[i][j]) + still_looking = False + # upper-left + i = y - 1 + j = x - 1 + still_looking = i >= 0 and j >= 0 + while still_looking: + if data[i][j].is_floor: + i -= 1 + j -= 1 + still_looking = i >= 0 and j >= 0 + else: + neighbors.append(data[i][j]) + still_looking = False + # upper-right + i = y - 1 + j = x + 1 + still_looking = i >= 0 and j < len(data[i]) + while still_looking: + if data[i][j].is_floor: + i -= 1 + j += 1 + still_looking = i >= 0 and j < len(data[i]) + else: + neighbors.append(data[i][j]) + still_looking = False + # lower-right + i = y + 1 + j = x + 1 + still_looking = i < len(data) and j < len(data[i]) + while still_looking: + if data[i][j].is_floor: + i += 1 + j += 1 + still_looking = i < len(data) and j < len(data[i]) + else: + neighbors.append(data[i][j]) + still_looking = False + # lower-left + i = y + 1 + j = x - 1 + still_looking = i < len(data) and j >= 0 + while still_looking: + if data[i][j].is_floor: + i += 1 + j -= 1 + still_looking = i < len(data) and j >= 0 + else: + neighbors.append(data[i][j]) + still_looking = False + return tuple(neighbors) + + +def part2(data: data_structure) -> int: + return process_day11(data, find_part2_neighbors, 5) + + +if __name__ == '__main__': + production_ready = True + raw_data = import_data(day) if production_ready else sample_data + print(part1(parse_data(raw_data))) + print(part2(parse_data(raw_data))) diff --git a/2020/Day12.py b/2020/Day12.py new file mode 100644 index 0000000000000000000000000000000000000000..547f71a77041da1b78ebabc70c89c84753a7f241 --- /dev/null +++ b/2020/Day12.py @@ -0,0 +1,113 @@ +from typing import List + +from ImportData import import_data + +day: int = 12 + +sample_data: List[str] = [ + 'F10', + 'N3', + 'F7', + 'R90', + 'F11', +] + +data_structure: type = List[str] + + +def parse_data(data: List[str]) -> data_structure: + return data + + +def modify_instruction(instruction: str, direction: int) -> str: + modified_instruction: str = instruction + if modified_instruction[0] == 'F': + if direction == 0: + modified_instruction = 'N' + modified_instruction[1:] + elif direction == 90: + modified_instruction = 'E' + modified_instruction[1:] + elif direction == 180: + modified_instruction = 'S' + modified_instruction[1:] + elif direction == 270: + modified_instruction = 'W' + modified_instruction[1:] + else: + raise ValueError(f'Unexpected direction: {direction} for instruction {instruction}') + if modified_instruction[0] == 'L': + rotation: int = -int(modified_instruction[1:]) + modified_instruction = 'R' + str(rotation) + if modified_instruction[0] == 'S': + distance: int = -int(modified_instruction[1:]) + modified_instruction = 'N' + str(distance) + if modified_instruction[0] == 'W': + distance: int = -int(modified_instruction[1:]) + modified_instruction = 'E' + str(distance) + return modified_instruction + + +def part1(data: data_structure) -> int: + direction: int = 90 + latitude: int = 0 + longitude: int = 0 + for instruction in data: + reduced_instruction = modify_instruction(instruction, direction) + command: str = reduced_instruction[0] + value: int = int(reduced_instruction[1:]) + if command == 'R': + direction = (direction + value) % 360 + elif command == 'N': + latitude += value + elif command == 'E': + longitude += value + else: + raise RuntimeError(f'Unexpected command: {instruction} reduced to {reduced_instruction}' + f' with command {command} and value {value}') + return abs(longitude) + abs(latitude) + + +def part2(data: data_structure) -> int: + latitude: int = 0 + longitude: int = 0 + waypoint_north_offset: int = 1 + waypoint_east_offset: int = 10 + for instruction in data: + command: str = instruction[0] + value: int = int(instruction[1:]) + if command == 'F': + latitude += waypoint_north_offset * value + longitude += waypoint_east_offset * value + elif command == 'N': + waypoint_north_offset += value + elif command == 'S': + waypoint_north_offset -= value + elif command == 'E': + waypoint_east_offset += value + elif command == 'W': + waypoint_east_offset -= value + elif command == 'R': + if value == 90: + waypoint_north_offset, waypoint_east_offset = -waypoint_east_offset, waypoint_north_offset + elif value == 180: + waypoint_north_offset, waypoint_east_offset = -waypoint_north_offset, -waypoint_east_offset + elif value == 270: + waypoint_north_offset, waypoint_east_offset = waypoint_east_offset, -waypoint_north_offset + else: + raise ValueError(f'Unexpected direction: {value} for instruction {instruction}') + elif command == 'L': + if value == 90: + waypoint_north_offset, waypoint_east_offset = waypoint_east_offset, -waypoint_north_offset + elif value == 180: + waypoint_north_offset, waypoint_east_offset = -waypoint_north_offset, -waypoint_east_offset + elif value == 270: + waypoint_north_offset, waypoint_east_offset = -waypoint_east_offset, waypoint_north_offset + else: + raise ValueError(f'Unexpected direction: {value} for instruction {instruction}') + else: + raise RuntimeError(f'Unexpected command: {instruction} with command {command} and value {value}') + return abs(longitude) + abs(latitude) + + +if __name__ == '__main__': + production_ready = True + raw_data = import_data(day) if production_ready else sample_data + print(part1(parse_data(raw_data))) + print(part2(parse_data(raw_data))) diff --git a/2020/Day13.py b/2020/Day13.py new file mode 100644 index 0000000000000000000000000000000000000000..2c1b0cf10fabb1bb3f363b843d142923f1271500 --- /dev/null +++ b/2020/Day13.py @@ -0,0 +1,60 @@ +from typing import List, Set, Tuple, Union + +from ImportData import import_data + +day: int = 13 + +sample_data: List[str] = [ + '939', + '7,13,x,x,59,x,31,19' +] + +data_structure: type = Tuple[int, List[Union[int, str]]] + + +def parse_data(data: List[str]) -> data_structure: # It sounds like they're setting up something for 'x', so + ready_time: int = int(data[0]) # we're not going to do all the data re-structuring here + busses: List[Union[int, str]] = [] + for bus in data[1].split(','): + busses.append(int(bus) if bus.isnumeric() else bus) + return ready_time, busses + + +def part1(data: data_structure) -> int: + ready_time: int = data[0] + busses: Set[Union[int, str]] = set(data[1]) + if 'x' in busses: + busses.remove('x') + first_bus: int = -1 + delay: int = max(busses) + 1 + for bus in busses: + this_delay: int = bus - (ready_time % bus) + if this_delay < delay: + delay = this_delay + first_bus = bus + return first_bus * delay + + +def fits_constraint(time: int, busses: List[Union[int, str]]) -> bool: + valid: bool = True + for i in range(len(busses)): + if busses[i] != 'x': + valid &= (time + i) % busses[i] == 0 + return valid + + +def part2(data: data_structure) -> int: + busses: List[Union[int, str]] = data[1] + multiplier: int = 1 + time: int = busses[0] * multiplier + while not fits_constraint(time, busses): + multiplier += 1 + time: int = busses[0] * multiplier + return time + + +if __name__ == '__main__': + production_ready = True + raw_data = import_data(day) if production_ready else sample_data + print(part1(parse_data(raw_data))) + print(part2(parse_data(raw_data))) diff --git a/2020/Day13a.py b/2020/Day13a.py new file mode 100644 index 0000000000000000000000000000000000000000..3ebf1fb2ee7863984538707997b035a1b00267cd --- /dev/null +++ b/2020/Day13a.py @@ -0,0 +1,76 @@ +from typing import List, Set, Tuple, Union + +from ImportData import import_data +from Day13 import day, sample_data, data_structure, parse_data, part1, fits_constraint + + +def find_initial_time(period0: Union[int, str], displacement0: int, + period1: Union[int, str], displacement1: int) -> Tuple[int, int]: + if period0 == period1 == 'x': + mutual_period = 0 + time = 0 + elif period0 == 'x': + assert displacement1 >= 0 + mutual_period = period1 + time = period1 - displacement1 + while time <= 0: + time += period1 + elif period1 == 'x': + assert displacement0 >= 0 + mutual_period = period0 + time = period0 - displacement0 + while time <= 0: + time += period0 + elif period0 == 0: + assert displacement1 <= 0 + mutual_period = period1 + time = abs(displacement1) + elif period1 == 0: + assert displacement0 <= 0 + mutual_period = period0 + time = abs(displacement0) + else: + mutual_period = period0 if period0 == period1 else period0 * period1 # mutually prime + time = -displacement0 if period0 > period1 else -displacement1 + period = max(period0, period1) + still_looking: bool = True + while still_looking: + time += period + satisfies_0 = (time + displacement0) % period0 == 0 + satisfies_1 = (time + displacement1) % period1 == 0 + still_looking = time < 0 or not (satisfies_0 and satisfies_1) + return mutual_period, round(time) + + +def divide_and_conquer(busses: List[Union[int, str]], offset: int) -> Tuple[int, int]: + if len(busses) == 1: + print(f'\t\t\tConquering position {offset}') + period, displacement = find_initial_time(busses[0], offset, busses[0], offset) + elif len(busses) == 2: + print(f'\t\t\tConquering positions {offset}-{offset + 1}') + period, displacement = find_initial_time(busses[0], offset, busses[1], offset + 1) + else: + print(f'Solving positions {offset}..{offset + len(busses) - 1}') + halfway = len(busses) // 2 + print(f'\tDividing positions {offset}..{offset + halfway - 1}') + period0, displacement0 = divide_and_conquer(busses[0:halfway], offset) + print(f'\tDividing positions {offset + halfway}..{offset + len(busses) - 1}') + period1, displacement1 = divide_and_conquer(busses[halfway:], offset + halfway) + print(f'\t\tConquering positions {offset}..{offset + len(busses) - 1}') + if len(busses) == 72: + print(f'\t\t\t\tperiod0 = {period0}, displacement0 = {-displacement0}, ' + f'period1 = {period1}, displacement1 = {-displacement1}') + period, displacement = find_initial_time(period0, -displacement0, period1, -displacement1) + return period, displacement + + +def part2(data: data_structure) -> int: + _, time = divide_and_conquer(data[1], 0) + return time + + +if __name__ == '__main__': + production_ready = True + raw_data = import_data(day) if production_ready else sample_data + print(part1(parse_data(raw_data))) + print(part2(parse_data(raw_data))) diff --git a/2020/Day14.py b/2020/Day14.py new file mode 100644 index 0000000000000000000000000000000000000000..182d10b6e1807f7f2b0771ae6599bfb6fffd89e5 --- /dev/null +++ b/2020/Day14.py @@ -0,0 +1,123 @@ +from typing import List, Union, Dict, Set + +from ImportData import import_data + +day: int = 14 + +sample_data1: List[str] = [ + 'mask = XXXXXXXXXXXXXXXXXXXXXXXXXXXXX1XXXX0X', + 'mem[8] = 11', + 'mem[7] = 101', + 'mem[8] = 0' +] + +sample_data2: List[str] = [ + 'mask = 000000000000000000000000000000X1001X', + 'mem[42] = 100', + 'mask = 00000000000000000000000000000000X0XX', + 'mem[26] = 1', +] + +data_structure: type = List[Dict[str, Union[str, int]]] + + +def parse_data(data: List[str]) -> data_structure: + instructions: List[Dict[str, Union[str, int]]] = [] + # noinspection PyUnusedLocal + instruction: Dict[str, Union[str, int]] = {} + for line in data: + halves: List[str] = line.split(' = ') + if halves[0][0:3] == 'mas': + instruction = {'opcode': 'mask', 'mask': halves[1]} + elif halves[0][0:3] == 'mem': + instruction = {'opcode': 'mem', 'address': (int(halves[0][4:-1])), 'value': int(halves[1])} + else: + instruction = {'opcode': 'nop'} + instructions.append(instruction) + return instructions + + +def apply_weird_mask(value: int, mask: str) -> int: + new_value: int = value + word_size: int = len(mask) # should be 36 + for i in range(word_size): + if mask[-i - 1] == '0': + new_value = new_value & ~(1 << i) + if mask[-i - 1] == '1': + new_value = new_value | (1 << i) + return new_value + + +def run_program1(data: data_structure) -> Dict[int, int]: + mask: str = 'X' * 36 + memory: Dict[int, int] = {} # not using a List to avoid index errors + for line in data: + if line['opcode'] == 'mask': + mask = line['mask'] + elif line['opcode'] == 'mem': + memory[line['address']] = apply_weird_mask(line['value'], mask) + else: + raise RuntimeError(f'Unexpected instruction: {line}') + return memory + + +def part1(data: data_structure) -> int: + memory = run_program1(data) + values_sum: int = 0 + for address in memory: + values_sum += memory[address] + return values_sum + + +def get_matching_addresses(address: int, mask: str) -> Set[int]: + wildcard_addresses: Set[str] = set() + new_mask: str = '' + word_size: int = len(mask) # should be 36 + new_address: str = f'{address:036b}' + for i in range(word_size): + if mask[i] == '0': + new_mask = new_mask + new_address[i] + else: + new_mask = new_mask + mask[i] + wildcard_addresses.add(new_mask) + for i in range(word_size): + new_wildcard_addresses: Set[str] = set() + for wildcard_address in wildcard_addresses: + if wildcard_address[i] == 'X': + new_wildcard_addresses.add(wildcard_address[:i] + '0' + wildcard_address[i+1:]) + new_wildcard_addresses.add(wildcard_address[:i] + '1' + wildcard_address[i+1:]) + else: + new_wildcard_addresses.add(wildcard_address) + wildcard_addresses = new_wildcard_addresses + addresses: Set[int] = set(map(lambda a: int(a,2), wildcard_addresses)) + return addresses + + +def run_program2(data: data_structure) -> Dict[int, int]: + word_size = 36 + mask: str = '0' * word_size + memory: Dict[int, int] = {} + for line in data: + if line['opcode'] == 'mask': + mask = line['mask'] + elif line['opcode'] == 'mem': + matching_addresses: Set[int] = get_matching_addresses(line['address'], mask) + for address in matching_addresses: + memory[address] = line['value'] + else: + raise RuntimeError(f'Unexpected instruction: {line}') + return memory + + +def part2(data: data_structure) -> int: + memory = run_program2(data) + values_sum: int = 0 + for address in memory: + values_sum += memory[address] + return values_sum + + +if __name__ == '__main__': + production_ready = True + print(part1(parse_data(import_data(day) if production_ready else sample_data1))) + print(part2(parse_data(import_data(day) if production_ready else sample_data2))) diff --git a/2020/Day15.py b/2020/Day15.py new file mode 100644 index 0000000000000000000000000000000000000000..6ef2df2ab97fdd47d028431f1d2ba3dbbf7fe188 --- /dev/null +++ b/2020/Day15.py @@ -0,0 +1,65 @@ +from typing import List, Dict + +day: int = 15 + +sample_data: List[List[int]] = [ + [0, 3, 6], # 436 + [1, 3, 2], # 1 + [2, 1, 3], # 10 + [1, 2, 3], # 27 + [2, 3, 1], # 78 + [3, 2, 1], # 438 + [3, 1, 2] # 1836 +] + +expected_result1: List[int] = [436, 1, 10, 27, 78, 438, 1836] +expected_result2: List[int] = [175594, 2578, 3544142, 261214, 6895259, 18, 362] + +live_data: List[int] = [10, 16, 6, 0, 1, 17] + +data_structure: type = List[int] + + +def simple_memory_game(data: data_structure, target_entry: int) -> int: + utterances: List[int] = data.copy() + utterances.insert(0, -1) + while len(utterances) <= target_entry: + last_utterance: int = utterances[-1] + current_timestamp: int = max(index for index, value in enumerate(utterances) if value == last_utterance) + previous_timestamp: int = \ + max(index for index, value in enumerate(utterances[:-1]) if value == last_utterance) \ + if utterances.index(last_utterance) < current_timestamp \ + else current_timestamp + next_utterance: int = current_timestamp - previous_timestamp + utterances.append(next_utterance) + return utterances[target_entry] + + +def memory_game(data: data_structure, target_entry: int) -> int: + timestamps: Dict[int, int] = {} # utterance, timestamp + timestamp: int = 0 + for utterance in data: + timestamp += 1 + timestamps[utterance] = timestamp + utterance: int = 0 + last_utterance: int = data[-1] + while timestamp < target_entry: + timestamp += 1 + previous_timestamp: int = timestamps[utterance] if utterance in timestamps else timestamp + timestamps[utterance] = timestamp + last_utterance = utterance + utterance = timestamp - previous_timestamp + return last_utterance + + +if __name__ == '__main__': + production_ready = True + if production_ready: + print(memory_game(live_data, 2020)) + print(memory_game(live_data, 30000000)) + else: + for i in range(len(sample_data)): + print(sample_data[i]) + print(f'\tSimple Part 1: expected {expected_result1[i]}, got {simple_memory_game(sample_data[i], 2020)}') + print(f'\tBetter Part 1: expected {expected_result1[i]}, got {memory_game(sample_data[i], 2020)}') + print(f'\tBetter Part 2: expected {expected_result2[i]}, got {memory_game(sample_data[i], 30000000)}') diff --git a/2020/Day16.py b/2020/Day16.py new file mode 100644 index 0000000000000000000000000000000000000000..9098ae8a477b399e416baa0d03dbf4b04833e95b --- /dev/null +++ b/2020/Day16.py @@ -0,0 +1,165 @@ +from typing import List, Dict, Tuple, Set + +from ImportData import import_data + +day: int = 16 + +sample_data1: List[str] = [ + 'class: 1-3 or 5-7', + 'row: 6-11 or 33-44', + 'seat: 13-40 or 45-50', + '', + 'your ticket:', + '7,1,14', + '', + 'nearby tickets:', + '7,3,47', + '40,4,50', + '55,2,20', + '38,6,12' +] + +sample_data2: List[str] = [ + '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', +] + + +class Ticket: + fields: Dict[str, List[Tuple[int, int]]] = {} # field name, [(lower_bound, upper_bound)] + + def __init__(self, numbers: List[int]): + self.numbers = numbers + + @classmethod + def add_field(cls, field_data: str) -> None: + field_name, range_data = field_data.split(': ') + value_ranges: List[str] = range_data.split(' or ') + ranges: List[Tuple[int, int]] = [] + for value_range in value_ranges: + lower_bound, upper_bound = value_range.split('-') + ranges.append((int(lower_bound), int(upper_bound))) + cls.fields[field_name] = ranges + + def is_valid_number(self, number: int, field: str) -> bool: + number_is_valid: bool = False + if field not in self.fields: + return False + for lower_bound, upper_bound in self.fields[field]: + if lower_bound <= number <= upper_bound: + number_is_valid = True + return number_is_valid + + def is_valid(self) -> bool: + # ticket_is_valid: bool = False + # for number in self.numbers: + # for field in self.fields: + # if self.is_valid_number(number, field): + # ticket_is_valid = True + # return ticket_is_valid + return self.ticket_error_rate() == 0 + + def ticket_error_rate(self) -> int: + error_rate: int = 0 + for number in self.numbers: + number_is_valid: bool = False + for field in self.fields: + if self.is_valid_number(number, field): + number_is_valid = True + if not number_is_valid: + error_rate += number + return error_rate + + @staticmethod + def error_rate(tickets: List["Ticket"]) -> int: + total_error_rate: int = 0 + for ticket in tickets: + total_error_rate += ticket.ticket_error_rate() + return total_error_rate + + @classmethod + def deduce_fields(cls, tickets: List["Ticket"]) -> Dict[str, int]: # field name, numbers index + valid_tickets: List[Ticket] = [] + for ticket in tickets: + if ticket.is_valid(): + valid_tickets.append(ticket) + assert Ticket.error_rate(valid_tickets) == 0 + # we now have a list of valid tickets + field_names: Set[str] = set(cls.fields.keys()) + indices: Set[int] = set(range(len(tickets[0].numbers))) + deduction: Dict[str, Set[int]] = {} + for field in field_names: + deduction[field] = indices.copy() + # we now have a deduction dictionary in which each field maps to all of the possible number indices + # for ticket in valid_tickets: + for ticket in tickets: + if ticket.is_valid(): + for index in indices: + for field in field_names: + if index in deduction[field] and not ticket.is_valid_number(ticket.numbers[index], field): + deduction[field].remove(index) + # some fields in the deductions dictionary should now map to a singleton set + # we'll use those to further reduce the non-singleton sets + conclusion: Dict[str, int] = {} + while len(deduction) > 0: + singletons: Set[str] = {field for field in deduction.keys() if len(deduction[field]) == 1} + if len(singletons) == 0: + raise RuntimeError('Ran out of singletons!') + field: str = singletons.pop() + index: int = deduction.pop(field).pop() + conclusion[field] = index + for field in deduction: + if index in deduction[field]: + deduction[field].remove(index) + assert len(deduction[field]) > 0 + return conclusion + + +data_structure: type = List[Ticket] + + +# noinspection PyUnusedLocal +def parse_data(data: List[str]) -> data_structure: + tickets: List[Ticket] = [] + delimiter: int = data.index('') + for field_data in data[:delimiter]: + Ticket.add_field(field_data) + ticket_numbers: List[int] = list(map(lambda n: int(n), data[delimiter + 2].split(','))) + tickets.append(Ticket(ticket_numbers)) # your ticket + delimiter += 3 + for ticket_data in data[delimiter + 2:]: # nearby tickets + ticket_numbers = list(map(lambda n: int(n), ticket_data.split(','))) + tickets.append(Ticket(ticket_numbers)) + return tickets + + +# noinspection PyUnusedLocal +def part1(data: data_structure) -> int: + return Ticket.error_rate(data) + + +# noinspection PyUnusedLocal +def part2(data: data_structure) -> int: + my_ticket: Ticket = data[0] + field_mapping: Dict[str, int] = Ticket.deduce_fields(data[1:]) + departure_fields = {field for field in my_ticket.fields if field.startswith('departure')} + product: int = 1 + for field in departure_fields: + index: int = field_mapping[field] + product *= my_ticket.numbers[index] + return product + + +if __name__ == '__main__': + production_ready = True + print(part1(parse_data(import_data(day) if production_ready else sample_data1))) + print(part2(parse_data(import_data(day) if production_ready else sample_data2))) diff --git a/2020/Day17.py b/2020/Day17.py new file mode 100644 index 0000000000000000000000000000000000000000..e2758696d1447e2c27793490fd8daafb378f78c2 --- /dev/null +++ b/2020/Day17.py @@ -0,0 +1,273 @@ +from typing import List, Set, FrozenSet, Tuple + +from ImportData import import_data + +day: int = 17 + +sample_data: List[str] = [ + '.#.', + '..#', + '###' +] + +data_structure: type = FrozenSet["Cube"] + + +class Cube: + cubes: Set["Cube"] = set() + + def __init__(self, x: int, y: int, z: int, active: bool = False): + self.w = None + self.x = x + self.y = y + self.z = z + self.active = active + self.next_state = active + + @classmethod + def initialize(cls, starter_data: List[str]) -> FrozenSet["Cube"]: + cls.cubes = set() + z: int = 0 + for y in range(len(starter_data)): + for x in range(len(starter_data[y])): + active: bool = starter_data[y][x] == '#' + cls.cubes.add(Cube(x, 2 - y, z, active)) + return frozenset(cls.cubes) + + @classmethod + def get_cube(cls, x: int, y: int, z: int, w: int = 0) -> "Cube": + matches: Set["Cube"] = {cube for cube in cls.cubes if cube.x == x and cube.y == y and cube.z == z} + if len(matches) == 0: + cube: "Cube" = Cube(x, y, z) + cls.cubes.add(cube) + return cube + elif len(matches) == 1: + return matches.pop() + else: + raise RuntimeError(f'Found {len(matches)} cubes with coordinates ({x}, {y}, {z}).') + + @classmethod + def absent(cls, x: int, y: int, z: int, w: int = 0) -> bool: + matches: Set["Cube"] = {cube for cube in cls.cubes if cube.x == x and cube.y == y and cube.z == z} + return len(matches) == 0 + + @classmethod + def get_bounds(cls) -> Tuple[int, ...]: + minimum_x: int = min({cube.x for cube in cls.cubes}) + maximum_x: int = max({cube.x for cube in cls.cubes}) + minimum_y: int = min({cube.y for cube in cls.cubes}) + maximum_y: int = max({cube.y for cube in cls.cubes}) + minimum_z: int = min({cube.z for cube in cls.cubes}) + maximum_z: int = max({cube.z for cube in cls.cubes}) + return maximum_x, maximum_y, maximum_z, minimum_x, minimum_y, minimum_z + + @classmethod + def expand_space(cls) -> FrozenSet["Cube"]: + maximum_x, maximum_y, maximum_z, minimum_x, minimum_y, minimum_z = cls.get_bounds() + for x in range(minimum_x - 1, maximum_x + 2): + for y in range(minimum_y - 1, maximum_y + 2): + for z in range(minimum_z - 1, maximum_z + 2): + if cls.absent(x, y, z): + cls.cubes.add(Cube(x, y, z)) + return frozenset(cls.cubes) + + def number_of_active_neighbors(self) -> int: + active_neighbors: Set["Cube"] = {cube for cube in self.cubes if + self.x - 1 <= cube.x <= self.x + 1 and + self.y - 1 <= cube.y <= self.y + 1 and + self.z - 1 <= cube.z <= self.z + 1 and + cube.active} + if self in active_neighbors: + active_neighbors.remove(self) + return len(active_neighbors) + + def activate(self) -> None: + self.next_state = True + + def deactivate(self) -> None: + self.next_state = False + + def prep_change(self) -> None: + active_neighbors: int = self.number_of_active_neighbors() + if self.active: + self.next_state = 2 <= active_neighbors <= 3 + else: + self.next_state = active_neighbors == 3 + + @classmethod + def commit_changes(cls) -> FrozenSet["Cube"]: + for cube in cls.cubes: + cube.active = cube.next_state + return frozenset(cls.cubes) + + @classmethod + def print_cubes(cls) -> None: + maximum_x, maximum_y, maximum_z, minimum_x, minimum_y, minimum_z = cls.get_bounds() + for z in range(minimum_z, maximum_z + 1): + print(f'z={z}') + for y in range(maximum_y, minimum_y - 1, -1): + for x in range(minimum_x, maximum_x + 1): + print('#' if cls.get_cube(x, y, z).active else '.', end='') + print() + print() + + def __repr__(self) -> str: + return f'({self.x},{self.y},{self.z}): {self.active}' + + +class Hypercube(Cube): + def __init__(self, x: int, y: int, z: int, w: int, active: bool = False): + super().__init__(x, y, z, active) + self.w = w + + @classmethod + def initialize(cls, starter_data: List[str]) -> FrozenSet[Cube]: + cls.cubes = set() + z: int = 0 + w: int = 0 + for y in range(len(starter_data)): + for x in range(len(starter_data[y])): + active: bool = starter_data[y][x] == '#' + cls.cubes.add(Hypercube(x, 2 - y, z, w, active)) + return frozenset(cls.cubes) + + @classmethod + def get_cube(cls, x: int, y: int, z: int, w: int = 0) -> Cube: + matches: Set[Cube] = {cube for cube in cls.cubes if cube.x == x and cube.y == y and cube.z == z and cube.w == w} + if len(matches) == 0: + cube: "Hypercube" = Hypercube(x, y, z, w) + cls.cubes.add(cube) + return cube + elif len(matches) == 1: + return matches.pop() + else: + raise RuntimeError(f'Found {len(matches)} cubes with coordinates ({x}, {y}, {z}).') + + @classmethod + def absent(cls, x: int, y: int, z: int, w: int = 0) -> bool: + matches: Set["Cube"] = {cube for cube in cls.cubes if + cube.x == x and cube.y == y and cube.z == z and cube.w == w} + return len(matches) == 0 + + @classmethod + def get_bounds(cls) -> Tuple[int, ...]: + maximum_x, maximum_y, maximum_z, minimum_x, minimum_y, minimum_z = super(Hypercube, cls).get_bounds() + minimum_w: int = min({cube.w for cube in cls.cubes}) + maximum_w: int = max({cube.w for cube in cls.cubes}) + return maximum_x, maximum_y, maximum_z, minimum_x, minimum_y, minimum_z, minimum_w, maximum_w + + @classmethod + def expand_space(cls) -> FrozenSet["Cube"]: + maximum_x, maximum_y, maximum_z, minimum_x, minimum_y, minimum_z, minimum_w, maximum_w = cls.get_bounds() + for x in range(minimum_x - 1, maximum_x + 2): + for y in range(minimum_y - 1, maximum_y + 2): + for z in range(minimum_z - 1, maximum_z + 2): + for w in range(minimum_w - 1, maximum_w + 2): + if cls.absent(x, y, z, w): + cls.cubes.add(Hypercube(x, y, z, w)) + return frozenset(cls.cubes) + + def number_of_active_neighbors(self) -> int: + active_neighbors: Set["Cube"] = {cube for cube in self.cubes if + self.x - 1 <= cube.x <= self.x + 1 and + self.y - 1 <= cube.y <= self.y + 1 and + self.z - 1 <= cube.z <= self.z + 1 and + self.w - 1 <= cube.w <= self.w + 1 and + cube.active} + if self in active_neighbors: + active_neighbors.remove(self) + return len(active_neighbors) + + @classmethod + def print_cubes(cls) -> None: + maximum_x, maximum_y, maximum_z, minimum_x, minimum_y, minimum_z, minimum_w, maximum_w = cls.get_bounds() + for w in range(minimum_w, maximum_w + 1): + for z in range(minimum_z, maximum_z + 1): + print(f'z={z}, w={w}') + for y in range(maximum_y, minimum_y - 1, -1): + print(f'{str(abs(y)).rjust(3)} ', end='') + for x in range(minimum_x, maximum_x + 1): + print('#' if cls.get_cube(x, y, z, w).active else '.', end='') + print() + print(' ', end='') + for x in range(minimum_x, maximum_x + 1): + print(abs(x) // 10, end='') + print() + print(' ', end='') + for x in range(minimum_x, maximum_x + 1): + print(abs(x) % 10, end='') + print() + print() + print() + + def __repr__(self) -> str: + return f'({self.x},{self.y},{self.z},{self.w}): {self.active}' + + @classmethod + def remove_inactive_hyperplane(cls, dimension: str, value: int): + trimming: bool = False + hyperplane: Set[Cube] = {cube for cube in cls.cubes if getattr(cube, dimension) == value and cube.active} + if len(hyperplane) == 0: + cls.cubes = cls.cubes - {cube for cube in cls.cubes if getattr(cube, dimension) == value} + trimming = True + return trimming + + @classmethod + def trim_space(cls) -> None: + trimming: bool = True + while trimming: + trimming = False + maximum_x, maximum_y, maximum_z, minimum_x, minimum_y, minimum_z, minimum_w, maximum_w = cls.get_bounds() + trimming |= cls.remove_inactive_hyperplane('x', minimum_x) + trimming |= cls.remove_inactive_hyperplane('x', maximum_x) + trimming |= cls.remove_inactive_hyperplane('y', minimum_y) + trimming |= cls.remove_inactive_hyperplane('y', maximum_y) + trimming |= cls.remove_inactive_hyperplane('z', minimum_z) + trimming |= cls.remove_inactive_hyperplane('z', maximum_z) + trimming |= cls.remove_inactive_hyperplane('w', minimum_w) + trimming |= cls.remove_inactive_hyperplane('w', maximum_w) + + +def parse_data1(data: List[str]) -> data_structure: + return Cube.initialize(data) + + +def parse_data2(data: List[str]) -> data_structure: + return Hypercube.initialize(data) + + +def part1(data: data_structure) -> int: + cubes: FrozenSet[Cube] = data + for cycle in range(6): + # print(f'Cycle: {cycle}') + cubes = Cube.expand_space() + # Cube.print_cubes() + for cube in cubes: + cube.prep_change() + cubes = Cube.commit_changes() + # print('Final:') + # Cube.print_cubes() + return len({cube for cube in cubes if cube.active}) + + +def part2(data: data_structure) -> int: + cubes: FrozenSet[Cube] = data + for cycle in range(6): + print(f'Cycle: {cycle}') + Hypercube.trim_space() + # Hypercube.print_cubes() + cubes = Hypercube.expand_space() + for cube in cubes: + cube.prep_change() + cubes = Hypercube.commit_changes() + print('Final:') + # Hypercube.trim_space() + # Hypercube.print_cubes() + return len({cube for cube in cubes if cube.active}) + + +if __name__ == '__main__': + production_ready = True + raw_data = import_data(day) if production_ready else sample_data + print(part1(parse_data1(raw_data))) + print(part2(parse_data2(raw_data))) diff --git a/2020/Day18.py b/2020/Day18.py new file mode 100644 index 0000000000000000000000000000000000000000..6e0acb721c19d1e693a52decbc573334acf7bc3c --- /dev/null +++ b/2020/Day18.py @@ -0,0 +1,320 @@ +from typing import List, Union, Optional + +from ImportData import import_data + +day: int = 18 + +sample_data: List[str] = [ + '1 + 2 * 3 + 4 * 5 + 6', + '1 + (2 * 3) + (4 * (5 + 6))', + '2 * 3 + (4 * 5)', + '5 + (8 * 3 + 9 + 3 * 4 * 3)', + '5 * 9 * (7 * 3 * 3 + 9 * 3 + (8 + 6 * 4))', + '((2 + 4 * 9) * (6 + 9 * 8 + 6) + 6) + 2 + 4 * 2' +] + +data_structure: type = List["Expression"] + + +# noinspection DuplicatedCode +class Expression: + def __init__(self, left: Union[int, "Expression"], operator: Optional[str] = None, + right: Optional[Union[int, "Expression"]] = None): + self.left: Union[int, "Expression"] = left + self.operator: Optional[str] = operator + self.right: Optional[Union[int, "Expression"]] = right + + @classmethod + def parse1(cls, expression: str) -> Optional["Expression"]: + tokens: List[str] = expression.split() + if len(tokens) == 0: + return None + if len(tokens) == 1: + # the only token should be a number + return Expression(int(tokens[0])) + if tokens[-1].isnumeric(): + right: int = int(tokens[-1]) + # the second-to-last token should be '+' or '*' + operator: str = tokens[-2] + offset: int = len(tokens[-1]) + 1 + len(tokens[-2]) + 1 + # by golly, there'd better be a left-expression + left: str = expression[:-offset] + return Expression(Expression.parse1(left), operator, right) + if tokens[-1][-1] == ')': + parentheses_count: int = 1 + offset: int = 1 + while parentheses_count > 0: + if expression[-offset - 1] == ')': + parentheses_count += 1 + if expression[-offset - 1] == '(': + parentheses_count -= 1 + offset += 1 + right_expression: str = expression[-offset + 1:-1] + offset += 2 + operator: Optional[str] = expression[-offset] if offset < len(expression) else None + offset += 1 + left_expression: str = expression[:-offset] if offset < len(expression) else '' + if left_expression == '': + left_expression, right_expression = right_expression, left_expression + return Expression(Expression.parse1(left_expression), operator, Expression.parse1(right_expression)) + raise RuntimeError(f'Uninterpretable expression: {expression}') + + @staticmethod + def is_balanced(token: str) -> bool: + left_parentheses: int = token.count('(') + right_parentheses: int = token.count(')') + return left_parentheses == right_parentheses + + @classmethod + def parse2(cls, expression: str) -> Optional["Expression"]: + # if expression[0] == '(' and expression[-1] == ')' and expression[1:-1].count('(') == 0: + # return cls.parse2(expression[1:-1]) + tokens: List[str] = expression.split(' * ') + if len(tokens) <= 1: + # only numbers, parentheses and addition + return cls.parse1(expression) + if cls.is_balanced(tokens[0]): + left_expression: str = tokens[0] + operator: str = '*' + right_expression: str = expression[len(left_expression) + 3:] + return Expression(Expression.parse2(left_expression), operator, Expression.parse2(right_expression)) + else: + left_parenthesis: int = -1 + right_parenthesis: int = -1 + parentheses_count: int = 0 + offset: int = 0 + while right_parenthesis < 0: + if expression[offset] == '(': + parentheses_count += 1 + if left_parenthesis < 0: + left_parenthesis = offset + if expression[offset] == ')': + parentheses_count -= 1 + if parentheses_count == 0: + right_parenthesis = offset + offset += 1 + left_expression: str = '' + operator: Optional[str] = None + right_expression: str = '' + if left_parenthesis == 0: + left_expression = expression[left_parenthesis + 1: right_parenthesis] + offset += 1 + if offset < len(expression): + operator = expression[offset] + offset += 2 + if offset < len(expression): + right_expression = expression[offset:] + # compensate for order of operations + offset = 1 + while operator == '+' and offset < len(right_expression): + if right_expression[offset] == '*': + if cls.is_balanced(right_expression[offset + 1:].strip()): + left_expression = '(' + left_expression + ') + ' + right_expression[:offset].strip() + operator = right_expression[offset] + right_expression = right_expression[offset + 1:].strip() + offset += 1 + else: + right_expression = expression[left_parenthesis:] + offset = left_parenthesis - 2 + if offset > 0: + operator = expression[offset] + offset -= 1 + if offset > 0: + left_expression = expression[:offset] + # compensate for order of operations + offset = len(left_expression) - 2 + while operator == '+' and offset > 0: + if left_expression[offset] == '*': + if cls.is_balanced(left_expression[:offset].strip()): + right_expression = left_expression[offset + 1:].strip() + ' + (' + right_expression + ')' + operator = left_expression[offset] + left_expression = left_expression[:offset].strip() + offset -= 1 + return Expression(Expression.parse2(left_expression), operator, Expression.parse2(right_expression)) + + @staticmethod + def trim_unwanted_parentheses(expression: str) -> str: + if expression == '' or expression[0] != '(': + return expression + trimmed_expression: str = expression[1:-1] + safe_to_trim: bool = True + number_of_parentheses: int = 0 + for character in trimmed_expression: + if character == '(': + number_of_parentheses += 1 + if character == ')': + number_of_parentheses -= 1 + if number_of_parentheses < 0: + safe_to_trim = False + return trimmed_expression if safe_to_trim else expression + + @classmethod + def parse3(cls, expression: str) -> Optional["Expression"]: + if '(' not in expression: + tokens: List[str] = expression.split(' * ') + if len(tokens) <= 1: + # only numbers, parentheses and addition + return cls.parse1(expression) + left_expression: str = tokens[0] + operator: str = '*' + right_expression: str = expression[len(left_expression) + 3:] + return Expression(Expression.parse2(left_expression), operator, Expression.parse2(right_expression)) + number_of_parentheses: int = 0 + left_expression: str = '' + operator: str = '' + right_expression: str = expression + building: bool = True + while building: + if right_expression == '': + building = False + else: + character: str = right_expression[0] + right_expression = right_expression[1:] + if character == '(': + number_of_parentheses += 1 + left_expression = left_expression + character + elif character == ')': + number_of_parentheses -= 1 + left_expression = left_expression + character + elif character == '*' and number_of_parentheses == 0: + operator = '*' + left_expression = left_expression.strip() + right_expression = right_expression.strip() + building = False + elif character == '+' and number_of_parentheses == 0 and right_expression.strip()[0] == '(': + operator = '+' + left_expression = left_expression.strip() + right_expression = right_expression.strip() + building = False + elif character == '+' and number_of_parentheses == 0 and '*' not in right_expression: + operator = '+' + left_expression = left_expression.strip() + right_expression = right_expression.strip() + building = False + else: + left_expression = left_expression + character + left_expression = cls.trim_unwanted_parentheses(left_expression) + right_expression = cls.trim_unwanted_parentheses(right_expression) + return Expression(Expression.parse3(left_expression), operator, Expression.parse3(right_expression)) + + @classmethod + def parse4(cls, expression: str) -> Optional["Expression"]: + expression = expression.strip() + if '(' not in expression: + tokens: List[str] = expression.split(' * ') + if len(tokens) <= 1: + # only numbers, parentheses and addition + return cls.parse1(expression) + left_expression: str = tokens[0] + operator: str = '*' + right_expression: str = expression[len(left_expression) + 3:] + return Expression(Expression.parse2(left_expression), operator, Expression.parse2(right_expression)) + left_parenthesis: int = -1 + right_parenthesis: int = -1 + number_of_parentheses: int = 0 + offset: int = 0 + left_expression: str = '' + left_operator: str = '' + right_operator: str = '' + right_expression: str = '' + while right_parenthesis < 0: + if expression[offset] == '(': + number_of_parentheses += 1 + if left_parenthesis < 0: + left_parenthesis = offset + if expression[offset] == ')': + number_of_parentheses -= 1 + if number_of_parentheses == 0: + right_parenthesis = offset + offset += 1 + if left_parenthesis > 0: + left_operator = expression[left_parenthesis - 2] + left_expression = expression[:left_parenthesis - 3] + if right_parenthesis < len(expression) - 1: + right_operator = expression[right_parenthesis + 2] + right_expression = expression[right_parenthesis + 4:] + parenthetical_expression: str = expression[left_parenthesis + 1:right_parenthesis] + middle: Expression = cls.parse4(parenthetical_expression) + if left_operator == '*': + left: Expression = cls.parse4(left_expression) + middle = Expression(left, left_operator, middle) + elif left_operator == '+': + if '*' in left_expression: + # there are no parentheses in left_expression + offset = -1 + while left_expression[offset] != '*': + offset -= 1 + left: Expression = cls.parse4(left_expression[:offset - 1]) + middle = Expression(cls.parse4(left_expression[offset + 2:]), left_operator, middle) + middle = Expression(left, '*', middle) + else: + left: Expression = cls.parse4(left_expression) + middle = Expression(left, left_operator, middle) + if right_operator == '*': + right: Expression = cls.parse4(right_expression) + middle = Expression(middle, right_operator, right) + elif right_operator == '+': + if '*' in right_expression: + # if the multiplication occurs within another parenthetical expression, we can ignore it for now + offset = 0 + number_of_parentheses = 0 + multiplication: int = -1 + while multiplication < 0 and offset < len(right_expression): + if right_expression[offset] == '(': + number_of_parentheses += 1 + if right_expression[offset] == ')': + number_of_parentheses -= 1 + if right_expression[offset] == '*' and number_of_parentheses == 0: + multiplication = offset + offset += 1 + if multiplication < 0: + right: Expression = cls.parse4(right_expression) + middle = Expression(middle, right_operator, right) + else: + right: Expression = cls.parse4(right_expression[offset + 1:]) + middle = Expression(middle, right_operator, cls.parse4(right_expression[:offset - 1])) + middle = Expression(middle, '*', right) + else: + right: Expression = cls.parse4(right_expression) + middle = Expression(middle, right_operator, right) + return middle + + def evaluate(self) -> int: + if self.operator is None and self.right is None: + if type(self.left) is int: + return self.left + else: + return self.left.evaluate() + if self.operator is not None and self.right is not None: + evaluated_left: int = self.left if type(self.left) is int else self.left.evaluate() + evaluated_right: int = self.right if type(self.right) is int else self.right.evaluate() + if self.operator == '+': + return evaluated_left + evaluated_right + elif self.operator == '*': + return evaluated_left * evaluated_right + else: + raise RuntimeError(f'Unexpected operator: {self.operator} for {self}') + raise RuntimeError(f'Unexpected expression: {self}') + + def __repr__(self) -> str: + return f'<{self.left}>' if self.operator is None and self.right is None \ + else f'<{self.left} {self.operator} {self.right}>' + + +def parse_data(data: List[str]) -> data_structure: + return [Expression.parse1(expression) for expression in data] + + +def part1(data: data_structure) -> int: + return sum([expression.evaluate() for expression in data]) + + +def part2(data: data_structure) -> int: + return sum([expression.evaluate() for expression in data]) + + +if __name__ == '__main__': + production_ready = True + raw_data = import_data(day) if production_ready else sample_data + print(part1([Expression.parse1(expression) for expression in raw_data])) + print(part2([Expression.parse4(expression) for expression in raw_data])) diff --git a/2020/Day18cheat.py b/2020/Day18cheat.py new file mode 100644 index 0000000000000000000000000000000000000000..fd1638399039931a67cb0d1b857bd711a7b94400 --- /dev/null +++ b/2020/Day18cheat.py @@ -0,0 +1,90 @@ +from ImportData import import_data + + +# noinspection PyShadowingNames +def evaluate(expression, operators, operations): + operation_stack = list() + number_stack = list() + + expression_list = expression[:-1].split(" ") if expression[-1] == '\n' else expression.split(" ") + + for string in expression_list: + if string in operators.keys(): + while len(operation_stack) > 0 and operators[string] <= operators[operation_stack[-1]]: + operation = operation_stack.pop() + num1 = number_stack.pop() + num2 = number_stack.pop() + + result = operations[operation](num1, num2) + number_stack.append(result) + + operation_stack.append(string) + elif string[0] == '(': + while string[0] == '(': + operation_stack.append('(') + string = string[1:] + number_stack.append(int(string)) + + elif string[-1] == ')': + number_stack.append(int(string[:string.index(')')])) + + while string[-1] == ')': + string = string[:-1] + operation = ')' + + while operation != '(': + operation = operation_stack.pop() + + if operation == '(': + break + + num1 = number_stack.pop() + num2 = number_stack.pop() + + result = operations[operation](num1, num2) + number_stack.append(result) + else: + number_stack.append(int(string)) + + while len(operation_stack) > 0: + operation = operation_stack.pop() + num1 = number_stack.pop() + num2 = number_stack.pop() + + result = operations[operation](num1, num2) + + number_stack.append(result) + + return number_stack[0] + + +operations = { + '+': lambda num1, num2: num1 + num2, + '*': lambda num1, num2: num1 * num2 +} + +# For part 1 +operators_part_1 = { + '+': 1, + '*': 1, + '(': 0 +} + +# For part 2 +operators_part_2 = { + '+': 2, + '*': 1, + '(': 0 +} + +if __name__ == '__main__': + expressions = import_data(18) + + sum_part_1 = 0 + sum_part_2 = 0 + for expression in expressions: + sum_part_1 += evaluate(expression, operators_part_1, operations) + sum_part_2 += evaluate(expression, operators_part_2, operations) + + print("Part 1 sum {}".format(sum_part_1)) + print("Part 2 sum {}".format(sum_part_2)) diff --git a/2020/Day19.py b/2020/Day19.py new file mode 100644 index 0000000000000000000000000000000000000000..a9c72ad098bfdb59d3a14c74aa6c1d07d7334968 --- /dev/null +++ b/2020/Day19.py @@ -0,0 +1,107 @@ +from typing import List, Tuple, Set, Union, Dict + +from ImportData import import_data + +day: int = 19 + +# noinspection SpellCheckingInspection +sample_data: List[str] = [ + '0: 4 1 5', + '1: 2 3 | 3 2', + '2: 4 4 | 5 5', + '3: 4 5 | 5 4', + '4: "a"', + '5: "b"', + '', + 'ababbb', + 'bababa', + 'abbbab', + 'aaabbb', + 'aaaabbb' +] + +data_structure: type = Tuple[List[Union[str, Set[Tuple[int, ...]]]], List[str]] + + +# rules: List[Union[str, Set[List[int]]]] +# As a string, a simple rule (e.g., 'a') +# As a Set[List[int]], one or more sequences of rule numbers +# messages: List[str] +# Self-explanatory + + +def parse_data(data: List[str]) -> data_structure: + delimiter: int = data.index('') + numbered_rules_list: List[str] = data[:delimiter] + messages: List[str] = data[delimiter + 1:] + rules_only_list: List[str] = [''] * len(numbered_rules_list) + for numbered_rule in numbered_rules_list: + number, rule = numbered_rule.split(': ') + rules_only_list[int(number)] = rule + rules: List[Union[str, Set[Tuple[int, ...]]]] = [] + for rule in rules_only_list: + if rule[0] == '"': + rules.append(rule[1]) + else: + options: List[str] = rule.split(' | ') + rule_set: Set[Tuple[int, ...]] = set() + for option in options: + rule_set.add(tuple([int(x) for x in option.split()])) + rules.append(rule_set) + return rules, messages + + +known_matches: Dict[str, Set[int]] = {} + + +def find_partial_match(message: str, rules: List[Union[str, Set[Tuple[int, ...]]]], + rule_number: int) -> Tuple[str, str]: + prefix: str = message + suffix: str = '' + found_a_partial_match: bool = False + while prefix != '' and not found_a_partial_match: + if matches(prefix, rules, rule_number): + found_a_partial_match = True + else: + suffix = prefix[-1] + suffix + prefix = prefix[:-1] + return prefix, suffix + + +def matches(message: str, rules: List[Union[str, Set[Tuple[int, ...]]]], rule_number: int) -> bool: + if type(rules[rule_number]) == str: + return message == rules[rule_number] + if message in known_matches and rule_number in known_matches[message]: + return True + found_a_match: bool = False + for rule in rules[rule_number]: + prefix: str = '' + suffix: str = message + for sub_rule in rule: + partial_prefix, suffix = find_partial_match(suffix, rules, sub_rule) + prefix = prefix + partial_prefix + if prefix == message and suffix == '': + found_a_match = True + if found_a_match: + if message in known_matches: + known_matches[message].add(rule_number) + else: + known_matches[message] = {rule_number} + return found_a_match + + +def part1(data: data_structure) -> int: + rules: List[Union[str, Set[List[int]]]] = data[0] + messages: List[str] = data[1] + return sum(map(lambda message: 1 if matches(message, rules, 0) else 0, messages)) + + +def part2(data: data_structure) -> int: + return -1 + + +if __name__ == '__main__': + production_ready = True + raw_data = import_data(day) if production_ready else sample_data + print(part1(parse_data(raw_data))) + print(part2(parse_data(raw_data))) diff --git a/2020/Day20.py b/2020/Day20.py new file mode 100644 index 0000000000000000000000000000000000000000..70d87a185a560fe2190d195719b011beafbc7376 --- /dev/null +++ b/2020/Day20.py @@ -0,0 +1,184 @@ +from functools import reduce +from typing import List, Union, Optional + +from ImportData import import_data, read_file + +day: int = 20 + +data_structure: type = List["Tile"] + + +class Tile: + def __init__(self, data: List[str]): + self.number = int(data[0][5:-1]) + self.pixels = data[1:] + # each border either links to the neighboring tile or holds a copy of the border string + self.borders: List[Union["Tile", str]] = [ + self.pixels[0], # top border + self.pixels[-1], # bottom border + ''.join(row[0] for row in self.pixels), # left border + ''.join(row[-1] for row in self.pixels) # right border + ] + self.right: Optional["Tile"] = None + self.down: Optional["Tile"] = None + self.number_of_neighbors = 0 + + def attach(self, other: "Tile") -> bool: + attached: bool = False + # print(f'Matching {self.number}...') + for i in range(len(self.borders)): + if type(self.borders[i]) == str and not attached: + # print(f'\t...maybe to {other.number}', end='') + attached = attached or self._try_to_attach(i, other) + return attached + + def _try_to_attach(self, my_border_index: int, other: "Tile") -> bool: + attached: bool = False + border: str = self.borders[my_border_index] + reversed_border: str = border[::-1] + your_border_index: int = 0 + while your_border_index < 4 and not attached and type(other.borders[your_border_index] == str): + if border == other.borders[your_border_index]: + attached = True + self.borders[my_border_index] = other + self.number_of_neighbors += 1 + other.borders[your_border_index] = self + other.number_of_neighbors += 1 + # print(f'\tMatched {self.number} to {other.number}') + elif reversed_border == other.borders[your_border_index]: + attached = True + self.borders[my_border_index] = other + self.number_of_neighbors += 1 + other.borders[your_border_index] = self + other.number_of_neighbors += 1 + # print(f'\tMatched {self.number} to {other.number}') + # else: + # print('\tno', end='') + your_border_index += 1 + print() + return attached + + def flip_diagonal(self) -> None: + new_pixels: List[str] = [''] * len(self.pixels[0]) + for index in range(len(new_pixels)): + new_pixels[index] = ''.join(row[index] for row in self.pixels) + self.pixels = new_pixels + + def flip_vertical(self) -> None: + new_pixels: List[str] = [''] * len(self.pixels[0]) + for index in range(len(new_pixels)): + new_pixels[index] = self.pixels[index][::-1] + self.pixels = new_pixels + + def flip_horizontal(self) -> None: + new_pixels: List[str] = [''] * len(self.pixels[0]) + height = len(new_pixels) + for index in range(height): + new_pixels[index] = self.pixels[height - index - 1] + self.pixels = new_pixels + + def rotate_clockwise(self)-> None: + self.flip_diagonal() + self.flip_vertical() + + def rotate_counterclockwise(self) -> None: + self.flip_diagonal() + self.flip_horizontal() + + def __repr__(self) -> str: + return f'{self.number}: ' # \ + # f'{self.borders[0].number if type(self.borders[0]) != str else None} ' \ + # f'{self.borders[1].number if type(self.borders[0]) != str else None} ' \ + # f'{self.borders[2].number if type(self.borders[0]) != str else None} ' \ + # f'{self.borders[3].number if type(self.borders[0]) != str else None}' + + +def parse_data(data: List[str]) -> data_structure: + tiles: List[Tile] = [] + for tile_index in range(0, len(data), 12): + tiles.append(Tile(data[tile_index:tile_index + 11])) + return tiles + + +def part1(tiles: data_structure) -> int: + for tile in tiles: + if tile.number_of_neighbors < 4: + for other_tile in tiles: + if other_tile != tile and other_tile.number_of_neighbors < 4: + tile.attach(other_tile) + assert [tile.number for tile in tiles if tile.number_of_neighbors == 0] == [] + assert [tile.number for tile in tiles if tile.number_of_neighbors == 1] == [] + assert len([tile.number for tile in tiles if tile.number_of_neighbors == 2]) == 4 + corner_numbers: List[int] = [tile.number for tile in tiles if tile.number_of_neighbors == 2] + product: int = reduce(lambda i, j: i * j, corner_numbers) + return product + + +def form_composite(tiles: data_structure) -> List[str]: + # First, grab a corner + upper_left: Tile = [tile for tile in tiles if tile.number_of_neighbors == 2][0] + # Assign orientation + neighbors: List[Tile] = [tile for tile in upper_left.borders if type(tile) == Tile] + upper_left.down = neighbors[0] + upper_left.right = neighbors[1] + # rotate & flip tiles + tile: Tile = upper_left + below_tile: Optional[Tile] = upper_left.down + above_tile: Optional[Tile] = None + next_row_left: Optional[Tile] = below_tile + right_tile: Optional[Tile] = tile.right + # noinspection PyUnusedLocal + next_right_tile: Optional[Tile] = None + index: int = right_tile.borders.index(tile) + if index == 2: # left + next_right_tile = right_tile.borders[3] if type(right_tile.borders[3]) == Tile else None + elif index == 3: # right + next_right_tile = right_tile.borders[2] if type(right_tile.borders[3]) == Tile else None + right_tile.flip_vertical() + elif index == 0: # above + next_right_tile = right_tile.borders[1] if type(right_tile.borders[3]) == Tile else None + right_tile.rotate_counterclockwise() + elif index == 1: # below + next_right_tile = right_tile.borders[0] if type(right_tile.borders[3]) == Tile else None + right_tile.rotate_clockwise() + else: + raise IndexError('Weird geometry is at play.') + + + # # build the composite + composite: List[str] = [] + + # neighbors: List[int] = [upper_left.borders.index(tile) for tile in upper_left.borders if type(tile) == Tile] + # down: int = neighbors[0] + # right: int = neighbors[1] + # # build the composite + # composite: List[str] = [] + # left_tile: Union[Tile, str] = upper_left + # while type(left_tile == Tile): + # for i in range(1, len(left_tile.pixels) - 1): + # tile: Union[Tile, str] = left_tile + # line: str = '' + # while type(tile) == Tile: + # line = line + tile.pixels[i][1:len(tile.pixels[i]) - 1] + # tile = tile.borders[right] + # composite.append(line) + # print(line) + # left_tile = left_tile.borders[down] + return composite + + +def part2(tiles: data_structure) -> int: + for tile in tiles: + if tile.number_of_neighbors < 4: + for other_tile in tiles: + if other_tile != tile and other_tile.number_of_neighbors < 4: + tile.attach(other_tile) + composite = form_composite(tiles) + return -1 + + +if __name__ == '__main__': + production_ready = False + raw_data = import_data(day) if production_ready else read_file('Day20SampleData.txt') + print(part1(parse_data(raw_data))) + print(part2(parse_data(raw_data))) diff --git a/2020/Day20SampleData.txt b/2020/Day20SampleData.txt new file mode 100644 index 0000000000000000000000000000000000000000..b07aa4badc82ab20bda9d40b735d8f3d2bd5250a --- /dev/null +++ b/2020/Day20SampleData.txt @@ -0,0 +1,107 @@ +Tile 2311: +..##.#..#. +##..#..... +#...##..#. +####.#...# +##.##.###. +##...#.### +.#.#.#..## +..#....#.. +###...#.#. +..###..### + +Tile 1951: +#.##...##. +#.####...# +.....#..## +#...###### +.##.#....# +.###.##### +###.##.##. +.###....#. +..#.#..#.# +#...##.#.. + +Tile 1171: +####...##. +#..##.#..# +##.#..#.#. +.###.####. +..###.#### +.##....##. +.#...####. +#.##.####. +####..#... +.....##... + +Tile 1427: +###.##.#.. +.#..#.##.. +.#.##.#..# +#.#.#.##.# +....#...## +...##..##. +...#.##### +.#.####.#. +..#..###.# +..##.#..#. + +Tile 1489: +##.#.#.... +..##...#.. +.##..##... +..#...#... +#####...#. +#..#.#.#.# +...#.#.#.. +##.#...##. +..##.##.## +###.##.#.. + +Tile 2473: +#....####. +#..#.##... +#.##..#... +######.#.# +.#...#.#.# +.######### +.###.#..#. +########.# +##...##.#. +..###.#.#. + +Tile 2971: +..#.#....# +#...###... +#.#.###... +##.##..#.. +.#####..## +.#..####.# +#..#.#..#. +..####.### +..#.#.###. +...#.#.#.# + +Tile 2729: +...#.#.#.# +####.#.... +..#.#..... +....#..#.# +.##..##.#. +.#.####... +####.#.#.. +##.####... +##..#.##.. +#.##...##. + +Tile 3079: +#.#.#####. +.#..###### +..#....... +######.... +####.#..#. +.#...#.##. +#.#####.## +..#.###... +..#....... +..#.###... diff --git a/2020/Day4.py b/2020/Day4.py new file mode 100644 index 0000000000000000000000000000000000000000..1e5f2ef8eb9a81affa9733e9635d477d2cdc6670 --- /dev/null +++ b/2020/Day4.py @@ -0,0 +1,95 @@ +import string +from typing import List, Dict + +from ImportData import import_data + + +def parse_documents(lines: List[str]) -> List[Dict[str, str]]: + records: List[Dict[str, str]] = [] + record = {'byr': '', 'iyr': '', 'eyr': '', 'hgt': '', 'hcl': '', 'ecl': '', 'pid': '', 'cid': ''} + for line in lines: + if line == '': + records.append(record) + record = {'byr': '', 'iyr': '', 'eyr': '', 'hgt': '', 'hcl': '', 'ecl': '', 'pid': '', 'cid': ''} + else: + for field in line.split(): + pair = field.split(':') + record[pair[0]] = pair[1] + records.append(record) + return records + + +def part1(records: List[Dict[str, str]]) -> int: + valid_records = 0 + for record in records: + invalid_fields = 0 + for field in record: + if record[field] == '': + invalid_fields += 1 + if invalid_fields == 0: + valid_records += 1 + elif invalid_fields == 1 and record['cid'] == '': + valid_records += 1 + else: + pass + return valid_records + + +def validate_year(year: str, lower_bound: int, upper_bound: int) -> bool: + if year == '': + return False + if not year.isnumeric(): + return False + return lower_bound <= int(year) <= upper_bound + + +def validate_height(height: str) -> bool: + if len(height) < 4: + return False + if not height[:-2].isnumeric(): + return False + magnitude = int(height[:-2]) + if height[-2:] == 'cm': + return 150 <= magnitude <= 193 + elif height[-2:] == 'in': + return 59 <= magnitude <= 76 + else: + return False + + +def validate_hair_color(hair_color: str) -> bool: + if len(hair_color) != 7: + return False + if hair_color[0] != '#': + return False + return all(color in string.hexdigits for color in hair_color[1:]) + + +def part2(records: List[Dict[str, str]]) -> int: + valid_records = 0 + for record in records: + is_valid = True + birth_year = record['byr'] + issue_year = record['iyr'] + expire_year = record['eyr'] + height = record['hgt'] + hair_color = record['hcl'] + eye_color = record['ecl'] + passport_id = record['pid'] + is_valid &= validate_year(birth_year, 1920, 2002) + is_valid &= validate_year(issue_year, 2010, 2020) + is_valid &= validate_year(expire_year, 2020, 2030) + is_valid &= validate_height(height) + is_valid &= validate_hair_color(hair_color) + is_valid &= eye_color in {'amb', 'blu', 'brn', 'gry', 'grn', 'hzl', 'oth'} + is_valid &= len(passport_id) == 9 and passport_id.isnumeric() + if is_valid: + valid_records += 1 + return valid_records + + +if __name__ == '__main__': + data = import_data(4) + documents = parse_documents(data) + print(part1(documents)) + print(part2(documents)) diff --git a/2020/Day5.py b/2020/Day5.py new file mode 100644 index 0000000000000000000000000000000000000000..12b466bd6b32247883f849acc690d4e549bd2468 --- /dev/null +++ b/2020/Day5.py @@ -0,0 +1,49 @@ +from typing import List + +from ImportData import import_data + + +def decode_seat(seat: str) -> int: + weight = 1 + value = 0 + for character in reversed(seat): + if character in {'F', 'L'}: + value += 0 + elif character in {'B', 'R'}: + value += weight + else: + raise ValueError(f'{character} is not a valid encoding') + weight *= 2 + return value + + +def part1(seats: List[str]) -> int: + maximum_seat_number = -1 + for seat in seats: + seat_number = decode_seat(seat) + maximum_seat_number = seat_number if seat_number > maximum_seat_number else maximum_seat_number + return maximum_seat_number + + +def part2(seats: List[str]) -> int: + missing_seats: List[int] = [] + filled_seats: List[int] = list(map(decode_seat, seats)) + my_seat: List[int] = [] + for seat in range(max(filled_seats)): + if seat not in filled_seats: + missing_seats.append(seat) + print(missing_seats) + for seat in missing_seats: + if seat - 1 in filled_seats and seat + 1 in filled_seats: + my_seat.append(seat) + if len(my_seat) != 1: + print(my_seat) + return -1 + else: + return my_seat[0] + + +if __name__ == '__main__': + data = import_data(5) + print(part1(data)) + print(part2(data)) diff --git a/2020/Day6.py b/2020/Day6.py new file mode 100644 index 0000000000000000000000000000000000000000..3886d0eabe7ab1dd413c58e765d60b48f130cd3e --- /dev/null +++ b/2020/Day6.py @@ -0,0 +1,53 @@ +import string +from typing import List, Dict + +from ImportData import import_data + + +def parse_answers(lines: List[str]) -> List[Dict[str, int]]: + answer_counts: List[Dict[str, int]] = [] + answer_count = {'group_size': 0} + for letter in string.ascii_lowercase: + answer_count[letter] = 0 + for line in lines: + if line == '': + answer_counts.append(answer_count) + answer_count = {'group_size': 0} + for letter in string.ascii_lowercase: + answer_count[letter] = 0 + else: + answer_count['group_size'] += 1 + for letter in line: + answer_count[letter] += 1 + answer_counts.append(answer_count) + return answer_counts + + +def part1(responses: List[Dict[str, int]]) -> int: + total = 0 + for response in responses: + count = 0 + for letter in string.ascii_lowercase: + if response[letter] > 0: + count += 1 + total += count + return total + + +def part2(responses: List[Dict[str, int]]) -> int: + total = 0 + for response in responses: + count = 0 + for letter in string.ascii_lowercase: + if response[letter] == response['group_size']: + count += 1 + total += count + return total + + +if __name__ == '__main__': + data = import_data(6) + # data = ['abc', '', 'a', 'b', 'c', '', 'ab', 'ac', '', 'a', 'a', 'a', 'a', '', 'b'] + answers = parse_answers(data) + print(part1(answers)) + print(part2(answers)) diff --git a/2020/Day7.py b/2020/Day7.py new file mode 100644 index 0000000000000000000000000000000000000000..95c607cce16fdbaa2d78710ae9f782d620d1d9ee --- /dev/null +++ b/2020/Day7.py @@ -0,0 +1,113 @@ +from typing import Dict, List, Set + +from ImportData import import_data + +day = 7 + +# sample_data = [ +# 'light red bags contain 1 bright white bag, 2 muted yellow bags.', +# 'dark orange bags contain 3 bright white bags, 4 muted yellow bags.', +# 'bright white bags contain 1 shiny gold bag.', +# 'muted yellow bags contain 2 shiny gold bags, 9 faded blue bags.', +# 'shiny gold bags contain 1 dark olive bag, 2 vibrant plum bags.', +# 'dark olive bags contain 3 faded blue bags, 4 dotted black bags.', +# 'vibrant plum bags contain 5 faded blue bags, 6 dotted black bags.', +# 'faded blue bags contain no other bags.', +# 'dotted black bags contain no other bags.' +# ] + +sample_data = [ + 'shiny gold bags contain 2 dark red bags.', + 'dark red bags contain 2 dark orange bags.', + 'dark orange bags contain 2 dark yellow bags.', + 'dark yellow bags contain 2 dark green bags.', + 'dark green bags contain 2 dark blue bags.', + 'dark blue bags contain 2 dark violet bags.', + 'dark violet bags contain no other bags.' +] + + +def parse_data(data: List[str]) -> Dict[str, Dict[str, int]]: + """ + Turns the raw input into a usable data structure + :param data: raw input + :return: outer_color : (inner_color : quantity) + """ + bags: Dict[str, Dict[str, int]] = {} + for line in data: + relationship = line.split(' bags contain ') + outer_color = relationship[0] + contents = relationship[1] + if outer_color in bags: + raise RuntimeError(f'Encountered a second rule for {outer_color}.') + bags[outer_color] = {} + if contents != 'no other bags.': + for content in contents.split(' bag'): + if len(content) > 2: # end of the rule, really should only be '.' or 's.' + if content[0].isnumeric(): # first inner bag + inner_bag = content + elif content[1].isnumeric(): + raise RuntimeError(f'This shouldn\'t be possible: {content}.') + elif content[2].isnumeric(): # previous quantity was 1 + inner_bag = content[2:] + elif content[3].isnumeric(): # previous quantity was greater than 1 + inner_bag = content[3:] + else: + raise RuntimeError(f'You still aren\'t parsing it correctly: {content}') + # my data appears to be single-digit quantities; that simplifies matters + quantity = int(inner_bag[0]) + inner_color = inner_bag[2:] + if inner_color in bags[outer_color]: + raise RuntimeError(f'Encountered a second quantity for {inner_color}.') + if inner_color[0].isnumeric(): + raise RuntimeError( + f'The assumption of single-digit quantities is unfounded for {outer_color}:{inner_color}') + bags[outer_color][inner_color] = quantity + return bags + + +def reverse_map(data: Dict[str, Dict[str, int]]) -> Dict[str, Set[str]]: + reverse_mapped_data: Dict[str, Set[str]] = {} + for outer_color in data: + for inner_color in data[outer_color]: + if inner_color not in reverse_mapped_data: + reverse_mapped_data[inner_color] = set() + reverse_mapped_data[inner_color].add(outer_color) + return reverse_mapped_data + + +def part1(data: Dict[str, Dict[str, int]]) -> int: + wrappers: Dict[str, Set[str]] = reverse_map(data) + possible_containers: Set[str] = set() + colors_to_check: Set[str] = set() + color: str = 'shiny gold' + if color in wrappers: + colors_to_check.update(wrappers[color] - possible_containers) + possible_containers.update(wrappers[color]) + while len(colors_to_check) > 0: + color = colors_to_check.pop() + if color in wrappers: + colors_to_check.update(wrappers[color] - possible_containers) + possible_containers.update(wrappers[color]) + return len(possible_containers) + + +def count_bags(colors_to_check: Dict[str, int], bag_mapping: Dict[str, Dict[str, int]]) -> int: + count = 1 + for color in colors_to_check: + next_level: Dict[str, int] = bag_mapping[color] + multiplier: int = colors_to_check[color] + count += multiplier * count_bags(next_level, bag_mapping) + return count + + +def part2(data: Dict[str, Dict[str, int]]) -> int: + return count_bags({'shiny gold': 1}, data) - 2 + + +if __name__ == '__main__': + production_ready = True + raw_data = import_data(day) if production_ready else sample_data + parsed_data = parse_data(raw_data) + print(part1(parsed_data)) + print(part2(parsed_data)) diff --git a/2020/Day8.py b/2020/Day8.py new file mode 100644 index 0000000000000000000000000000000000000000..ccae3cde18f46f464c97d7eaf2e631147e05a265 --- /dev/null +++ b/2020/Day8.py @@ -0,0 +1,88 @@ +from typing import List, Set, Tuple + +from ImportData import import_data + +day = 8 + +sample_data = [ + 'nop +0', + 'acc +1', + 'jmp +4', + 'acc +3', + 'jmp -3', + 'acc -99', + 'acc +1', + 'jmp -4', + 'acc +6' +] + + +def parse_data(data: List[str]) -> List[str]: + return data + + +def parse_instruction(instruction: str) -> Tuple[str, int]: + fields: List[str] = instruction.split() + opcode: str = fields[0] + operand: int = int(fields[1]) + return opcode, operand + + +def execute_instruction(opcode: str, operand: int, program_counter: int, accumulator: int) -> Tuple[int, int]: + if opcode == 'nop': + program_counter += 1 + elif opcode == 'acc': + accumulator += operand + program_counter += 1 + elif opcode == 'jmp': + program_counter += operand + else: + raise RuntimeError(f'Illegal opcode: {opcode}') + return program_counter, accumulator + + +def part1(program: List[str]) -> int: + accumulator: int = 0 + program_counter: int = 0 + history: Set[int] = set() + while program_counter not in history: + opcode, operand = parse_instruction(program[program_counter]) + history.add(program_counter) + program_counter, accumulator = execute_instruction(opcode, operand, program_counter, accumulator) + return accumulator + + +def mutate_opcode(opcode: str) -> str: + if opcode == 'nop': + return 'jmp' + if opcode == 'jmp': + return 'nop' + return opcode + + +def part2(program: List[str]) -> int: + accumulator: int = 0 + mutation_position: int = -1 + successful: bool = False + while not successful: + accumulator = 0 + program_counter: int = 0 + history: Set[int] = set() + mutation_position += 1 + while program_counter != len(program) and program_counter not in history: + opcode, operand = parse_instruction(program[program_counter]) + history.add(program_counter) + if program_counter == mutation_position: + opcode = mutate_opcode(opcode) + program_counter, accumulator = execute_instruction(opcode, operand, program_counter, accumulator) + if program_counter == len(program): + successful = True + return accumulator + + +if __name__ == '__main__': + production_ready = True + raw_data = import_data(day) if production_ready else sample_data + parsed_data = parse_data(raw_data) + print(part1(parsed_data)) + print(part2(parsed_data)) diff --git a/2020/Day9.py b/2020/Day9.py new file mode 100644 index 0000000000000000000000000000000000000000..7bd9b663de78be0c500e1d93bbc6b3c1d8179da2 --- /dev/null +++ b/2020/Day9.py @@ -0,0 +1,105 @@ +from typing import List, Tuple, Optional + +from ImportData import import_data + +day: int = 9 + +sample_data: List[str] = [ + '35', + '20', + '15', + '25', + '47', + '40', + '62', + '55', + '65', + '95', + '102', + '117', + '150', + '182', + '127', + '219', + '299', + '277', + '309', + '576' +] + +data_structure: type = List[int] + + +def parse_data(data: List[str]) -> data_structure: + return list(map(lambda s: int(s), data)) + + +def validity_check(value: int, preamble: List[int]) -> Tuple[Optional[int], Optional[int]]: + preamble_size: int = len(preamble) + i: int = 0 + witness: List[Optional[int]] = [None, None] + while i < preamble_size - 1: + j: int = i + 1 + while j < preamble_size: + if preamble[i] + preamble[j] == value: + witness = [preamble[i], preamble[j]] + i = preamble_size + 1 + j = preamble_size + 1 + else: + j += 1 + i += 1 + return witness[0], witness[1] + + +def part1(data: data_structure, preamble_size: int) -> int: + preamble: List[int] = data[:preamble_size] + working_queue: List[int] = data[preamble_size:] + found_invalid_number: bool = False + while not found_invalid_number: + witness = validity_check(working_queue[0], preamble) + if witness[0] is None: + found_invalid_number = True + else: + preamble.pop(0) + preamble.append(working_queue.pop(0)) + return working_queue[0] + + +def find_range(number: int, data: List[int]) -> Tuple[int, int]: + data_length = len(data) + left: int = 0 + # noinspection PyUnusedLocal + right: int = 0 + weakness_sum: int = 0 + while left < data_length - 1 and weakness_sum != number: + right = left + 1 + weakness_sum = data[left] + while right < data_length and weakness_sum < number: + weakness_sum += data[right] + if weakness_sum != number: + right += 1 + if weakness_sum != number: + left += 1 + return left, right + + +def part2(number: int, data: data_structure) -> int: + left, right = find_range(number, data) + least: int = data[left] + greatest: int = data[left] + for number in data[left:right + 1]: + if number < least: + least = number + if number > greatest: + greatest = number + return least + greatest + + +if __name__ == '__main__': + production_ready = True + raw_data = import_data(day) if production_ready else sample_data + preamble_length: int = 25 if production_ready else 5 + parsed_data = parse_data(raw_data) + invalid_number = part1(parsed_data, preamble_length) + print(invalid_number) + print(part2(invalid_number, parsed_data)) diff --git a/2020/DayXX b/2020/DayXX new file mode 100644 index 0000000000000000000000000000000000000000..9740ec2f1a8e18a5d7aa6dc34105c088ec6cc327 --- /dev/null +++ b/2020/DayXX @@ -0,0 +1,30 @@ +from typing import List + +from ImportData import import_data + +day: int = X + +sample_data: List[str] = [ + +] + +data_structure: type = Y + + +def parse_data(data: List[str]) -> data_structure: + return None + + +def part1(data: data_structure) -> int: + return -1 + + +def part2(data: data_structure) -> int: + return -1 + + +if __name__ == '__main__': + production_ready = False + raw_data = import_data(day) if production_ready else sample_data + print(part1(parse_data(raw_data))) + print(part2(parse_data(raw_data))) diff --git a/2020/ImportData.TEMPLATE b/2020/ImportData.TEMPLATE new file mode 100644 index 0000000000000000000000000000000000000000..dec59dc9991895eaf23c557f5c3ca033f190f62c --- /dev/null +++ b/2020/ImportData.TEMPLATE @@ -0,0 +1,18 @@ +from typing import List + +import requests + +# noinspection SpellCheckingInspection +session_token = '' + + +def import_data(aoc_day: int, year: int = 2020) -> List[str]: + return requests.get(url=f"https://adventofcode.com/{year}/day/{aoc_day}/input", + headers={'Cookie': f'session={session_token}'}).text.split("\n")[:-1] + + +def read_file(filename: str) -> List[str]: + with open(filename) as file: + data = file.readlines() + line: str = '' + return [line.strip() for line in data] diff --git a/2021/pom.xml b/2021/pom.xml new file mode 100644 index 0000000000000000000000000000000000000000..22efcffb89dca126b7434ebb05893b6b82e3d72e --- /dev/null +++ b/2021/pom.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <groupId>edu.unl.cse.bohn</groupId> + <artifactId>AdventOfCoding</artifactId> + <version>1.0-SNAPSHOT</version> + + <properties> + <maven.compiler.source>17</maven.compiler.source> + <maven.compiler.target>17</maven.compiler.target> + <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> + </properties> + + <dependencies> + <dependency> + <groupId>com.googlecode.json-simple</groupId> + <artifactId>json-simple</artifactId> + <version>1.1.1</version> + </dependency> + </dependencies> + +</project> \ No newline at end of file diff --git a/2021/src/main/java/edu/unl/cse/bohn/ImportData.java b/2021/src/main/java/edu/unl/cse/bohn/ImportData.java new file mode 100644 index 0000000000000000000000000000000000000000..e472acd7745eaa72bdead83c1254703abbf21534 --- /dev/null +++ b/2021/src/main/java/edu/unl/cse/bohn/ImportData.java @@ -0,0 +1,91 @@ +package edu.unl.cse.bohn; + +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; + +import java.io.BufferedReader; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLConnection; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; + +public class ImportData { + + public static final String FILENAME = "apikeys.json"; + + protected final String protocol; + protected final String host; + protected final String path; + protected final String apiKey; + + @SuppressWarnings("unused") + public static List<String> readFile(String filename) throws IOException { + List<String> data = new LinkedList<>(); + BufferedReader bufferedReader; + bufferedReader = new BufferedReader(new FileReader(filename)); + while (bufferedReader.ready()) { + data.add(bufferedReader.readLine()); + } + return data; + } + + public ImportData(String apiKeyName, int year, int day) { + String apiKey; + protocol = "https"; + host = "adventofcode.com"; + path = "/" + year + "/day/" + day + "/input"; + try { + apiKey = getApiKey(apiKeyName); + } catch (IOException ioException) { + System.err.println("Could not retrieve API key: " + ioException.getMessage()); + apiKey = null; + } + this.apiKey = apiKey; + } + + protected String getApiKey(String apiKeyName) throws IOException { + JSONObject apiKeyJson; + try (InputStreamReader inputStreamReader = new InputStreamReader( + Objects.requireNonNull(ImportData.class.getClassLoader().getResourceAsStream(FILENAME)))) { + apiKeyJson = (JSONObject)new JSONParser().parse(inputStreamReader); + } catch (NullPointerException nullPointerException) { + FileNotFoundException newException = new FileNotFoundException("File " + FILENAME + " not found."); + newException.initCause(nullPointerException); + throw newException; + } catch (ParseException parseException) { + throw new IOException("Error while parsing file " + FILENAME + ".", parseException); + } + if (!apiKeyJson.containsKey(apiKeyName)) { + System.err.println("WARNING! Could not locate API key named " + apiKeyName + " in file " + FILENAME + "."); + } + String apiKey = apiKeyJson.get(apiKeyName).toString(); + if (apiKey.equals("")) { + System.err.println("WARNING! API key named " + apiKeyName + " in file " + FILENAME + " is blank."); + } + return apiKey; + } + + public List<String> importData() throws IOException { + List<String> data = new LinkedList<>(); + BufferedReader bufferedReader; + try { + URLConnection connection = new URI(protocol, host, path, null).toURL().openConnection(); + connection.setRequestProperty("Cookie", apiKey); + bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream())); + } catch (URISyntaxException | MalformedURLException originalException) { + throw new IOException("Could not retrieve usable data from " + host + ".", originalException); + } + while (bufferedReader.ready()) { + data.add(bufferedReader.readLine()); + } + return data; + } +} diff --git a/2021/src/main/java/edu/unl/cse/bohn/Main.java b/2021/src/main/java/edu/unl/cse/bohn/Main.java new file mode 100644 index 0000000000000000000000000000000000000000..f2d082fd7b968486479b4092672d1675706c6af0 --- /dev/null +++ b/2021/src/main/java/edu/unl/cse/bohn/Main.java @@ -0,0 +1,49 @@ +package edu.unl.cse.bohn; + +import java.lang.reflect.InvocationTargetException; +import java.util.Calendar; +import java.util.Scanner; + +public class Main { + public static final String defaultApiKey = "aoc"; + + private static String getUserInput(String prompt, String defaultValue, Scanner scanner) { + System.out.print(prompt + "[" + defaultValue + "]: "); + String userInput = scanner.nextLine(); + return userInput.equals("") ? defaultValue : userInput; + } + + public static void main(String... arguments) { + Scanner scanner = new Scanner(System.in); + String apiKey = getUserInput("Enter API key", defaultApiKey, scanner); + Calendar calendar = Calendar.getInstance(); + int year = Integer.parseInt(getUserInput("Enter puzzle year", + Integer.toString(calendar.get(Calendar.YEAR)), scanner)); + int day = Integer.parseInt(getUserInput("Enter puzzle day", + Integer.toString(calendar.get(Calendar.DAY_OF_MONTH)), scanner)); + scanner.close(); + ImportData dataSource = new ImportData(apiKey, year, day); + String className = Main.class.getPackageName() + ".year" + year + ".Day" + day; + Puzzle puzzle = null; + try { + puzzle = (Puzzle)Class.forName(className).getConstructors()[0] + .newInstance(); + } catch (ClassNotFoundException classNotFoundException) { + System.err.println("Could not find class " + className); + System.exit(1); + } catch (InstantiationException ignored) { + System.err.println(className + " is an abstract class."); + System.exit(1); + } catch (IllegalAccessException ignored) { + System.err.println("The requested constructor for " + className + " is inaccessible."); + System.exit(1); + } catch (InvocationTargetException invocationTargetException) { + System.err.println("The constructor for " + className + " threw an exception."); + System.err.println(invocationTargetException.getMessage()); + Throwable originalException = invocationTargetException.getCause(); + System.err.println("Caused by: " + originalException); + System.exit(1); + } + puzzle.solvePuzzle(dataSource); + } +} diff --git a/2021/src/main/java/edu/unl/cse/bohn/Puzzle.java b/2021/src/main/java/edu/unl/cse/bohn/Puzzle.java new file mode 100644 index 0000000000000000000000000000000000000000..ef977db5137db368296ef5a4e7b6e7b3f34cf3f3 --- /dev/null +++ b/2021/src/main/java/edu/unl/cse/bohn/Puzzle.java @@ -0,0 +1,25 @@ +package edu.unl.cse.bohn; + +import java.io.IOException; +import java.util.List; + +public abstract class Puzzle { + protected Integer day = null; + protected String sampleData = ""; + protected boolean isProductionReady = false; + + public abstract int computePart1(List<String> data); + public abstract int computePart2(List<String> data); + + public void solvePuzzle(ImportData dataSource) { + List<String> data = null; + try { + data = isProductionReady ? dataSource.importData() : List.of(sampleData.split(System.lineSeparator())); + } catch (IOException ioException) { + System.err.println("Could not retrieve data: " + ioException); + System.exit(1); + } + System.out.println("Part 1: " + computePart1(data)); + System.out.println("Part 2: " + computePart2(data)); + } +} diff --git a/2021/src/main/java/edu/unl/cse/bohn/year2021/Day1.java b/2021/src/main/java/edu/unl/cse/bohn/year2021/Day1.java new file mode 100644 index 0000000000000000000000000000000000000000..2ad7cd2dbd863120672cbc78ec8431534f7fe6a4 --- /dev/null +++ b/2021/src/main/java/edu/unl/cse/bohn/year2021/Day1.java @@ -0,0 +1,56 @@ +package edu.unl.cse.bohn.year2021; + +import edu.unl.cse.bohn.Puzzle; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +@SuppressWarnings("unused") +public class Day1 extends Puzzle { + List<Integer> depths; + + public Day1() { + day = 1; + sampleData = """ + 199 + 200 + 208 + 210 + 200 + 207 + 240 + 269 + 260 + 263"""; + isProductionReady = true; + } + + private int countIncreases(List<Integer> values) { + Integer lastValue = null; + int increases = 0; + for (int value : values) { + if ((lastValue != null) && (value > lastValue)) { + increases++; + } + lastValue = value; + } + return increases; + } + + @Override + public int computePart1(List<String> data) { + depths = data.stream().map(Integer::parseInt).collect(Collectors.toList()); + return countIncreases(depths); + } + + @Override + public int computePart2(List<String> data) { + int numberOfWindows = depths.size() - 2; + List<Integer> slidingWindows = new ArrayList<>(numberOfWindows); + for (int i = 0; i < numberOfWindows; i++) { + slidingWindows.add(depths.get(i) + depths.get(i + 1) + depths.get(i + 2)); + } + return countIncreases(slidingWindows); + } +} diff --git a/2021/src/main/resources/apikeys.TEMPLATE b/2021/src/main/resources/apikeys.TEMPLATE new file mode 100644 index 0000000000000000000000000000000000000000..13923fa04cdb8a3358bde4f36423622de4e22d3c --- /dev/null +++ b/2021/src/main/resources/apikeys.TEMPLATE @@ -0,0 +1,3 @@ +{ + "aoc": "" +} diff --git a/README.md b/README.md index 5344c27f29239bf20ff5efc7cb36d8d7b1d5824b..4c081ccc9269d9530b25a813f82950697918e739 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,5 @@ -# Advent of Coding +# 2021 Advent of Coding Solutions +## Day 1 + +A nice little warmup exercise in which we iterate over a list.