Skip to content
Snippets Groups Projects
Commit e982821c authored by Christopher Bohn's avatar Christopher Bohn :thinking:
Browse files

Started 2021 Advent of Code

parent 926b13e0
No related branches found
No related tags found
No related merge requests found
# 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/
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)))
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)))
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)))
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)))
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)))
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)))
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)))
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
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))
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))
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)))
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)))
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)))
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)))
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)))
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)}')
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)))
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)))
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]))
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment