mirror of
https://github.com/thib8956/advent-of-code.git
synced 2025-08-24 00:11:57 +00:00
chore: create new project structure and aoc.py runner script
This commit is contained in:
48
adventofcode/2023/day1/day1.py
Normal file
48
adventofcode/2023/day1/day1.py
Normal file
@@ -0,0 +1,48 @@
|
||||
def part1(lines):
|
||||
res = []
|
||||
|
||||
for line in lines:
|
||||
digits = [c for c in line if c.isnumeric()]
|
||||
res.append(int(digits[0] + digits[-1]))
|
||||
|
||||
print(f"Part 1: {sum(res)}")
|
||||
|
||||
|
||||
spelled_digits = { "one": "o1e", "two": "t2o", "three": "t3e", "four": "f4r", "five": "f5e", "six": "s6x", "seven": "s7n", "eight": "e8t", "nine": "n9e" }
|
||||
|
||||
|
||||
def substitute_digits(s, trans):
|
||||
res = s
|
||||
for word, digit in trans.items():
|
||||
res = res.replace(word, digit)
|
||||
return res
|
||||
|
||||
|
||||
def part2(lines):
|
||||
res = []
|
||||
|
||||
for line in lines:
|
||||
line = line.rstrip()
|
||||
res_line = []
|
||||
|
||||
nline = ""
|
||||
for c in line:
|
||||
nline += c
|
||||
nline = substitute_digits(nline, spelled_digits)
|
||||
|
||||
digits = [c for c in nline if c.isnumeric()]
|
||||
r = int(digits[0] + digits[-1])
|
||||
print(f"{line} => {nline}, {r}")
|
||||
res.append(r)
|
||||
|
||||
print(f"Part 2: {sum(res)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
infile = sys.argv[1]
|
||||
|
||||
with open(infile) as f:
|
||||
lines = f.readlines()
|
||||
part1(lines)
|
||||
part2(lines)
|
158
adventofcode/2023/day10/day10.py
Normal file
158
adventofcode/2023/day10/day10.py
Normal file
@@ -0,0 +1,158 @@
|
||||
from typing import Dict, List, Set, Tuple
|
||||
from dataclasses import dataclass
|
||||
from collections import defaultdict
|
||||
from enum import Enum
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Vec2d:
|
||||
x: int
|
||||
y: int
|
||||
|
||||
def __add__(self, other):
|
||||
return Vec2d(self.x + other.x, self.y + other.y)
|
||||
|
||||
|
||||
class Direction(Enum):
|
||||
NORTH = Vec2d(0, -1) # [0, 0] is the top-left corner, so y increases going downwards
|
||||
SOUTH = Vec2d(0, 1)
|
||||
EAST = Vec2d(1, 0)
|
||||
WEST = Vec2d(-1, 0)
|
||||
|
||||
|
||||
"""
|
||||
| is a vertical pipe connecting north and south.
|
||||
- is a horizontal pipe connecting east and west.
|
||||
L is a 90-degree bend connecting north and east.
|
||||
J is a 90-degree bend connecting north and west.
|
||||
7 is a 90-degree bend connecting south and west.
|
||||
F is a 90-degree bend connecting south and east.
|
||||
. is ground; there is no pipe in this tile.
|
||||
S is the starting position of the animal; there is a pipe on this tile, but your sketch doesn't show what shape the pipe has.
|
||||
"""
|
||||
PIPES = {
|
||||
"|": (Direction.SOUTH, Direction.NORTH),
|
||||
"-": (Direction.EAST, Direction.WEST),
|
||||
"L": (Direction.NORTH, Direction.EAST),
|
||||
"J": (Direction.NORTH, Direction.WEST),
|
||||
"7": (Direction.SOUTH, Direction.WEST),
|
||||
"F": (Direction.SOUTH,Direction.EAST),
|
||||
}
|
||||
|
||||
PIPES_REVERSE_LOOKUP = {v: k for k,v in PIPES.items()}
|
||||
|
||||
|
||||
def find_start_position(grid: List[str]) -> Vec2d:
|
||||
for y, row in enumerate(grid):
|
||||
for x, c in enumerate(row):
|
||||
if c == "S":
|
||||
return Vec2d(x, y)
|
||||
raise RuntimeError("The start position was not found")
|
||||
|
||||
|
||||
def update_start_symbol(grid: List[str], start_pos: Vec2d):
|
||||
"""
|
||||
Updates the map by replacing the start symbol "S" with its actual corresponding pipe
|
||||
"""
|
||||
# check which neighbors are connected to the start position
|
||||
connections = []
|
||||
north = start_pos + Direction.NORTH.value
|
||||
south = start_pos + Direction.SOUTH.value
|
||||
east = start_pos + Direction.EAST.value
|
||||
west = start_pos + Direction.WEST.value
|
||||
|
||||
if grid[north.y][north.x] in "|7F":
|
||||
connections.append(Direction.NORTH)
|
||||
|
||||
if grid[south.y][south.x] in "|LJ":
|
||||
connections.append(Direction.SOUTH)
|
||||
|
||||
if grid[east.y][east.x] in "-7J":
|
||||
connections.append(Direction.EAST)
|
||||
|
||||
if grid[west.y][west.x] in "-LF":
|
||||
connections.append(Direction.WEST)
|
||||
|
||||
print("Start symbol has the following connections: ", connections)
|
||||
assert len(connections) == 2, "start symbol has invalid connections"
|
||||
pipe = PIPES_REVERSE_LOOKUP[tuple(connections)]
|
||||
print(f"Start symbol is a {pipe} pipe")
|
||||
|
||||
# replace it in the grid accordingly
|
||||
grid[start_pos.y] = grid[start_pos.y].replace("S", pipe)
|
||||
|
||||
|
||||
def parse_graph(grid: List[str]) -> Dict[Vec2d, List[Vec2d]]:
|
||||
graph = defaultdict(list)
|
||||
|
||||
for y, row in enumerate(grid):
|
||||
for x, pipe in enumerate(row):
|
||||
pos = Vec2d(x, y)
|
||||
if pipe in PIPES:
|
||||
for direction in PIPES[pipe]:
|
||||
next_pos = pos + direction.value
|
||||
graph[pos].append(next_pos)
|
||||
return graph
|
||||
|
||||
|
||||
def traverse_graph(graph, start_pos) -> Tuple[int, Set[Vec2d]]:
|
||||
"""
|
||||
traverse the graph using BFS, return the path and the
|
||||
find the length of the longest path in the graph
|
||||
"""
|
||||
queue = [(start_pos, 0)] # (pos, distance from start)
|
||||
max_dist = 0
|
||||
visited = {start_pos}
|
||||
|
||||
while queue != []:
|
||||
cur, dist = queue.pop(0)
|
||||
max_dist = max(max_dist, dist)
|
||||
|
||||
for next_pos in graph[cur]:
|
||||
if next_pos not in visited:
|
||||
visited.add(next_pos)
|
||||
queue.append((next_pos, dist+1))
|
||||
|
||||
return max_dist, visited
|
||||
|
||||
|
||||
def count_enclosed_tiles(grid, edges):
|
||||
"""
|
||||
count the number of enclosed tiles in the loop by casting a ray on each row
|
||||
and counting the number of intersections with the edges of the loop
|
||||
"""
|
||||
enclosed_count = 0
|
||||
for y, row in enumerate(grid):
|
||||
crossings = 0
|
||||
for x, pipe in enumerate(row):
|
||||
pos = Vec2d(x, y)
|
||||
if pos in edges:
|
||||
if pipe in "L|J":
|
||||
crossings += 1
|
||||
elif crossings % 2 == 1:
|
||||
enclosed_count += 1
|
||||
return enclosed_count
|
||||
|
||||
|
||||
def main(grid):
|
||||
rows, cols = len(grid), len(grid[0])
|
||||
start_pos = find_start_position(grid)
|
||||
print("Start pos ", start_pos)
|
||||
update_start_symbol(grid, start_pos)
|
||||
graph = parse_graph(grid)
|
||||
|
||||
max_dist, visited = traverse_graph(graph, start_pos)
|
||||
print("Part 1: ", max_dist)
|
||||
|
||||
# visited edges are the ones that are part of the loop
|
||||
inside_count = count_enclosed_tiles(grid, visited)
|
||||
print("Part 2: ", inside_count)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
infile = sys.argv[1]
|
||||
|
||||
with open(infile) as f:
|
||||
lines = [l.rstrip() for l in f.readlines()]
|
||||
main(lines)
|
45
adventofcode/2023/day2/day2.py
Normal file
45
adventofcode/2023/day2/day2.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from math import prod
|
||||
|
||||
|
||||
def part1(lines):
|
||||
result = set()
|
||||
for index, line in enumerate(lines):
|
||||
game_id = index + 1
|
||||
_, line = line.split(": ")
|
||||
result.add(int(game_id))
|
||||
hands = line.rstrip().split("; ")
|
||||
for hand in hands:
|
||||
colors = {"red": 0, "green": 0, "blue": 0}
|
||||
cubes = hand.split(", ")
|
||||
for cube in cubes:
|
||||
n, color = cube.split()
|
||||
colors[color] = int(n)
|
||||
if colors["red"] > 12 or colors["green"] > 13 or colors["blue"] > 14:
|
||||
# impossible configuration, remove this game_id from the result (if present)
|
||||
result.discard(int(game_id))
|
||||
print(f"Part 1: {sum(result)}")
|
||||
|
||||
|
||||
def part2(lines):
|
||||
result = []
|
||||
for line in lines:
|
||||
colors = {"red": 0, "green": 0, "blue": 0}
|
||||
_, line = line.split(": ")
|
||||
hands = line.rstrip().split("; ")
|
||||
for hand in hands:
|
||||
cubes = hand.split(", ")
|
||||
for cube in cubes:
|
||||
n, color = cube.split()
|
||||
colors[color] = max(colors[color], int(n))
|
||||
result.append(prod(colors.values()))
|
||||
print(f"Part 2: {sum(result)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
infile = sys.argv[1]
|
||||
with open(infile) as f:
|
||||
lines = f.readlines()
|
||||
part1(lines)
|
||||
part2(lines)
|
||||
|
116
adventofcode/2023/day3/day3.py
Normal file
116
adventofcode/2023/day3/day3.py
Normal file
@@ -0,0 +1,116 @@
|
||||
from typing import Tuple, List, Set
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Item:
|
||||
pos: Tuple[int, int]
|
||||
symbol: str
|
||||
|
||||
|
||||
def browse_schema(schema):
|
||||
total_parts = 0
|
||||
buf = []
|
||||
max_row, max_col = len(schema), len(schema[0])
|
||||
|
||||
symbols: List[Tuple[Item, Set[Item]]] = []
|
||||
numbers: List[Item] = []
|
||||
|
||||
for y in range(max_row):
|
||||
for x in range(max_col):
|
||||
item = schema[y][x]
|
||||
if item.isnumeric():
|
||||
# continue parsing full number
|
||||
buf.append(item)
|
||||
else:
|
||||
neighbors = get_neighbors_of((x, y), schema)
|
||||
symbols.append((Item((x, y), item), set(neighbors)))
|
||||
if buf and not item.isnumeric():
|
||||
# end of a number, do the engine part check
|
||||
number = "".join(buf)
|
||||
neighbors = get_neighbors((x, y), len(buf), schema)
|
||||
start_pos = (x-len(number), y)
|
||||
symbols.append((Item((x, y), number), get_neighbors_of((x, y), schema)))
|
||||
numbers.append(Item(start_pos, number))
|
||||
|
||||
if is_engine_part(neighbors):
|
||||
total_parts += int(number)
|
||||
|
||||
buf.clear() # reached end of a number, clear buffer
|
||||
|
||||
print(f"Part 1, sum of the parts numbers = {total_parts}")
|
||||
part2(symbols, numbers)
|
||||
|
||||
def part2(symbols, numbers):
|
||||
total_gears = 0
|
||||
stars = [(s, neighbors) for s, neighbors in symbols if s.symbol == "*"]
|
||||
for _, neighbors in stars:
|
||||
corresponding_numbers = set()
|
||||
digits = [n for n in neighbors if n.symbol.isdigit()]
|
||||
for digit in digits:
|
||||
# find full number (number.start_pos < digit.pos < number.end_pos)
|
||||
for number in numbers:
|
||||
if number.pos[1] - 1 <= digit.pos[1] <= number.pos[1] + 1 and number.pos[0] <= digit.pos[0] <= number.pos[0]+len(number.symbol):
|
||||
corresponding_numbers.add(number.symbol)
|
||||
|
||||
if len(corresponding_numbers) == 2:
|
||||
a, b = corresponding_numbers
|
||||
total_gears += int(a) * int(b)
|
||||
#print(f"star: {star.pos} {corresponding_numbers}")
|
||||
|
||||
print(f"Part 2, sum of gear ratios = {total_gears}")
|
||||
|
||||
|
||||
|
||||
def is_engine_part(neighbors: List[Item]) -> bool:
|
||||
# get list of symbols (not '.', \n or a number)
|
||||
symbols = filter(lambda x: not x.symbol.isnumeric() and not x.symbol in (".", "\n"), neighbors)
|
||||
return next(symbols, None) is not None
|
||||
|
||||
|
||||
def get_neighbors(pos: Tuple[int, int], length: int, schema: List[List[str]]) -> List[Item]:
|
||||
x, y = pos
|
||||
start_x = x - length
|
||||
neighbors = [get_neighbors_of((x, y), schema) for x in range(start_x, x)]
|
||||
neighbors = [item for sublist in neighbors for item in sublist] # flatten list of list
|
||||
return neighbors
|
||||
|
||||
|
||||
def get_neighbors_of(pos: Tuple[int, int], schema: List[List[str]]) -> List[Item]:
|
||||
max_row, max_col = len(schema), len(schema[0])
|
||||
x, y = pos
|
||||
neighbors: List[Item] = []
|
||||
|
||||
# top
|
||||
if y-1 >= 0:
|
||||
neighbors.append(Item((x, y-1), schema[y-1][x]))
|
||||
# bottom:
|
||||
if y+1 < max_row:
|
||||
neighbors.append(Item((x, y+1), schema[y+1][x]))
|
||||
# left
|
||||
if x-1 >= 0:
|
||||
neighbors.append(Item((x-1, y), schema[y][x-1]))
|
||||
# right
|
||||
if x+1 < max_col:
|
||||
neighbors.append(Item((x+1, y), schema[y][x+1]))
|
||||
# top-left
|
||||
if y-1 >= 0 and x-1 >= 0:
|
||||
neighbors.append(Item((x-1, y-1), schema[y-1][x-1]))
|
||||
# top-right
|
||||
if y-1 >= 0 and x+1 < max_col:
|
||||
neighbors.append(Item((x+1, y-1), schema[y-1][x+1]))
|
||||
# bottom-left
|
||||
if y+1 < max_row and x-1 >= 0:
|
||||
neighbors.append(Item((x-1, y+1), schema[y+1][x-1]))
|
||||
# bottom-right
|
||||
if y+1 < max_row and x+1 < max_col:
|
||||
neighbors.append(Item((x+1, y+1), schema[y+1][x+1]))
|
||||
|
||||
return neighbors
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
infile = sys.argv[1]
|
||||
with open(infile) as f:
|
||||
schema = [[c for c in line] for line in f.readlines()]
|
||||
browse_schema(schema)
|
44
adventofcode/2023/day4/day4.py
Normal file
44
adventofcode/2023/day4/day4.py
Normal file
@@ -0,0 +1,44 @@
|
||||
def parse_tickets(lines):
|
||||
tickets = []
|
||||
for line in lines:
|
||||
_, nums = line.rstrip().split(": ")
|
||||
winning, played = nums.split(" | ")
|
||||
winning, played = set(winning.split()), set(played.split())
|
||||
tickets.append((winning, played))
|
||||
return tickets
|
||||
|
||||
|
||||
def part1(tickets):
|
||||
total = 0
|
||||
for ticket in tickets:
|
||||
winning, played = ticket
|
||||
num_wins = len(winning.intersection(played))
|
||||
points = 0 if num_wins == 0 else 2**(num_wins-1)
|
||||
total += points
|
||||
print(f"part 1, total={total}")
|
||||
|
||||
|
||||
def part2(tickets):
|
||||
tickets = [[1, t] for t in tickets]
|
||||
for index, ticket in enumerate(tickets):
|
||||
mult = ticket[0]
|
||||
winning, played = ticket[1]
|
||||
num_wins = len(winning.intersection(played))
|
||||
for i in range(index+1, index+1+num_wins):
|
||||
tickets[i][0] += mult
|
||||
num_tickets = sum(n for n, _ in tickets)
|
||||
print(f"part 2, number of tickets: {num_tickets}")
|
||||
|
||||
|
||||
def main(f):
|
||||
tickets = parse_tickets(f)
|
||||
part1(tickets)
|
||||
part2(tickets)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
infile = sys.argv[1]
|
||||
with open(infile) as f:
|
||||
main(f)
|
||||
|
70
adventofcode/2023/day5/day5.py
Normal file
70
adventofcode/2023/day5/day5.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from itertools import zip_longest
|
||||
|
||||
|
||||
def grouper(n, iterable):
|
||||
"grouper(3, 'ABCDEFG', 'x') --> ABC DEF Gxx"
|
||||
args = [iter(iterable)] * n
|
||||
return zip_longest(*args)
|
||||
|
||||
|
||||
def part1(sections):
|
||||
# consume seed section
|
||||
_, seeds = next(sections)
|
||||
seeds = [int(s) for s in seeds]
|
||||
for _, mapping in sections:
|
||||
seeds = [apply_mapping(s, mapping) for s in seeds]
|
||||
print(f"Part 1, lowest location number = {min(seeds)}")
|
||||
|
||||
|
||||
def part2(sections):
|
||||
# consume seed section
|
||||
_, seeds = next(sections)
|
||||
seeds = [int(s) for s in seeds]
|
||||
|
||||
min_seed = 2**128
|
||||
for start_seed, length in grouper(2, seeds):
|
||||
subseeds = range(start_seed, start_seed + length)
|
||||
print(f"calculate_for_subseeds(subseeds len={len(subseeds)})")
|
||||
res = calculate_for_subseeds(subseeds, sections)
|
||||
mini = min(res)
|
||||
if mini < min_seed:
|
||||
min_seed = mini
|
||||
|
||||
print(f"Part 2 {min_seed}")
|
||||
|
||||
|
||||
def calculate_for_subseeds(seeds, sections):
|
||||
new_seeds = seeds
|
||||
for _, mapping in sections:
|
||||
new_seeds = [apply_mapping(s, mapping) for s in new_seeds]
|
||||
return new_seeds
|
||||
|
||||
|
||||
def apply_mapping(seed, mapping):
|
||||
for dst, src, length in grouper(3, mapping):
|
||||
src, length, dst = int(src), int(length), int(dst)
|
||||
end = src + length
|
||||
if src <= seed < end:
|
||||
return seed + (dst-src)
|
||||
return seed
|
||||
|
||||
|
||||
def parse_input(infile):
|
||||
with open(infile) as f:
|
||||
sections = f.read().split("\n\n")
|
||||
sections = ((title, numbers) for title, numbers in (s.split(":") for s in sections))
|
||||
sections = ((title, numbers.split()) for title, numbers in sections)
|
||||
return sections
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
import os
|
||||
SCRIPTPATH = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
infile = next(iter(sys.argv[1:]), None)
|
||||
sections = parse_input(infile or os.path.join(SCRIPTPATH, "example.txt"))
|
||||
part1(sections)
|
||||
sections = parse_input(infile or os.path.join(SCRIPTPATH, "example.txt"))
|
||||
part2(sections)
|
||||
|
40
adventofcode/2023/day6/day6.py
Normal file
40
adventofcode/2023/day6/day6.py
Normal file
@@ -0,0 +1,40 @@
|
||||
def parse_part1(path):
|
||||
with open(path) as f:
|
||||
time, distance = f.readlines()
|
||||
time = [int(x) for x in time.split()[1:]]
|
||||
distance = [int(x) for x in distance.split()[1:]]
|
||||
return zip(time, distance)
|
||||
|
||||
|
||||
def calculate_wins(data):
|
||||
total = 1
|
||||
for time, record in data:
|
||||
ways = 0
|
||||
for n in range(0, time+1):
|
||||
speed = n
|
||||
distance = (time-n) * speed
|
||||
if distance > record:
|
||||
ways += 1
|
||||
total *= ways
|
||||
return total
|
||||
|
||||
|
||||
def parse_part2(path):
|
||||
with open(path) as f:
|
||||
time, distance = f.readlines()
|
||||
time = time.split(":")[1].replace(" ", "").rstrip()
|
||||
distance = distance.split(":")[1].replace(" ", "").rstrip()
|
||||
return int(time), int(distance)
|
||||
|
||||
if __name__ == "__main__":
|
||||
assert calculate_wins(zip(*[[7, 15, 30], [9, 40, 200]])) == 288 # part 1 example
|
||||
assert calculate_wins([[71530, 940200]]) == 71503 # part 2 example
|
||||
|
||||
import sys
|
||||
if len(sys.argv) == 2:
|
||||
data = parse_part1(sys.argv[1])
|
||||
res = calculate_wins(data)
|
||||
print(f"Part 1, res={res}")
|
||||
data = parse_part2(sys.argv[1])
|
||||
res = calculate_wins([data])
|
||||
print(f"Part 2, res={res}")
|
70
adventofcode/2023/day7/day7.py
Normal file
70
adventofcode/2023/day7/day7.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from collections import Counter
|
||||
|
||||
|
||||
def calculate_rank(hand, part2=False):
|
||||
card_ranks = {'2': 1, '3': 2, '4': 3, '5': 4, '6': 5, '7': 6, '8': 7, '9': 8, 'T': 9, 'J': 10, 'Q': 11, 'K': 12, 'A': 13}
|
||||
if part2:
|
||||
# in part2, jokers are the weakest card
|
||||
card_ranks['J'] = 0
|
||||
# substitute cards with their ranks to make them sortable
|
||||
hand = [card_ranks[c] for c in hand]
|
||||
cnt = Counter(hand)
|
||||
|
||||
if part2 and cnt[0] != 5: # edge case if hand == 'JJJJJ'
|
||||
# substitute jokers with the most common card that isn't a joker
|
||||
most_common_card = cnt.most_common(2)[0][0] if cnt.most_common(2)[0][0] != 0 else cnt.most_common(2)[1][0]
|
||||
new_hand = [most_common_card if c == 0 else c for c in hand]
|
||||
cnt = Counter(new_hand)
|
||||
|
||||
rank = 0
|
||||
match sorted(cnt.values()):
|
||||
case [5]: rank = 7
|
||||
case [1, 4]: rank = 6
|
||||
case [2, 3]: rank = 5
|
||||
case [1, 1, 3]: rank = 4
|
||||
case [1, 2, 2]: rank = 3
|
||||
case [1, 1, 1, 2]: rank = 2
|
||||
case [1, 1, 1, 1, 1]: rank = 1
|
||||
# return rank, and hand as a tiebreaker
|
||||
return (rank, hand)
|
||||
|
||||
def parse_input(inp):
|
||||
hands = [l.strip().split() for l in inp.strip().split("\n")]
|
||||
return hands
|
||||
|
||||
|
||||
def calculate_wins(hands, part2=False):
|
||||
total = 0
|
||||
hands = sorted(hands, key=lambda hb: calculate_rank(hb[0], part2=part2))
|
||||
for rank, hand in enumerate(hands):
|
||||
hand, bid = hand
|
||||
total += (rank + 1) * int(bid)
|
||||
return total
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sample_input = """
|
||||
32T3K 765
|
||||
T55J5 684
|
||||
KK677 28
|
||||
KTJJT 220
|
||||
QQQJA 483
|
||||
"""
|
||||
res = calculate_wins(parse_input(sample_input))
|
||||
print(f"part 1 example: {res}")
|
||||
assert res == 6440
|
||||
|
||||
res = calculate_wins(parse_input(sample_input), part2=True)
|
||||
print(f"part 2 example: {res}")
|
||||
assert res == 5905
|
||||
|
||||
import sys
|
||||
if len(sys.argv) == 2:
|
||||
with open(sys.argv[1]) as f:
|
||||
inp = parse_input(f.read())
|
||||
|
||||
res = calculate_wins(inp)
|
||||
print(f"Part 1, res={res}")
|
||||
|
||||
res = calculate_wins(inp, part2=True)
|
||||
print(f"Part 2, res={res}")
|
75
adventofcode/2023/day8/day8.py
Normal file
75
adventofcode/2023/day8/day8.py
Normal file
@@ -0,0 +1,75 @@
|
||||
import re
|
||||
import math
|
||||
from itertools import cycle
|
||||
|
||||
def parse_input(infile):
|
||||
with open(infile) as f:
|
||||
content = f.read().rstrip()
|
||||
directions, nodes = content.split("\n\n")
|
||||
directions = directions.strip()
|
||||
nodes = nodes.split("\n")
|
||||
nodes = [n.split(" = ") for n in nodes]
|
||||
nodes = {k: re.findall(r"\w{3}", v) for k, v in nodes}
|
||||
return directions, nodes
|
||||
|
||||
|
||||
def part1(directions, nodes):
|
||||
iterations = 0
|
||||
current_node = "AAA"
|
||||
for d in cycle(directions):
|
||||
if current_node == "ZZZ":
|
||||
break
|
||||
iterations += 1
|
||||
if d == "L":
|
||||
current_node = nodes[current_node][0]
|
||||
else:
|
||||
current_node = nodes[current_node][1]
|
||||
print(f"Part 1: reached 'ZZZ' in {iterations} iterations")
|
||||
|
||||
|
||||
def part2(directions, nodes):
|
||||
current_nodes = [k for k in nodes.keys() if k.endswith("A")]
|
||||
# keep track of iterations number for each visited node
|
||||
# (the number will stop to beeing incremented once the node n_i value reached the target value 'xxZ')
|
||||
iterations = [0] * len(current_nodes)
|
||||
|
||||
for d in cycle(directions):
|
||||
if all(c.endswith("Z") for c in current_nodes):
|
||||
break
|
||||
|
||||
if d == "L":
|
||||
new_nodes = []
|
||||
for i, n in enumerate(current_nodes):
|
||||
if n.endswith("Z"): # end condition already reached for this node
|
||||
new_nodes.append(n)
|
||||
else:
|
||||
new_nodes.append(nodes[n][0])
|
||||
iterations[i] += 1
|
||||
current_nodes = new_nodes
|
||||
else:
|
||||
new_nodes = []
|
||||
for i, n in enumerate(current_nodes):
|
||||
if n.endswith("Z"): # end condition already reached for this node
|
||||
new_nodes.append(n)
|
||||
else:
|
||||
new_nodes.append(nodes[n][1])
|
||||
iterations[i] += 1
|
||||
current_nodes = new_nodes
|
||||
|
||||
# the result is the lowest common multiple between the number of iterations
|
||||
# for each node
|
||||
result = math.lcm(*iterations)
|
||||
print(f"Part 2: reached all nodes such that 'xxZ' in {result} iterations")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
import os
|
||||
SCRIPTPATH = os.path.dirname(os.path.realpath(__file__))
|
||||
infile = sys.argv[1] if len(sys.argv) == 2 else "example.txt"
|
||||
|
||||
directions, nodes = parse_input(os.path.join(SCRIPTPATH, infile))
|
||||
part1(directions, nodes)
|
||||
|
||||
directions, nodes = parse_input(os.path.join(SCRIPTPATH, infile))
|
||||
part2(directions, nodes)
|
33
adventofcode/2023/day9/day9.py
Normal file
33
adventofcode/2023/day9/day9.py
Normal file
@@ -0,0 +1,33 @@
|
||||
def parse_input(infile):
|
||||
with open(infile) as f:
|
||||
return [[int(x) for x in l.strip().split()] for l in f.readlines()]
|
||||
|
||||
|
||||
def process_line(line):
|
||||
if set(line) == {0}:
|
||||
return 0
|
||||
else:
|
||||
next_line = [cur - next for next, cur in zip(line, line[1:])]
|
||||
return line[-1] + process_line(next_line)
|
||||
|
||||
|
||||
def process_line_back(line):
|
||||
if set(line) == {0}:
|
||||
return 0
|
||||
else:
|
||||
next_line = [cur - next for next, cur in zip(line, line[1:])]
|
||||
return line[0] - process_line_back(next_line)
|
||||
|
||||
|
||||
def solve(data):
|
||||
print(f"Part 1: {sum(process_line(l) for l in data)}")
|
||||
print(f"Part 2: {sum(process_line_back(l) for l in data)}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
import os
|
||||
SCRIPTPATH = os.path.dirname(os.path.realpath(__file__))
|
||||
infile = sys.argv[1] if len(sys.argv) == 2 else "example.txt"
|
||||
data = parse_input(os.path.join(SCRIPTPATH, infile))
|
||||
solve(data)
|
Reference in New Issue
Block a user