Add 2023 solutions

This commit is contained in:
2024-07-07 20:30:53 -04:00
parent da1c37ffa7
commit d582dfc777
29 changed files with 2826 additions and 0 deletions

89
2023/d1.py Normal file
View File

@@ -0,0 +1,89 @@
import re
EXAMPLE = """
1abc2
pqr3stu8vwx
a1b2c3d4e5f
treb7uchet
"""
EXAMPLE2 = """
two1nine
eightwothree
abcone2threexyz
xtwone3four
4nineeightseven2
zoneight234
7pqrstsixteen
"""
WORD_TO_DIGIT = {
"1": "1",
"2": "2",
"3": "3",
"4": "4",
"5": "5",
"6": "6",
"7": "7",
"8": "8",
"9": "9",
"one": "1",
"two": "2",
"three": "3",
"four": "4",
"five": "5",
"six": "6",
"seven": "7",
"eight": "8",
"nine": "9",
}
def get_sum(text: str):
s = 0
r = re.compile(r"[^\d]")
for line in text.splitlines():
if line.strip() == "":
continue
line = r.sub("", line)
s += int(line[0] + line[-1])
return s
def get_sum2(text: str):
s = 0
for line in text.splitlines():
if line.strip() == "":
continue
first = ""
for i in range(len(line)):
for (word, digit) in WORD_TO_DIGIT.items():
if word == line[i:i + len(word)]:
first = digit
break
if first != "":
break
last = ""
for i in range(len(line), 0, -1):
for (word, digit) in WORD_TO_DIGIT.items():
if word == line[i - len(word):i]:
last = digit
break
if last != "":
break
v = first + last
s += int(v)
return s
if __name__ == "__main__":
assert(get_sum(EXAMPLE) == 142)
assert(get_sum2(EXAMPLE2) == 281)
with open("d1.txt", 'r') as f:
text = f.read()
r = get_sum(text)
print(r)
assert(r == 55386)
r = get_sum2(text)
print(r)
assert(r == 54824)

218
2023/d10.py Normal file
View File

@@ -0,0 +1,218 @@
import lib
EXAMPLE = """
..F7.
.FJ|.
SJ.L7
|F--J
LJ...
"""
EXAMPLE2 = """
...........
.S-------7.
.|F-----7|.
.||.....||.
.||.....||.
.|L-7.F-J|.
.|..|.|..|.
.L--J.L--J.
...........
"""
EXAMPLE3 = """
..........
.S------7.
.|F----7|.
.||OOOO||.
.||OOOO||.
.|L-7F-J|.
.|II||II|.
.L--JL--J.
..........
"""
EXAMPLE4 = """
FF7FSF7F7F7F7F7F---7
L|LJ||||||||||||F--J
FL-7LJLJ||||||LJL-77
F--JF--7||LJLJIF7FJ-
L---JF-JLJIIIIFJLJJ7
|F|F-JF---7IIIL7L|7|
|FFJF7L7F-JF7IIL---7
7-L-JL7||F7|L7F-7F7|
L.L7LFJ|||||FJL7||LJ
L7JLJL-JLJLJL--JLJ.L
"""
# | 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.
# Mapping of in-direction to out-direction for each piece.
DIRS = {
'|': {
(1, 0): (1, 0),
(-1, 0): (-1, 0),
},
'-': {
(0, -1): (0, -1),
(0, 1): (0, 1),
},
'L': {
(1, 0): (0, 1),
(0, -1): (-1, 0),
},
'J': {
(0, 1): (-1, 0),
(1, 0): (0, -1),
},
'7': {
(0, 1): (1, 0),
(-1, 0): (0, -1),
},
'F': {
(-1, 0): (0, 1),
(0, -1): (1, 0),
}
}
RIGHTS = {
'|': {
(1, 0): [(0, -1)],
(-1, 0): [(0, 1)],
},
'-': {
(0, -1): [(-1, 0)],
(0, 1): [(1, 0)],
},
'L': {
(1, 0): [(0, -1), (1, 0)],
(0, -1): [(-1, 1)],
},
'J': {
(0, 1): [(1, 0), (0, 1)],
(1, 0): [(-1, -1)],
},
'7': {
(0, 1): [(1, -1)],
(-1, 0): [(0, 1), (-1, 0)],
},
'F': {
(-1, 0): [(1, 1)],
(0, -1): [(-1, 0), (0, -1)],
}
}
def solve(lines: list[str], return_path=False):
res = 0
maze = []
start = ()
max_path, max_right = [], []
maze = list(map(list, lines))
for i, row in enumerate(maze):
for j, col in enumerate(row):
if col == "S":
start = (i , j)
row_max, col_max = len(maze), len(maze[0])
for current_dir in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
current_field = start
steps = 0
path = []
right = []
# print(f"START: {start} {current_dir=}")
while True:
path.append(current_field)
next_field = (current_field[0] + current_dir[0], current_field[1] + current_dir[1])
if next_field == start:
break
if next_field[0] < 0 or next_field[1] < 0 or next_field[0] >= row_max or next_field[1] >= col_max:
# print(f"BREAK: Cannot go to {next_field=}!")
break
prev_dir = current_dir
next_char = maze[next_field[0]][next_field[1]]
try:
current_dir = DIRS[next_char][current_dir]
except KeyError:
# print(f"BREAK: Cannot go from {current_field=} with {current_dir=} to {next_field=} ({next_char})!")
break
for rights in RIGHTS[next_char][prev_dir]:
rf = (next_field[0] + rights[0], next_field[1] + rights[1])
right.append(rf)
current_field = next_field
steps += 1
res = max(res, (steps + 1) // 2)
if len(path) > len(max_path):
max_path = path
max_right = right
if return_path:
return res, max_path, max_right
else:
return res
def solve2(lines: list[str]):
row_max = len(lines)
col_max = len(lines[0])
length, path, right = solve(lines, True)
def clean(fields):
fields = list(set(fields))
cleaned = []
for f in fields:
if f in path:
continue
if f[0] < 0 or f[1] < 0 or f[0] >= row_max or f[1] >= col_max:
continue
cleaned.append(f)
return cleaned
right = clean(right)
visited = set()
to_visit = list(right)
while to_visit:
current = to_visit.pop()
for nb in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
f = (current[0] + nb[0], current[1] + nb[1])
if f[0] < 0 or f[1] < 0 or f[0] >= row_max or f[1] >= col_max:
continue
if f not in visited and f not in path:
right.append(f)
to_visit.append(f)
visited.add(f)
right = clean(right)
n_all = row_max * col_max
n_right = len(right)
n_left = n_all - n_right - len(path)
return min(n_left, n_right)
def main():
lines = lib.str_to_lines_no_empty(EXAMPLE)
print("Example 1:", solve(lines))
lines = lib.str_to_lines_no_empty(open("d10.txt").read())
print("Solution 1:", solve(lines))
lines = lib.str_to_lines_no_empty(EXAMPLE4)
print("Example 2:", solve2(lines))
lines = lib.str_to_lines_no_empty(open("d10.txt").read())
print("Solution 2:", solve2(lines))
if __name__ == "__main__":
main()

111
2023/d11.py Normal file
View File

@@ -0,0 +1,111 @@
import lib
EXAMPLE = """
...#......
.......#..
#.........
..........
......#...
.#........
.........#
..........
.......#..
#...#.....
"""
def mdist(a, b):
return abs(a[0] - b[0]) + abs(a[1] - b[1])
def solve(lines: list[str]):
res = 0
g = list(map(list, lines))
er = []
for (i, r) in enumerate(g):
if "#" not in r:
er.append(i)
ec = []
for j in range(len(g[0])):
for row in g:
if "#" == row[j]:
break
else:
ec.append(j)
for r in reversed(er):
g.insert(r, ["." for _ in range(len(g[0]))])
for row in g:
for c in reversed(ec):
row.insert(c, ".")
# for row in g:
# print("".join(row))
gxs = []
for (row, line) in enumerate(g):
for (col, c) in enumerate(line):
if c == '#':
gxs.append((row, col))
for i in range(len(gxs)):
for j in range(i, len(gxs)):
a, b = gxs[i], gxs[j]
d = mdist(a, b)
# print(a, b, d)
res += d
# 16:00
return res
def solve2(lines: list[str], factor):
res = 0
g = list(map(list, lines))
gxs = []
for (row, line) in enumerate(g):
for (col, c) in enumerate(line):
if c == '#':
gxs.append((row, col, row, col))
for (row_i, row) in enumerate(g):
if "#" not in row:
for i in range(len(gxs)):
row, col, orig_row, orig_col = gxs[i]
if orig_row > row_i:
gxs[i] = (row + factor, col, orig_row, orig_col)
for col_j in range(len(g[0])):
for row in g:
if "#" == row[col_j]:
break
else:
for i in range(len(gxs)):
row, col, orig_row, orig_col = gxs[i]
if orig_col > col_j:
gxs[i] = (row, col + factor, orig_row, orig_col)
for i in range(len(gxs)):
for j in range(i, len(gxs)):
a, b = gxs[i], gxs[j]
d = mdist(a, b)
# print(a, b, d)
res += d
# 16:00
return res
def main():
lines = lib.str_to_lines_no_empty(EXAMPLE)
print("Example 1:", solve(lines))
lines = lib.str_to_lines_no_empty(open("d11.txt").read())
print("Solution 1:", solve(lines))
lines = lib.str_to_lines_no_empty(EXAMPLE)
print("Example 2:", solve2(lines, 99))
lines = lib.str_to_lines_no_empty(open("d11.txt").read())
print("Solution 2:", solve2(lines, 10**6 - 1))
if __name__ == "__main__":
main()

69
2023/d12.py Normal file
View File

@@ -0,0 +1,69 @@
import lib
from functools import lru_cache
EXAMPLE = """
???.### 1,1,3
.??..??...?##. 1,1,3
?#?#?#?#?#?#?#? 1,3,1,6
????.#...#... 4,1,1
????.######..#####. 1,6,5
?###???????? 3,2,1
"""
@lru_cache
def count(elems, groups):
if not elems and not groups:
return 1
elif elems and not groups:
if all(e in ['.', '?'] for e in elems):
return 1
return 0
elif not elems and groups:
return 0
r = 0
if elems[0] in ['?', '.']:
r += count(elems[1:], groups)
g = groups[0]
if len(elems) < g:
return 0
es, rest = elems[:g], elems[g:]
if all(e in ['?', '#'] for e in es) and (len(rest) == 0 or rest[0] in ['.', '?']):
r += count(rest[1:], groups[1:])
return r
def solve(lines: list[str], repeat=1):
all = []
for (_, line) in enumerate(lines):
springs, numbers = line.split()
numbers = tuple(lib.str_to_ints(numbers))
ns = "?".join([springs for _ in range(repeat)])
all.append((tuple(ns), numbers * repeat))
res = 0
for a in all:
# print(a, count(*a))
res += count(*a)
# 28:00
# 50:00 total
return res
def main():
lines = lib.str_to_lines_no_empty(EXAMPLE)
print("Example 1:", solve(lines))
lines = lib.str_to_lines_no_empty(open("d12.txt").read())
print("Solution 1:", solve(lines))
lines = lib.str_to_lines_no_empty(EXAMPLE)
print("Example 2:", solve(lines, 5))
lines = lib.str_to_lines_no_empty(open("d12.txt").read())
print("Solution 2:", solve(lines, 5))
if __name__ == "__main__":
main()

92
2023/d13.py Normal file
View File

@@ -0,0 +1,92 @@
from lib import *
EXAMPLE = """
#.##..##.
..#.##.#.
##......#
##......#
..#.##.#.
..##..##.
#.#.##.#.
#...##..#
#....#..#
..##..###
#####.##.
#####.##.
..##..###
#....#..#
""".replace("\n", "", count=1)
def solve(i: Input, second=False):
res = 0
ps = i.paras()
for p in ps:
g = Input(p).grid2()
row_res, col_res = [], []
rows = g.rows()
cols = g.cols()
for i_row in range(len(rows) - 1):
for i in range(min(i_row + 1, len(rows) - i_row - 1)):
if rows[i_row - i] != rows[i_row + i + 1]:
break
else:
if not second:
res += (i_row + 1) * 100
row_res.append(i_row)
for i_col in range(len(cols) - 1):
for i in range(min(i_col + 1, len(cols) - i_col - 1)):
if cols[i_col - i] != cols[i_col + i + 1]:
break
else:
if not second:
res += (i_col + 1)
col_res.append(i_col)
if not second:
continue
for c in g.all_coords():
g[c] = '.' if g[c] == '#' else '#'
rows = g.rows()
cols = g.cols()
for i_row in range(len(rows) - 1):
for i in range(min(i_row + 1, len(rows) - i_row - 1)):
if rows[i_row - i] != rows[i_row + i + 1]:
break
else:
if not i_row in row_res:
res += (i_row + 1) * 100
row_res.append(i_row)
for i_col in range(len(cols) - 1):
for i in range(min(i_col + 1, len(cols) - i_col - 1)):
if cols[i_col - i] != cols[i_col + i + 1]:
break
else:
if not i_col in col_res:
res += (i_col + 1)
col_res.append(i_col)
g[c] = '.' if g[c] == '#' else '#'
if len(row_res + col_res) != 2:
raise Exception("Unexpected amount of mirrors!!")
return res
def main():
DAY_INPUT = "d13.txt"
print("Example 1:", solve(Input(EXAMPLE)))
print("Solution 1:", solve(Input(DAY_INPUT)))
assert 29846 == solve(Input(DAY_INPUT))
print("Example 2:", solve(Input(EXAMPLE), True))
print("Solution 2:", solve(Input(DAY_INPUT), True))
assert 25401 == solve(Input(DAY_INPUT), True)
if __name__ == "__main__":
main()

88
2023/d14.py Normal file
View File

@@ -0,0 +1,88 @@
from lib import *
EXAMPLE = """
O....#....
O.OO#....#
.....##...
OO.#O....O
.O.....O#.
O.#..O.#.#
..O..#O..O
.......O..
#....###..
#OO..#....
"""
def get_nth_elem(xs, n):
""" Find the nth element of a repeating sequence. """
found = set()
x = None
for (i, x) in enumerate(xs):
if x in found:
break
found.add(x)
if i is None:
raise Exception("No repeating sequence.")
first_repeat = i
initial_sequence_length = xs.index(x)
repeating_sequence_length = first_repeat - xs.index(x)
adjusted_n = n - initial_sequence_length
position_in_repeating_sequence = adjusted_n % repeating_sequence_length
element = xs[initial_sequence_length + position_in_repeating_sequence]
return element
def solve(i: Input, second=False):
res = 0
g = i.grid2()
if second:
dirs = [Grid2D.N, Grid2D.W, Grid2D.S, Grid2D.E]
cycles = 1000000000
else:
dirs = [Grid2D.N]
cycles = 1
fs = {}
v = []
for i in range(cycles):
h = hash(tuple(g.find('O')))
fs[h] = tuple(g.find('O'))
v.append(h)
if v.count(h) > 1:
break
for d in dirs:
moved = True
while moved:
moved = False
for r in g.find('O'):
rn = add2(r, d)
if rn in g and g[rn] == '.':
g[r] = '.'
g[rn] = 'O'
moved = True
if second:
h = get_nth_elem(v, 1000000000)
os = fs[h]
for r, c in os:
s = g.n_rows - r
res += s
else:
for r, c in g.find('O'):
s = g.n_rows - r
res += s
return res
def main():
DAY_INPUT = "d14.txt"
print("Example 1:", solve(Input(EXAMPLE)))
print("Solution 1:", solve(Input(DAY_INPUT)))
# 5:55
print("Example 2:", solve(Input(EXAMPLE), True))
print("Solution 2:", solve(Input(DAY_INPUT), True))
# 48:00
if __name__ == "__main__":
main()

71
2023/d15.py Normal file
View File

@@ -0,0 +1,71 @@
from lib import *
from collections import OrderedDict
EXAMPLE = "rn=1,cm-,qp=3,cm=2,qp-,pc=4,ot=9,ab=5,pc-,pc=6,ot=7"
def hash(word):
# Init to 0
# Determine the ASCII code for the current character of the string.
# Increase the current value by the ASCII code you just determined. Set
# the current value to itself multiplied by 17. Set the current value to
# the remainder of dividing itself by 256.
h = 0
for c in word:
v = ord(c)
h += v
h *= 17
h %= 256
return h
def solve(input: Input, second=False):
res = 0
line = input.lines()[0]
for word in line.split(","):
h = hash(word)
res += h
if not second:
return res
boxes = [OrderedDict() for _ in range(256)]
for word in line.split(","):
h = hash(word)
if '=' in word:
label, focal = word.split('=')
box = boxes[hash(label)]
if label in box:
box[label] = (label, focal)
else:
box[label] = (label, focal)
elif '-' in word:
label = word.replace('-', '')
box = boxes[hash(label)]
if label in box:
del box[label]
else:
raise Exception()
res = 0
for i, b in enumerate(boxes):
for j, label in enumerate(b):
label, focal = b[label]
r = (i + 1) * (j + 1) * int(focal)
res += r
return res
def main():
DAY_INPUT = "d15.txt"
print("Example 1:", solve(Input(EXAMPLE)))
print("Solution 1:", solve(Input(DAY_INPUT)))
assert solve(Input(DAY_INPUT)) == 511343
# 4:30
print("Example 2:", solve(Input(EXAMPLE), True))
print("Solution 2:", solve(Input(DAY_INPUT), True))
assert solve(Input(DAY_INPUT), True) == 294474
# 31:20
return
if __name__ == "__main__":
main()

109
2023/d16.py Normal file
View File

@@ -0,0 +1,109 @@
import lib
EXAMPLE = r"""
.|...\....
|.-.\.....
.....|-...
........|.
..........
.........\
..../.\\..
.-.-/..|..
.|....-|.\
..//.|....
"""
NEW = {
'.': {
'w': (0, -1, 'w'),
'e': (0, 1, 'e'),
's': (1, 0, 's'),
'n': (-1, 0, 'n'),
},
'|': {
'w': [(1, 0, 's'), (-1, 0, 'n')],
'e': [(1, 0, 's'), (-1, 0, 'n')],
's': (1, 0, 's'),
'n': (-1, 0, 'n'),
},
'-': {
's': [(0, -1, 'w'), (0, 1, 'e')],
'n': [(0, -1, 'w'), (0, 1, 'e')],
'e': (0, 1, 'e'),
'w': (0, -1, 'w'),
},
'/': {
's': (0, -1, 'w'),
'n': (0, 1, 'e'),
'e': (-1, 0, 'n'),
'w': (1, 0, 's'),
},
'\\': {
's': (0, 1, 'e'),
'n': (0, -1, 'w'),
'e': (1, 0, 's'),
'w': (-1, 0, 'n'),
},
}
def solve(lines: list[str], second=False):
res = 0
g = list(map(list, lines))
rows = len(g)
cols = len(g[0])
if not second:
starts = [(0, 0, 'e')]
else:
starts = [(r, 0, 'e') for r in range(rows)]
starts += [(r, cols - 1, 'w') for r in range(rows)]
starts += [(0, c, 's') for c in range(cols)]
starts += [(rows - 1, c, 'n') for c in range(cols)]
for start in starts:
visited = set()
beams = [start]
while beams:
b = beams.pop()
row, col, d = b
visited.add((row, col, d))
f = g[row][col]
new = NEW[f][d]
if isinstance(new, tuple):
r, c, nd = new
nr = row + r
nc = col + c
if nr >= 0 and nc >= 0 and nr < rows and nc < cols:
if not (nr, nc, nd) in visited:
beams.append((nr, nc, nd))
else:
for r, c, nd in new:
nr = row + r
nc = col + c
if nr >= 0 and nc >= 0 and nr < rows and nc < cols:
if not (nr, nc, nd) in visited:
beams.append((nr, nc, nd))
v = set([(r, c) for (r, c, _) in list(visited)])
res = max(res, len(v))
return res
def main():
lines = lib.str_to_lines_no_empty(EXAMPLE)
print("Example 1:", solve(lines))
lines = lib.str_to_lines_no_empty(open("d16.txt").read())
print("Solution 1:", solve(lines))
lines = lib.str_to_lines_no_empty(EXAMPLE)
print("Example 2:", solve(lines, True))
lines = lib.str_to_lines_no_empty(open("d16.txt").read())
print("Solution 2:", solve(lines, True))
if __name__ == "__main__":
main()

71
2023/d17.py Normal file
View File

@@ -0,0 +1,71 @@
from lib import *
EXAMPLE = """
2413432311323
3215453535623
3255245654254
3446585845452
4546657867536
1438598798454
4457876987766
3637877979653
4654967986887
4564679986453
1224686865563
2546548887735
4322674655533
"""
def solve(i: Input, second=False):
g = i.grid2()
starts = [((0, 0), (0, None))]
def is_goal(node):
pos, _ = node
return pos == (g.n_cols - 1, g.n_rows - 1)
def neighbors(node):
pos, dirs = node
repeats, prev_dir = dirs
nbs = []
for dir in g.dirs_ort():
if second:
if repeats < 4 and prev_dir is not None and prev_dir != dir:
continue
if repeats == 10 and prev_dir == dir:
continue
else:
if repeats == 3 and prev_dir == dir:
continue
if prev_dir == g.flip_ort(dir):
continue
nb = add2(pos, dir)
if nb not in g:
continue
nbs.append((nb, (repeats + 1 if dir == prev_dir else 1, dir)))
return nbs
def h(node):
pos, _ = node
return abs(g.n_rows - 1 - pos[0]) + abs(g.n_cols - 1 - pos[1])
def d(_, b):
pos, _ = b
if pos == (0, 0):
return 0
return int(g[pos])
a = A_Star(starts, is_goal, h, d, neighbors)
return a.cost
def main():
DAY_INPUT = "d17.txt"
print("Example 1:", solve(Input(EXAMPLE)))
print("Solution 1:", solve(Input(DAY_INPUT)))
print("Example 2:", solve(Input(EXAMPLE), True))
print("Solution 2:", solve(Input(DAY_INPUT), True))
return
if __name__ == "__main__":
main()

143
2023/d18.py Normal file
View File

@@ -0,0 +1,143 @@
from lib import *
EXAMPLE = """
R 6 (#70c710)
D 5 (#0dc571)
L 2 (#5713f0)
D 2 (#d2c081)
R 2 (#59c680)
D 2 (#411b91)
L 5 (#8ceee2)
U 2 (#caa173)
L 1 (#1b58a2)
U 2 (#caa171)
R 2 (#7807d2)
U 3 (#a77fa3)
L 2 (#015232)
U 2 (#7a21e3)
"""
m = {
"R": Grid2D.E,
"D": Grid2D.S,
"L": Grid2D.W,
"U": Grid2D.N,
}
def solve(i: Input, second=False):
lines = i.lines()
ins = []
for l in lines:
if not l:
continue
d, l, c = l.split()
ins.append((int(l), m[d]))
c = (0, 0)
coords: list[tuple[int, int]] = [c]
for count, d in ins:
for _ in range(count):
c = add2(c, d)
coords.append(c)
to_visit: list[tuple[int, int]] = [(1, 1)]
while to_visit:
c = to_visit.pop()
for n in [Grid2D.S, Grid2D.E, Grid2D.W, Grid2D.N]:
nc = add2(c, n)
if nc not in coords:
coords.append(nc)
to_visit.append(nc)
return len(set(coords))
def area(ins):
c = (0, 0)
corners = []
for i, (count, d) in enumerate(ins):
c = (c[0] + count * d[0], c[1] + count * d[1])
nd = ins[(i + 1) % len(ins)][1]
if d == nd:
raise Exception("Dirs should not be equal!")
# XXX: This mapping only works when going clockwise!
match d, nd:
case Grid2D.W, Grid2D.N:
c_log = (c[0] + 1, c[1])
case Grid2D.W, Grid2D.S:
c_log = (c[0] + 1, c[1] + 1)
case Grid2D.E, Grid2D.N:
c_log = (c[0], c[1])
case Grid2D.E, Grid2D.S:
c_log = (c[0], c[1] + 1)
case Grid2D.N, Grid2D.E:
c_log = (c[0], c[1])
case Grid2D.N, Grid2D.W:
c_log = (c[0] + 1, c[1])
case Grid2D.S, Grid2D.E:
c_log = (c[0], c[1] + 1)
case Grid2D.S, Grid2D.W:
c_log = (c[0] + 1, c[1] + 1)
case d, nd:
raise Exception(f"Uncoverred {d=} -> {nd=}")
corners.append(c_log)
return int(shoelace_area(corners))
def area2(ins):
# Solution based on Shoelace area and the idea that the outside area is
# perimate * 1 // 2, and then +1 (for four quarter corners). All other
# corners cancel out to zero.
c = (0, 0)
corners = []
perimeter = 0
for i, (count, d) in enumerate(ins):
c = (c[0] + count * d[0], c[1] + count * d[1])
corners.append(c)
perimeter += count
return int(shoelace_area(corners)) + perimeter // 2 + 1
def solve2(i: Input, second=False):
lines = i.lines()
ins = []
for line in lines:
if not line:
continue
_, _, c = line.split()
c = c.replace("(#", "").replace(")", "")
d = int(c[:5], 16)
m = {"0": Grid2D.E, "1": Grid2D.S, "2": Grid2D.W, "3": Grid2D.N}[c[5]]
ins.append((d, m))
assert area(ins) == area2(ins)
return area(ins)
def debug():
ins = [
( 3, Grid2D.S,),
( 2, Grid2D.E,),
( 1, Grid2D.N,),
( 1, Grid2D.E,),
( 1, Grid2D.N,),
( 1, Grid2D.W,),
( 1, Grid2D.N,),
( 2, Grid2D.W,),
]
# Should be 14 but is 2 because it's going counter-clockwise, but mapping
# only works for clockwise.
print(area(ins))
def main():
DAY_INPUT = "d18.txt"
print("Example 1:", solve(Input(EXAMPLE)))
print("Solution 1:", solve(Input(DAY_INPUT)))
print("Example 2:", solve2(Input(EXAMPLE), True))
print("Solution 2:", solve2(Input(DAY_INPUT), True))
return
if __name__ == "__main__":
main()

148
2023/d19.py Normal file
View File

@@ -0,0 +1,148 @@
from lib import *
EXAMPLE = """px{a<2006:qkq,m>2090:A,rfg}
pv{a>1716:R,A}
lnx{m>1548:A,A}
rfg{s<537:gd,x>2440:R,A}
qs{s>3448:A,lnx}
qkq{x<1416:A,crn}
crn{x>2662:A,R}
in{s<1351:px,qqz}
qqz{s>2770:qs,m<1801:hdj,R}
gd{a>3333:R,R}
hdj{m>838:A,pv}
{x=787,m=2655,a=1222,s=2876}
{x=1679,m=44,a=2067,s=496}
{x=2036,m=264,a=79,s=2244}
{x=2461,m=1339,a=466,s=291}
{x=2127,m=1623,a=2188,s=1013}
"""
def solve(i: Input, second=False):
res = 0
ps = i.paras()
wfs, parts = ps
wfs = wfs.splitlines()
parts = parts.splitlines()
workflows = {}
for w in wfs:
name, ins = w.split("{")
ins = ins.replace("}", "")
conds = ins.split(",")
wf = []
for cond in conds[:-1]:
cmp, n = cond.split(":")
if '<' in cmp:
a, b = cmp.split('<')
wf.append((a, 'smaller', int(b), n))
elif '>' in cmp:
a, b = cmp.split('>')
wf.append((a, 'greater', int(b), n))
else:
raise Exception()
wf.append(conds[-1:])
workflows[name] = wf
def parse_part(part):
d = {}
p = part.replace("{", "").replace("}", "")
for pair in p.split(","):
a, b = pair.split("=")
d[a] = int(b)
return d
parts = list(map(parse_part, parts))
if not second:
for p in parts:
current = 'in'
while current not in ['A', 'R']:
for inst in workflows[current]:
if len(inst) == 4:
letter = inst[0]
value = inst[2]
next = inst[3]
if inst[1] == 'smaller':
if p[letter] < value:
current = next
break
elif inst[1] == 'greater':
if p[letter] > value:
current = next
break
else:
raise Exception()
elif len(inst) == 1:
current = inst[0]
else:
raise Exception()
if current == 'A':
r = sum(p.values())
res += r
return res
ranges = [{c: (1, 4000) for c in 'xmas'}]
ranges[0]['cur'] = 'in'
ranges[0]['idx'] = 0
accepted = []
while ranges:
r = ranges.pop()
cur, idx = r['cur'], r['idx']
if cur == 'A':
accepted.append(r)
continue
elif cur == 'R':
continue
inst = workflows[cur][idx]
if len(inst) == 4:
letter = inst[0]
value = inst[2]
nxt = inst[3]
ro = r[letter]
r1, r2 = dict(r), dict(r)
if inst[1] == 'smaller':
r1[letter] = (ro[0], value - 1)
r1['idx'] = 0
r1['cur'] = nxt
r2[letter] = (value, ro[1])
r2['idx'] += 1
elif inst[1] == 'greater':
r1[letter] = (ro[0], value)
r1['idx'] += 1
r2[letter] = (value + 1, ro[1])
r2['idx'] = 0
r2['cur'] = nxt
if r1[letter][1] >= r1[letter][0]:
ranges.append(r1)
if r2[letter][1] >= r2[letter][0]:
ranges.append(r2)
elif len(inst) == 1:
r['cur'] = inst[0]
r['idx'] = 0
ranges.append(r)
res = 0
for a in accepted:
r = 1
for c in 'xmas':
l, u = a[c]
r *= (u + 1 - l)
res += r
return res
def main():
DAY_INPUT = "d19.txt"
print("Example 1:", solve(Input(EXAMPLE)))
print("Solution 1:", solve(Input(DAY_INPUT)))
# 25:00
print("Example 2:", solve(Input(EXAMPLE), True))
assert solve(Input(EXAMPLE), True) == 167409079868000
print("Solution 2:", solve(Input(DAY_INPUT), True))
# 120:00
if __name__ == "__main__":
main()

66
2023/d2.py Normal file
View File

@@ -0,0 +1,66 @@
import re
GAME1 = """
Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green
Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue
Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red
Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red
Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green
"""
"""
he bag contained only 12 red cubes, 13 green cubes, and 14 blue cubes?
"""
MAX = {
"red": 12,
"green": 13,
"blue": 14,
}
def solve(text):
r = re.compile(r"(\d+) (\w+)")
r2 = re.compile(r"Game (\d+):")
s = 0
for line in text.splitlines():
if line.strip() == "":
continue
id = r2.match(line).groups()[0]
m = r.findall(line)
for n, c in m:
if int(n) > MAX[c]:
break
else:
s += int(id)
return s
def solve2(text):
r = re.compile(r"(\d+) (\w+)")
s = 0
for line in text.splitlines():
max = {
"red": 0,
"green": 0,
"blue": 0,
}
if line.strip() == "":
continue
m = r.findall(line)
for n, c in m:
n = int(n)
if n > max[c]:
max[c] = int(n)
s += (max["red"] * max["green"] * max["blue"])
return s
if __name__ == "__main__":
assert(solve(GAME1) == 8)
assert(solve2(GAME1) == 2286)
# 10:55
with open("d2.txt", 'r') as f:
text = f.read()
print(solve(text))
print(solve2(text))

109
2023/d20.py Normal file
View File

@@ -0,0 +1,109 @@
from lib import *
from math import lcm
from collections import deque
EXAMPLE = """
broadcaster -> a, b, c
%a -> b
%b -> c
%c -> inv
&inv -> a
"""
EXAMPLE2 = """
broadcaster -> a
%a -> inv, con
&inv -> b
%b -> con
&con -> output
"""
def visualize(modules):
with open("g20.dot", 'w') as f:
f.write("digraph G {\n")
for m in modules.values():
for cm in m[3]:
f.write(" " + m[1] + ' -> ' + cm + "\n")
f.write("}")
def solve(input: Input, second=False):
modules = {}
for line in input.lines():
if not line: continue
src, dsts = line.split(" -> ")
dsts = dsts.split(", ")
if src.startswith("%"): # flip-flop
modules[src[1:]] = [src[0], src[1:], 0, dsts]
elif src.startswith("&"): # conjunction
modules[src[1:]] = [src[0], src[1:], {}, dsts]
elif src.startswith("broadcaster"):
modules[src] = ["b", src, 0, dsts]
else:
raise Exception()
for m in modules.values():
for d in m[3]:
if d in modules and modules[d][0] == "&":
modules[d][2][m[1]] = 0
if second:
# visualize(modules)
(feed,) = [m[1] for m in modules.values() if "rx" in m[3]]
periods = {d: [] for d in modules[feed][2].keys()}
BUTTON_PUSHES = 10000
else:
BUTTON_PUSHES = 1000
lo, hi = 0, 0
for i in range(BUTTON_PUSHES):
qs = deque([("button module", "broadcaster", 0)])
while qs:
src, dst, sig = qs.popleft()
if sig == 0:
lo += 1
else:
hi += 1
if not dst in modules:
continue
m = modules[dst]
new_pulse = None
if m[0] == "b":
new_pulse = 0
elif m[0] == "%":
if sig == 0:
m[2] = (m[2] + 1) % 2
new_pulse = m[2]
elif m[0] == "&":
m[2][src] = sig
if all(s == 1 for s in m[2].values()):
new_pulse = 0
else:
new_pulse = 1
else:
raise Exception()
if new_pulse is not None:
for nxtdst in m[3]:
if second and nxtdst == feed and new_pulse == 1:
# print(f"{i:>4}: {dst[:4]:>4} -{new_pulse}> {nxtdst}")
periods[dst].append(i)
qs.append((dst, nxtdst, new_pulse))
if second:
# print(periods)
# Yeah, we got a bit lucky that this works, I guess, but it does.
return lcm(*[v[1] - v[0] for v in periods.values()])
return lo * hi
def main():
DAY_INPUT = "d20.txt"
print("Example 1:", solve(Input(EXAMPLE)))
print("Example 2:", solve(Input(EXAMPLE2)))
print("Solution 1:", solve(Input(DAY_INPUT)))
print("Solution 2:", solve(Input(DAY_INPUT), True))
assert solve(Input(DAY_INPUT), True) == 244178746156661
if __name__ == "__main__":
main()

213
2023/d21.py Normal file
View File

@@ -0,0 +1,213 @@
from lib import *
import os
EXAMPLE = """...........
.....###.#.
.###.##..#.
..#.#...#..
....#.#....
.##..S####.
.##..#...#.
.......##..
.##.#.####.
.##..##.##.
...........
"""
def solve(input: Input):
g = input.grid2()
s = g.find('S')[0]
g[s] = 'O'
steps = 64
seen = set()
for i in range(steps):
os = tuple(g.find('O'))
if os in seen:
seen.add(os)
print(f"SEEN {i}")
break
for o in os:
g[o] = '.'
for o in os:
for nb in g.neighbors_ort(o):
if not g[nb] == "#":
g[nb] = 'O'
return len(g.find('O'))
def plot(xs, poss):
os.system("clear")
rcoords = [x[0] for x in xs]
ccoords = [x[1] for x in xs]
rmin = min(rcoords)
rmax = max(rcoords)
cmin = min(ccoords)
cmax = max(ccoords)
for r in range(rmin, rmax + 1):
s = ""
for c in range(cmin, cmax + 1):
if (r, c) in xs:
s += "#"
elif (r, c) in poss:
s += "O"
else:
s += " "
print(s)
def move(xs, roff, coff):
rcoords = [x[0] for x in xs]
ccoords = [x[1] for x in xs]
rd = max(rcoords) - min(rcoords) + 3
cd = max(ccoords) - min(ccoords) + 3
newxs = [(x[0] + roff * rd, x[1] + coff * cd) for x in xs]
return set(newxs)
def iter(poss, stones):
nposs = set()
for r, c in poss:
for ro, co in [(-1, 0), (0, 1), (1, 0), (0, -1)]:
nr, nc = r + ro, c + co
if not (nr, nc) in stones:
nposs.add((nr, nc))
return nposs
def get_bounds(size, ro, co):
rmin = size * ro
rmax = size + size * ro
cmin = size * co
cmax = size + size * co
return rmin, rmax, cmin, cmax
def count(poss, size, ro, co):
rmin, rmax, cmin, cmax = get_bounds(size, ro, co)
res = 0
for (r, c) in poss:
if (rmin <= r < rmax) and (cmin <= c < cmax):
res += 1
return res
def solve2(ip: Input):
base_stones = set()
poss = set()
size = len(ip.lines())
assert size == len(ip.lines()[0])
for r, row in enumerate(ip.lines()):
for c, col in enumerate(row):
if col == "#":
base_stones.add((r, c))
if col == "S":
poss.add((r, c))
stones = base_stones.copy()
off = 19 // 2
for ro in range(-off, off + 1):
for co in range(-off, off + 1):
stones |= move(base_stones, ro, co)
hists = {}
for ro in range(-off, off + 1):
for co in range(-off, off + 1):
hists[(ro, co)] = []
#for step in range(590):
# if step % 1 == 0:
# sanity = 0
# os.system("clear")
# for ro in range(-off, off + 1):
# s = ""
# for co in range(-off, off + 1):
# v = count(poss, size, ro, co)
# sanity += v
# if v > 0:
# hists[(ro, co)].append(v)
# s += f"{v:6}"
# else:
# s += 6 * " "
# print(s)
# # input(f"{step=} {step//size=} {len(poss)} ({sanity}) cont...")
# print(f"{step=} {step//size=} {len(poss)} ({sanity}) cont...")
# poss = iter(poss, stones)
# 66, 197, 328 459 # cycle starts
# 196, 327, 458, 589 # targets
def calc(len, xs):
if len % 2 == 0:
return len // 2 * sum(xs)
else:
return len // 2 * sum(xs) + xs[0]
target = 196
target = 327
target = 458
target = 589
target = 26501365
# for target in [196, 327, 458, 589]:
print()
print(target)
cycle = 131
c = target // cycle
d = (target // cycle) * 2 + 1 - 2
print(f"{c=} {d=}")
res = 0
res += 5698 + 5703 + 5709 + 5704 # corners
res += c * 964 + c * 984 + c * 968 + c * 978 # outer
res += (c - 1) * 6637 + (c - 1) * 6624 + (c - 1) * 6643 + (c - 1) * 6619 # inner
for i in range(d, 0, -2):
res += calc(i, [7623, 7558])
for i in range(d - 2, 0, -2):
res += calc(i, [7623, 7558])
print(res)
return res
# def get_till(xs, ts):
# ts = ts[:]
# r = []
# for x in xs:
# r.append(x)
# if x in ts:
# ts.remove(x)
# if ts == []:
# break
# return r
osz_values = hists[(0, 4)][-2:]
# se = get_till(hists[0, 5], osz_values)
# sn = get_till(hists[-5, 0], osz_values)
# ss = get_till(hists[5, 0], osz_values)
# sw = get_till(hists[0, -5], osz_values)
# print(se)
# print(sn)
# print(sw)
# print(ss)
# sne = get_till(hists[-5, 5], osz_values)
# sse = get_till(hists[5, 5], osz_values)
# ssw = get_till(hists[5, -5], osz_values)
# snw = get_till(hists[-5, -5], osz_values)
# print(sne)
# print(sse)
# print(ssw)
# print(snw)
# for i in range(3, 10):
# print(hists[(0, i)][:5])
# print(hists[(0, -i)][:5])
def main():
DAY_INPUT = "d21.txt"
# print("Example 1:", solve(Input(EXAMPLE)))
# print("Solution 1:", solve(Input(DAY_INPUT)))
# print("Example 2:", solve2(Input(EXAMPLE)))
print("Solution 2:", solve2(Input(DAY_INPUT)))
if __name__ == "__main__":
main()

121
2023/d22.py Normal file
View File

@@ -0,0 +1,121 @@
from lib import *
EXAMPLE = """1,0,1~1,2,1
0,0,2~2,0,2
0,2,3~2,2,3
0,0,4~0,2,4
2,0,5~2,2,5
0,1,6~2,1,6
1,1,8~1,1,9
"""
def overlap(a, b):
# Could be made generic with for loop.
[(ax1, ax2), (ay1, ay2), (az1, az2)] = a
[(bx1, bx2), (by1, by2), (bz1, bz2)] = b
xo = (bx1 <= ax2 and bx2 >= ax1)
yo = (by1 <= ay2 and by2 >= ay1)
zo = (bz1 <= az2 and bz2 >= az1)
return xo and yo and zo
def can_overlap(a, b):
a, b = list(a), list(b)
za1, za2 = a[2]
zb1, zb2 = b[2]
zad = za1 - 1
a[2] = (za1 - zad, za2 - zad)
zbd = zb1 - 1
b[2] = (zb1 - zbd, zb2 - zbd)
return overlap(a, b)
def solve(input: Input, second=False):
bricks = []
for line in input.lines():
s = tuple(str_to_ints(line))
s = tuple([tuple(sorted([s[i], s[i + 3]])) for i in range(3)])
bricks.append(s)
# Check which bricks can overlap in general
d = {i: [] for i in range(len(bricks))}
for a in range(len(bricks)):
for b in range(a + 1, len(bricks)):
if can_overlap(bricks[a], bricks[b]):
# print(f"{bricks[a]} can overlap with {bricks[b]}")
d[a].append(b)
d[b].append(a)
# Lower bricks as much as possible
idxs = sorted(range(len(bricks)), key=lambda i: bricks[i][2][1])
for idx_active_idx, idx_active in enumerate(idxs):
b = list(bricks[idx_active])
lowest_z = 1
for idx_to_check in idxs[:idx_active_idx]:
if idx_to_check in d[idx_active]:
lowest_z = max(lowest_z, bricks[idx_to_check][2][1] + 1)
zp = list(b[2])
zp[0], zp[1] = lowest_z, zp[1] - (zp[0] - lowest_z)
b[2] = tuple(zp)
# print(f"{bricks[idx_active]} -> {b}")
bricks[idx_active] = b
# for l, b in zip(LETTERS_UPPER, bricks): print(l, b)
if second:
# Create a map that for each objects, shows what objects it is
# supported by.
supported_by = {i: set() for i in range(len(bricks))}
for i in range(len(bricks)):
b = bricks[i]
zl = b[2][0]
if zl == 1:
supported_by[i].add(-1)
for ni in d[i]:
if bricks[ni][2][1] + 1 == zl:
supported_by[i].add(ni)
res = 0
for i in range(len(bricks)):
removed = set([i])
to_process = [i]
while to_process:
ri = to_process.pop()
for ni in d[ri]:
if supported_by[ni].issubset(removed) and not ni in removed:
removed.add(ni)
to_process.append(ni)
res += len(removed) - 1
return res
else:
support_map = {i: [] for i in range(len(bricks))}
support_bricks = set()
# For all bricks, check if it would fall if a particular brick was removed.
for i in range(len(bricks)):
b = bricks[i]
zl = b[2][0]
if zl == 1:
continue # supported by floor
for ni in d[i]:
if bricks[ni][2][1] + 1 == zl:
support_map[i].append(ni)
if len(support_map[i]) == 1:
support_bricks.add(support_map[i][0])
# print(f"{bricks[i]} supported by {support_map[i]}")
return len(bricks) - len(support_bricks)
def main():
DAY_INPUT = "d22.txt"
print("Example 1:", solve(Input(EXAMPLE)))
print("Solution 1:", solve(Input(DAY_INPUT)))
assert solve(Input(DAY_INPUT)) == 428
print("Example 2:", solve(Input(EXAMPLE), True))
print("Solution 2:", solve(Input(DAY_INPUT), True))
assert solve(Input(DAY_INPUT), True) == 35654
if __name__ == "__main__":
main()

146
2023/d23.py Normal file
View File

@@ -0,0 +1,146 @@
from lib import *
from collections import deque
EXAMPLE = """#.#####################
#.......#########...###
#######.#########.#.###
###.....#.>.>.###.#.###
###v#####.#v#.###.#.###
###.>...#.#.#.....#...#
###v###.#.#.#########.#
###...#.#.#.......#...#
#####.#.#.#######.#.###
#.....#.#.#.......#...#
#.#####.#.#.#########v#
#.#...#...#...###...>.#
#.#.#v#######v###.###v#
#...#.>.#...>.>.#.###.#
#####v#.#.###v#.#.###.#
#.....#...#...#.#.#...#
#.#########.###.#.#.###
#...###...#...#...#.###
###.###.#.###v#####v###
#...#...#.#.>.>.#.>.###
#.###.###.#.###.#.#v###
#.....###...###...#...#
#####################.#
"""
SLOPES = {
"^": (-1, 0),
">": (0, 1),
"v": (1, 0),
"<": (0, -1),
}
def first(input):
g = input.grid2()
start = (0, 1)
end = (g.n_rows - 1, g.n_cols - 2)
longest = 0
paths = [(set([start]), start)]
while True:
new_paths = []
for p in paths:
hist, pos = p
for d in g.COORDS_ORTH:
nb = add2(pos, d)
if nb[0] < 0 or nb[0] >= g.n_rows or nb[1] < 0 or nb[1] >= g.n_cols:
continue
c = g[nb]
if c in SLOPES.keys() and d != SLOPES[c]:
continue
if c == "#" or nb in hist:
continue
if nb == end:
l = len(hist)
if l > longest:
longest = l
continue
nhist = hist.copy()
nhist.add(nb)
new_paths.append((nhist, nb))
paths = new_paths
if len(paths) == 0:
break
return longest
def solve(input: Input, second=False):
if not second:
return first(input)
g = input.grid2()
start = (0, 1)
end = (g.n_rows - 1, g.n_cols - 2)
seen = set()
q = deque([[start, (1, 1)]])
# The intuition is that we can brute force much quicker if we have a pure
# graph instead of following the maze along the whole time. So, we create
# a graph from the maze and then brute force on the maze.
sg = {start: set()} # {node: {(node, dist), ...}}
while q:
trail = q.popleft()
pos = trail[-1]
while True:
nbs = []
for d in g.COORDS_ORTH:
nb = add2(pos, d)
if nb[0] < 0 or nb[0] >= g.n_rows or nb[1] < 0 or nb[1] >= g.n_cols:
continue
if g[nb] == "#" or nb == trail[-2]:
continue
nbs.append(nb)
if len(nbs) == 1:
pos = nbs[0]
trail.append(pos)
else:
break
if not pos in sg:
sg[pos] = set()
dist = len(trail) - 1
sg[trail[0]].add((pos, dist))
sg[pos].add((trail[0], dist))
seen.add(pos)
for nb in nbs:
if not nb in seen:
seen.add(nb)
q.append([pos, nb])
# for key, value in sg.items():
# print(key, value)
# Brute force in bf order.
longest = 0
q = deque([(set(), start, 0)])
while q:
hist, pos, dist = q.popleft()
if pos == end:
if dist > longest:
longest = dist
continue
for nb, d in sg[pos]:
if nb in hist:
continue
nhist = hist.copy()
nhist.add(nb)
q.append((nhist, nb, dist + d))
return longest
def main():
DAY_INPUT = "d23.txt"
# print("Example 1:", solve(Input(EXAMPLE)))
# print("Solution 1:", solve(Input(DAY_INPUT)))
print("Example 2:", solve(Input(EXAMPLE), True))
print("Solution 2:", solve(Input(DAY_INPUT), True))
if __name__ == "__main__":
main()

74
2023/d24.py Normal file
View File

@@ -0,0 +1,74 @@
from lib import *
import sympy as sp
EXAMPLE = """19, 13, 30 @ -2, 1, -2
18, 19, 22 @ -1, -1, -2
20, 25, 34 @ -2, -2, -4
12, 31, 28 @ -1, -2, -1
20, 19, 15 @ 1, -5, -3
"""
def solve1(input: Input):
if len(input.lines()) == 5:
lb = 7
ub = 27
else:
lb = 200000000000000
ub = 400000000000000
# On paper:
# (px - sx1) / vx1 = (py - sy1) / vy1
# (px - sx1) * vy1 = (py - sy1) * vx1
# (px - sx1) * vy1 - (py - sy1) * vx1 = 0
res = 0
eqs = [str_to_ints(l) for l in input.lines()]
for i, eq1 in enumerate(eqs):
for eq2 in eqs[:i]:
sx1, sy1, _, vx1, vy1, _ = eq1
sx2, sy2, _, vx2, vy2, _ = eq2
px, py = sp.symbols("px py")
es = [
vy1 * (px - sx1) - vx1 * (py - sy1),
vy2 * (px - sx2) - vx2 * (py - sy2),
]
r = sp.solve(es)
if not r:
continue
x, y = r[px], r[py]
if lb <= x <= ub and lb <= y < ub:
t1 = (x - sx1) / vx1
t2 = (x - sx2) / vx2
if (t1 > 0 and t2 > 0):
res += 1
return res
def solve2(input: Input):
eqs = [str_to_ints(l) for l in input.lines()]
px, py, pz, vxo, vyo, vzo = sp.symbols("px py pz vxo vyo vzo")
es = []
# The first six equations are enough to find a solution for my problem set.
# Might have to be increased depending on input.
for i, (x, y, z, vx, vy, vz) in enumerate(eqs[:6]):
t = sp.symbols(f"t{i}")
es.append(px + vxo * t - x - vx * t)
es.append(py + vyo * t - y - vy * t)
es.append(pz + vzo * t - z - vz * t)
r = sp.solve(es)[0]
return r[px] + r[py] + r[pz]
def main():
DAY_INPUT = "d24.txt"
print("Solution 1:", solve1(Input(EXAMPLE)))
print("Solution 1:", solve1(Input(DAY_INPUT)))
print("Example 2:", solve2(Input(EXAMPLE)))
print("Solution 2:", solve2(Input(DAY_INPUT)))
return
if __name__ == "__main__":
main()

87
2023/d25.py Normal file
View File

@@ -0,0 +1,87 @@
from lib import *
from random import choice
from collections import deque
# def plot(graph):
# import networkx as nx
# import matplotlib
# import matplotlib.pyplot as plt
# G = nx.Graph()
# for node, connected_nodes in graph.items():
# for connected_node in connected_nodes:
# G.add_edge(node, connected_node)
# # pos = nx.spring_layout(G, k=2.0, iterations=20) # Adjust k as needed
# pos = nx.shell_layout(G)
# nx.draw(G, with_labels=True, node_color='lightblue', edge_color='gray', node_size=2000, font_size=15, font_weight='bold')
# matplotlib.use('qtagg')
# plt.show()
def solve(input: Input):
graph = {}
edges = {}
for line in input.lines():
src, dsts = line.split(":")
dsts = dsts.strip().split(" ")
if not src in graph:
graph[src] = []
for dst in dsts:
graph[src].append(dst)
if not dst in graph:
graph[dst] = []
graph[dst].append(src)
edge = tuple(sorted([src, dst]))
edges[edge] = 0
for _ in range(100):
first_node = choice(list(graph.keys()))
seen = set([first_node])
visit = deque([first_node])
while visit:
node = visit.popleft()
for nb in graph[node]:
if not nb in seen:
seen.add(nb)
visit.append(nb)
edge = tuple(sorted([node, nb]))
edges[edge] += 1
# Orignally, I used `plot(graph)` to visually find the nodes that have to
# be removed. I then came up with this heuristic approach. The idea is that
# we have to cross one of the three nodes when we do a breadth first
# search. By repeatedly doing that we can identify the "bridges" as the
# three edges that are used the most often.
most_visited = sorted(edges.items(), key=lambda t: t[1], reverse=True)[:3]
# to_remove = (("plt", "mgb"), ("jxm", "qns"), ("dbt", "tjd")) # found visually
# for node, count in most_visited:
# print(node, count) # should print the same as `to_remove`
for (a, b), _ in most_visited:
graph[a].remove(b)
graph[b].remove(a)
to_visit = [choice(list(graph.keys()))]
seen = set(to_visit)
while to_visit:
node = to_visit.pop()
for nb in graph[node]:
if not nb in seen:
seen.add(nb)
to_visit.append(nb)
return len(seen) * (len(graph) - len(seen))
def main():
DAY_INPUT = "d25.txt"
print("Solution 1:", solve(Input(DAY_INPUT)), "(hands-free)")
if __name__ == "__main__":
main()

139
2023/d3.py Normal file
View File

@@ -0,0 +1,139 @@
import re
from lib import *
EXAMPLE = """
467..114..
...*......
..35..633.
......#...
617*......
.....+.58.
..592.....
......755.
...$.*....
.664.598..
"""
def solve(i: Input, second=False):
"""
This is a new version I have implemented after the fact to test the
improvements to my library. My original versions are `solve1` and `solve2`.
"""
g = i.grid2()
if not second:
parts = g.find_not(NUMBERS + ".")
else:
parts = g.find("*")
res = 0
numbers = []
for p in parts:
numbers_gear = []
for n in g.neighbors_adj(p):
if g[n] in NUMBERS:
while n in g and g[n] in NUMBERS:
n = (n[0], n[1] - 1)
number = ""
n = (n[0], n[1] + 1)
start = n
while n in g and g[n] in NUMBERS:
number += g[n]
n = (n[0], n[1] + 1)
numbers.append((int(number), n[0], start))
numbers_gear.append(int(number))
numbers_gear = list(set(numbers_gear))
if len(numbers_gear) == 2:
res += numbers_gear[0] * numbers_gear[1]
if second:
return res
else:
return sum([n for n, _, _ in list(set(numbers))])
def clean(text: str) -> list[str]:
return list(filter(lambda l: l.strip() != "", text.splitlines()))
def is_adj_to_symbol(x, y, lines):
non_symbol = re.compile(r"[^0-9\.]")
for xo, yo in [(-1, -1), (-1, 0), (-1, 1), (0, -1), (0, 1), (1, -1), (1, 0), (1, 1)]:
try:
if x + xo < 0 or y + yo < 0:
continue
if non_symbol.match(lines[y + yo][x + xo]):
return True
except IndexError:
pass
return False
def solve1(lines: list[str]):
d = ""
adj_to_symbol = False
s = 0
for (y, line) in enumerate(lines):
for (x, c) in enumerate(line):
if c not in NUMBERS:
if len(d) > 0 and adj_to_symbol:
s += int(d)
d = ""
adj_to_symbol = False
else:
if is_adj_to_symbol(x, y, lines):
adj_to_symbol = True
d = d + c
return s
def get_entire_number(x, y, lines):
assert(lines[y][x] in NUMBERS)
x_start = x - 1
while lines[y][x_start] in NUMBERS and x_start >= 0:
x_start -= 1
x_start += 1
x_start_orig = x_start
r = ""
while x_start < len(lines[0]) and lines[y][x_start] in NUMBERS:
r += lines[y][x_start]
x_start += 1
return x_start_orig, int(r)
def find_sourrounding_numbers(x, y, lines):
full_coords = []
numbers = []
for xo, yo in [(-1, -1), (0, -1), (1, -1), (-1, 0), (1, 0), (-1, 1), (0, 1), (1, 1)]:
new_x = x + xo
new_y = y + yo
if new_x < 0 or new_y < 0 or new_x >= len(lines[0]) or new_y >= len(lines):
continue
if lines[new_y][new_x] in NUMBERS:
x_start_orig, n = get_entire_number(new_x, new_y, lines)
if (x_start_orig, new_y, n) not in full_coords:
numbers.append(n)
full_coords.append((x_start_orig, y + yo, n))
return numbers
def solve2(lines: list[str]):
s = 0
for (y, line) in enumerate(lines):
for (x, c) in enumerate(line):
if c == '*':
numbers = find_sourrounding_numbers(x, y, lines)
if len(numbers) == 2:
# print(numbers)
s += numbers[0] * numbers[1]
return s
def main():
input = Input(EXAMPLE)
print("Example 1:", solve(input))
input = Input("d3.txt")
print("Solution 1:", solve(input))
input = Input(EXAMPLE)
print("Example 2:", solve(input, True))
input = Input("d3.txt")
print("Solution 2:", solve(input, True))
return
if __name__ == "__main__":
main()

69
2023/d4.py Normal file
View File

@@ -0,0 +1,69 @@
from string import ascii_lowercase, ascii_uppercase, digits
import re
EXAMPLE = """
Card 1: 41 48 83 86 17 | 83 86 6 31 17 9 48 53
Card 2: 13 32 20 16 61 | 61 30 68 82 17 32 24 19
Card 3: 1 21 53 59 44 | 69 82 63 72 16 21 14 1
Card 4: 41 92 73 84 69 | 59 84 76 51 58 5 54 83
Card 5: 87 83 26 28 32 | 88 30 70 12 93 22 82 36
Card 6: 31 18 13 56 72 | 74 77 10 23 35 67 36 11
"""
def clean(text: str) -> list[str]:
return list(filter(lambda l: l.strip() != "", text.splitlines()))
def solve(lines: list[str]):
res = 0
for (i, line) in enumerate(lines):
c, s = line.split(":")
w, y = s.split("|")
w = set([int(c) for c in w.strip().split()])
y = set([int(c) for c in y.strip().split()])
z = w.intersection(y)
if z:
ps = 1
else:
ps = 0
for _ in range(len(z) - 1):
ps *= 2
res += ps
return res
def solve2(lines: list[str]):
res = 0
matches = []
for (i, line) in enumerate(lines):
c, s = line.split(":")
w, y = s.split("|")
w = set([int(c) for c in w.strip().split()])
y = set([int(c) for c in y.strip().split()])
z = w.intersection(y)
matches.append(len(z))
scratchcards = list(range(len(matches)))
while scratchcards:
sc = scratchcards.pop()
res += 1
for i in range(matches[sc]):
if sc + i + 1 < len(matches):
scratchcards.append(sc + i + 1)
return res
def main():
example = clean(EXAMPLE)
print("Example 1:", solve(example))
data = clean(open("d4.txt").read())
print("Solution 1:", solve(data))
example = clean(EXAMPLE)
print("Example 2:", solve2(example))
data = clean(open("d4.txt").read())
print("Solution 2:", solve2(data))
return
if __name__ == "__main__":
main()

163
2023/d5.py Normal file
View File

@@ -0,0 +1,163 @@
EXAMPLE = """
seeds: 79 14 55 13
seed-to-soil map:
50 98 2
52 50 48
soil-to-fertilizer map:
0 15 37
37 52 2
39 0 15
fertilizer-to-water map:
49 53 8
0 11 42
42 0 7
57 7 4
water-to-light map:
88 18 7
18 25 70
light-to-temperature map:
45 77 23
81 45 19
68 64 13
temperature-to-humidity map:
0 69 1
1 0 69
humidity-to-location map:
60 56 37
56 93 4
"""
def clean(text: str) -> list[str]:
return list(text.splitlines())
def solve(lines: list[str]):
seeds = []
idx = -1
maps = []
for (_, line) in enumerate(lines):
if line.startswith("seeds:"):
seeds = map(int, line.replace("seeds:", "").strip().split(" "))
continue
if line.strip() == "":
continue
if "map" in line:
maps.append([])
idx += 1
continue
else:
maps[idx].append(list(map(int, line.split(" "))))
locs = []
for current in seeds:
for m in maps:
for mapping in m:
dest_range_start, source_range_start, range_len = mapping
if current >= source_range_start and current < source_range_start + range_len:
current = dest_range_start + (current - source_range_start)
break
else:
current = current
locs.append(current)
return min(locs)
def solve2(lines: list[str]):
seeds = []
idx = -1
maps = []
for (_, line) in enumerate(lines):
if line.startswith("seeds:"):
all_seeds = []
seeds = list(map(int, line.replace("seeds:", "").strip().split(" ")))
for i in range(0, len(seeds), 2):
all_seeds.append(seeds[i:i+2])
seeds = all_seeds
continue
if line.strip() == "":
continue
if "map" in line:
maps.append([])
idx += 1
continue
else:
maps[idx].append(list(map(int, line.split(" "))))
# source: |---------------------|
# seeds:
# |---|
# aaaaaabbbb
# aaaaaabbbbbbbbbbbbbbbbbbbbbbcccc
# aaaaaaaaaaaaaaaa
# aaaaaaaaaaaaaaaaaaaccccc
# |-------|
for m in maps:
next_seeds = []
while seeds:
seed_start, seed_range = seeds.pop()
seed_end = seed_start + seed_range
for mapping in m:
dest_start, source_start, m_range = mapping
source_end = source_start + m_range
if seed_end <= source_start:
continue
elif seed_start < source_start and seed_end <= source_end:
a_range = source_start - seed_start
seeds.append([seed_start, a_range])
b_range = seed_end - source_start
next_seeds.append([dest_start, b_range])
break
elif seed_start < source_start and seed_end > source_end:
a_range = source_start - seed_start
seeds.append([seed_start, a_range])
next_seeds.append([dest_start, m_range]) # b range
c_range = seed_end - source_end
next_seeds.append([source_end, c_range])
break
elif seed_start >= source_start and seed_end <= source_end:
new_seed_start = dest_start + (seed_start - source_start)
next_seeds.append([new_seed_start, seed_range])
break
elif seed_start >= source_start and seed_start < source_end:
new_seed_start = dest_start + (seed_start - source_start)
a_range = source_end - seed_start
next_seeds.append([new_seed_start, a_range])
c_range = seed_end - source_end
seeds.append([source_end, c_range])
break
elif seed_start >= source_end:
continue
else:
print(f"{seed_start=} {seed_range=} {seed_end=}")
print(f"{source_start=} {source_end=}")
raise Exception("Unexpected case")
else:
next_seeds.append([seed_start, seed_range])
seeds = next_seeds
return min(seeds)[0]
def main():
example = clean(EXAMPLE)
print("Example 1:", solve(example))
data = clean(open("d5.txt").read())
print("Solution 1:", solve(data))
example = clean(EXAMPLE)
print("Example 2:", solve2(example))
data = clean(open("d5.txt").read())
print("Solution 2:", solve2(data))
assert(solve2(data) == 63179500)
if __name__ == "__main__":
main()

57
2023/d6.py Normal file
View File

@@ -0,0 +1,57 @@
EXAMPLE = """
Time: 7 15 30
Distance: 9 40 200
"""
def str_to_int_lst(s):
return list(map(int, s.split()))
def clean(text: str) -> list[str]:
return list(filter(lambda l: l.strip() != "", text.splitlines()))
def solve(lines: list[str]):
res = 1
t = []
for (_, line) in enumerate(lines):
numbers = str_to_int_lst(line.replace("Time:", "").replace("Distance:", "").strip())
t.append(numbers)
t = list(zip(*t))
for time, dist in t:
opt = 0
for press_time in range(1, time):
dist_trav = press_time * (time - press_time)
if dist_trav > dist:
opt += 1
res *= opt
return res
def solve2(lines: list[str]):
t = []
for (_, line) in enumerate(lines):
numbers = str_to_int_lst(line.replace("Time:", "").replace("Distance:", "").replace(" ", "").strip())
t.append(numbers[0])
time, dist = t
opt = 0
for press_time in range(1, time):
dist_trav = press_time * (time - press_time)
if dist_trav > dist:
opt += 1
return opt
def main():
example = clean(EXAMPLE)
print("Example 1:", solve(example))
data = clean(open("d6.txt").read())
print("Solution 1:", solve(data))
example = clean(EXAMPLE)
print("Example 2:", solve2(example))
data = clean(open("d6.txt").read())
print("Solution 2:", solve2(data))
return
if __name__ == "__main__":
main()

133
2023/d7.py Normal file
View File

@@ -0,0 +1,133 @@
import lib
EXAMPLE = """
32T3K 765
T55J5 684
KK677 28
KTJJT 220
QQQJA 483
"""
def rank_hand(hand):
def key_to_int(c):
map = {'A': 14, 'K': 13, 'Q': 12, 'J': 11, 'T': 10}
if c in map:
return map[c]
else:
return ord(c) - 48
suite_to_count = {}
hand = list(map(key_to_int, hand))
for c in hand:
try:
suite_to_count[c] += 1
except:
suite_to_count[c] = 1
freq_to_hand = sorted([(v, k) for (k, v) in suite_to_count.items()], reverse=True)
rank = 0
match freq_to_hand:
case [_, _, _, _, _]:
rank = 0
case [(2, _), (1, _), (1, _), (1, _)]:
rank = 1
case [(2, _), (2, _), (1, _)]:
rank = 2
case [(3, _), (1, _), (1, _)]:
rank = 3
case [(3, _), (2, _)]:
rank = 4
case [(4, _), (1, _)]:
rank = 5
case [(5, _)]:
rank = 6
case _:
print("unexpected hand")
return (rank, hand)
def solve(lines: list[str]):
hands = []
for (i, line) in enumerate(lines):
hand, value = line.split()
hands.append((hand, value))
res = 0
hands = sorted(hands, key=lambda h: rank_hand(h[0]))
for i, (hand, value) in enumerate(hands):
v = (i + 1) * int(value)
res += v
return res
def rank_hand2(hand):
orig = str(hand)
def key_to_int(c):
map = {'A': 14, 'K': 13, 'Q': 12, 'T': 10, 'J': 0}
if c in map:
return map[c]
else:
return ord(c) - 48
j_count = hand.count("J")
suite_to_count = {}
hand = list(map(key_to_int, hand))
for c in hand:
if c == 0:
continue
try:
suite_to_count[c] += 1
except:
suite_to_count[c] = 1
rank = 0
freqs = list(sorted(suite_to_count.values(), reverse=True))
if freqs:
freqs[0] += j_count
else:
freqs = [j_count]
rank = 0
match freqs:
case [_, _, _, _, _]:
rank = 0
case [2, 2, _]:
rank = 2
case [2, _, _, _]:
rank = 1
case [3, 2]:
rank = 4
case [3, _, _]:
rank = 3
case [4, _]:
rank = 5
case [5]:
rank = 6
case h:
print("unexpected hand", h, orig)
return (rank, hand)
def solve2(lines: list[str]):
hands = []
for (i, line) in enumerate(lines):
hand, value = line.split()
hands.append((hand, value))
res = 0
hands = sorted(hands, key=lambda h: rank_hand2(h[0]))
for i, (hand, value) in enumerate(hands):
v = (i + 1) * int(value)
res += v
return res
def main():
lines = lib.str_to_lines_no_empty(EXAMPLE)
print("Example 1:", solve(lines))
lines = lib.str_to_lines_no_empty(open("d7.txt").read())
print("Solution 1:", solve(lines))
lines = lib.str_to_lines_no_empty(EXAMPLE)
print("Example 2:", solve2(lines))
lines = lib.str_to_lines_no_empty(open("d7.txt").read())
print("Solution 2:", solve2(lines))
if __name__ == "__main__":
main()

105
2023/d8.py Normal file
View File

@@ -0,0 +1,105 @@
import lib
EXAMPLE = """
LLR
AAA = (BBB, BBB)
BBB = (AAA, ZZZ)
ZZZ = (ZZZ, ZZZ)
"""
EXAMPLE2 = """
LR
11A = (11B, XXX)
11B = (XXX, 11Z)
11Z = (11B, XXX)
22A = (22B, XXX)
22B = (22C, 22C)
22C = (22Z, 22Z)
22Z = (22B, 22B)
XXX = (XXX, XXX)
"""
def solve(lines: list[str]):
res = 0
elems = {}
for (i, line) in enumerate(lines):
if i == 0:
first = list(line)
else:
left, pair = line.split("=")
left = left.strip()
pair = pair.replace("(", "").replace(")", "").strip().split(", ")
elems[left] = pair
i = 0
start = "AAA"
while start != "ZZZ":
dir = first[i % len(first)]
if dir == "L":
start = elems[start][0]
elif dir == "R":
start = elems[start][1]
else:
raise Exception("Unex")
i += 1
return i
def solve2(lines: list[str]):
res = 0
elems = {}
starts = []
ends = {}
for (i, line) in enumerate(lines):
if i == 0:
first = list(line)
else:
left, pair = line.split("=")
left = left.strip()
if left.endswith("A"):
starts.append(left)
if left.endswith("Z"):
ends[left] = 0
pair = pair.replace("(", "").replace(")", "").strip().split(", ")
elems[left] = pair
i = 0
while True:
new_starts = []
for start in starts:
dir = first[i % len(first)]
if dir == "L":
new_starts.append(elems[start][0])
elif dir == "R":
new_starts.append(elems[start][1])
else:
raise Exception("Unex")
i += 1
for start in new_starts:
if start in ends and ends[start] == 0:
ends[start] = i
starts = new_starts
for e in ends.values():
if e == 0:
break
else:
break
# 25:00
return lib.lcm(ends.values())
def main():
lines = lib.str_to_lines_no_empty(EXAMPLE)
print("Example 1:", solve(lines))
lines = lib.str_to_lines_no_empty(open("d8.txt").read())
print("Solution 1:", solve(lines))
lines = lib.str_to_lines_no_empty(EXAMPLE2)
print("Example 2:", solve2(lines))
lines = lib.str_to_lines_no_empty(open("d8.txt").read())
print("Solution 2:", solve2(lines))
if __name__ == "__main__":
main()

53
2023/d9.py Normal file
View File

@@ -0,0 +1,53 @@
import lib
EXAMPLE = """
0 3 6 9 12 15
1 3 6 10 15 21
10 13 16 21 30 45
"""
def solve(lines: list[str]):
res = 0
for (i, line) in enumerate(lines):
digits = lib.str_to_ints(line)
last_digits = []
while not all(d == 0 for d in digits):
last_digits.append(digits[-1])
digits = [digits[i + 1] - digits[i] for i in range(len(digits) - 1)]
s = 0
for d in reversed(last_digits):
s = s + d
res += s
# 50:00 ... problem in my helper function... lol
return res
def solve2(lines: list[str]):
res = 0
for (i, line) in enumerate(lines):
digits = lib.str_to_ints(line)
first_digits = []
while not all(d == 0 for d in digits):
first_digits.append(digits[0])
digits = [digits[i + 1] - digits[i] for i in range(len(digits) - 1)]
s = 0
for d in reversed(first_digits):
s = d - s
res += s
# 57:00...
return res
def main():
lines = lib.str_to_lines_no_empty(EXAMPLE)
print("Example 1:", solve(lines))
lines = lib.str_to_lines_no_empty(open("d9.txt").read())
print("Solution 1:", solve(lines))
lines = lib.str_to_lines_no_empty(EXAMPLE)
print("Example 2:", solve2(lines))
lines = lib.str_to_lines_no_empty(open("d9.txt").read())
print("Solution 2:", solve2(lines))
if __name__ == "__main__":
main()

30
2023/dx.py Normal file
View File

@@ -0,0 +1,30 @@
from lib import *
EXAMPLE = """
"""
def solve(input: Input, second=False):
res = 0
input.stats()
# g = input.grid2()
# ls = input.lines()
# ps = input.paras()
return res
def main():
DAY_INPUT = "ix.txt"
print("Example 1:", solve(Input(EXAMPLE)))
return
print("Solution 1:", solve(Input(DAY_INPUT)))
return
print("Example 2:", solve(Input(EXAMPLE), True))
return
print("Solution 2:", solve(Input(DAY_INPUT), True))
return
if __name__ == "__main__":
main()

1
2023/lib.py Symbolic link
View File

@@ -0,0 +1 @@
../lib.py

1
2023/monitor.py Symbolic link
View File

@@ -0,0 +1 @@
../monitor.py

View File

@@ -98,3 +98,53 @@ written in Python.
- Day 7: 20:15
- Day 8: 18:35
- Day 9:
# 2022
# 2023
- Day 1: 40:00 (I don't know what I am doing.)
- Day 2: 14:15 (Okay, but far way from leaderboard.)
- Day 3: 1st 20:00, 2nd 70:00... (I had a logic error that took me a while to
find.)
- Day 4: 1st 9:06, 2nd 22:31; it wasn't hard but I didn't think quick enough :/
- Day 5: 1st 25:00, 2nd 1:55:00; Required patience and accuracy
- Day 6: 13:54; I was slow because I thought it is much harder?
- Day 7: 75:00; leaderboard 16:00... that was just bad; no excuse
- Day 8: 25:00; I was doing pretty decent here.
- Day 9: 57:00; my input parse function did not consider negative values...
- Day 10: 180:00; this one was hard for me.
- Day 11: 68:00; okay but not elegant and way too slow ofc; x-ray solution
would have been neat
- Day 12: 52:00 and 22:00 for leaderboard; had the right idea and I am good at
this type of problem
- Day 13: 90:00; pretty straightforward but way too slow
- Day 14: 5:55 for first and then 48:00; straightforward but slow, ofc
- Day 15: 4:30 and 31:20; more reading comprehension than programming
- Day 16: 00:27:30 745; best placement so far, of course still horribly slow
- Day 17: a couple of hours; I realized that I need A* after a while; reused
implementation from Project Euler but improved with heapq which was super fun
- Day 18: a couple of hours; I realized that I need shoelace algo for part two
but didn't realize that I have to compute the outer edges for a while and
after I did, I still got clockwise/counter-clockwise issues. They could have
made it meaner by using different clock directions for example and input.
- Day 19: This one was pretty straightforward and required the interval
technique we applied earlier.
- Day 20: Part 2 was tough. I had the right idea of printing out the periods of
the input conjunction gate pretty early, but then messed up the
implementation and thought it wasn't gonna work. Spent a half day thinking up
something else before returning to the idea and it worked flawlessly.
- Day 21: Part 1 was straightforward, but part 2 maybe the hardest problem this
year.
- Day 22: Not too hard, but definitely way too slow for leaderboard.
- Day 23: I found this fun because it required some creativity for part 2. Slow
af, of course.
- Day 24: Solve problem with sympy. I first used numpy to solve part 1 and it
was much faster than using sympy, but I lost that solution when switching to
sympy. Takes about three minutes to run for part 1 and then part 2 is under a
second.
- Day 25: I cheeky solved this by plotting the graph and manually removing the
nodes. I should probably try to write an algorith that does that, but meh.
Manually plotting requires matplotlib and networkx packages.