Compare commits

...

8 Commits

10 changed files with 502 additions and 26 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
Pipfile
__pycache__ __pycache__
*.txt *.txt

View File

@@ -2,6 +2,9 @@ My solutions to the Advent of Code 2023 programming challenges.
Thanks to Eric Wastl for creating this enjoyable event. Thanks to Eric Wastl for creating this enjoyable event.
- Requires `lib.py` from [aocpy](https://git.felixm.de/felixm/aocpy) repository.
- Requires `sympy` for day 24.
- Requires `matplotlib` and `networkx` for hands-on day 25.
# Times # Times
@@ -36,8 +39,15 @@ Thanks to Eric Wastl for creating this enjoyable event.
the input conjunction gate pretty early, but then messed up the 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 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. something else before returning to the idea and it worked flawlessly.
- Day 21: - 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 22: Not too hard, but definitely way too slow for leaderboard.
- Day 23: - Day 23: I found this fun because it required some creativity for part 2. Slow
- Day 24: af, of course.
- Day 25: - 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.

2
d12.py
View File

@@ -39,7 +39,7 @@ def solve(lines: list[str], repeat=1):
all = [] all = []
for (_, line) in enumerate(lines): for (_, line) in enumerate(lines):
springs, numbers = line.split() springs, numbers = line.split()
numbers = tuple(lib.str_to_int_list(numbers)) numbers = tuple(lib.str_to_ints(numbers))
ns = "?".join([springs for _ in range(repeat)]) ns = "?".join([springs for _ in range(repeat)])
all.append((tuple(ns), numbers * repeat)) all.append((tuple(ns), numbers * repeat))

3
d19.py
View File

@@ -140,10 +140,9 @@ def main():
# 25:00 # 25:00
print("Example 2:", solve(Input(EXAMPLE), True)) print("Example 2:", solve(Input(EXAMPLE), True))
print("Correct 2:", 167409079868000) assert solve(Input(EXAMPLE), True) == 167409079868000
print("Solution 2:", solve(Input(DAY_INPUT), True)) print("Solution 2:", solve(Input(DAY_INPUT), True))
# 120:00 # 120:00
return
if __name__ == "__main__": if __name__ == "__main__":
main() main()

1
d20.py
View File

@@ -103,6 +103,7 @@ def main():
print("Example 2:", solve(Input(EXAMPLE2))) print("Example 2:", solve(Input(EXAMPLE2)))
print("Solution 1:", solve(Input(DAY_INPUT))) print("Solution 1:", solve(Input(DAY_INPUT)))
print("Solution 2:", solve(Input(DAY_INPUT), True)) print("Solution 2:", solve(Input(DAY_INPUT), True))
assert solve(Input(DAY_INPUT), True) == 244178746156661
if __name__ == "__main__": if __name__ == "__main__":
main() main()

192
d21.py
View File

@@ -1,7 +1,7 @@
from lib import * from lib import *
import os
EXAMPLE = """ EXAMPLE = """...........
...........
.....###.#. .....###.#.
.###.##..#. .###.##..#.
..#.#...#.. ..#.#...#..
@@ -14,13 +14,11 @@ EXAMPLE = """
........... ...........
""" """
def solve(i: Input, second=False): def solve(input: Input):
res = 0 g = input.grid2()
g = i.grid2()
s = g.find('S')[0] s = g.find('S')[0]
g[s] = 'O' g[s] = 'O'
# steps = 64 steps = 64
steps = 26501365
seen = set() seen = set()
for i in range(steps): for i in range(steps):
os = tuple(g.find('O')) os = tuple(g.find('O'))
@@ -38,18 +36,178 @@ def solve(i: Input, second=False):
g[nb] = 'O' g[nb] = 'O'
return len(g.find('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(): def main():
DAY_INPUT = "i21.txt" DAY_INPUT = "i21.txt"
# print("Example 1:", solve(Input(EXAMPLE)))
print("Example 1:", solve(Input(EXAMPLE))) # print("Solution 1:", solve(Input(DAY_INPUT)))
print("Solution 1:", solve(Input(DAY_INPUT))) # print("Example 2:", solve2(Input(EXAMPLE)))
return print("Solution 2:", solve2(Input(DAY_INPUT)))
print("Example 2:", solve(Input(EXAMPLE), True))
return
print("Solution 2:", solve(Input(DAY_INPUT), True))
return
if __name__ == "__main__": if __name__ == "__main__":
main() main()

146
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 = "i23.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
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 = "i24.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
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 = "i25.txt"
print("Solution 1:", solve(Input(DAY_INPUT)), "(hands-free)")
if __name__ == "__main__":
main()

4
d9.py
View File

@@ -9,7 +9,7 @@ EXAMPLE = """
def solve(lines: list[str]): def solve(lines: list[str]):
res = 0 res = 0
for (i, line) in enumerate(lines): for (i, line) in enumerate(lines):
digits = lib.str_to_int_list(line) digits = lib.str_to_ints(line)
last_digits = [] last_digits = []
while not all(d == 0 for d in digits): while not all(d == 0 for d in digits):
last_digits.append(digits[-1]) last_digits.append(digits[-1])
@@ -24,7 +24,7 @@ def solve(lines: list[str]):
def solve2(lines: list[str]): def solve2(lines: list[str]):
res = 0 res = 0
for (i, line) in enumerate(lines): for (i, line) in enumerate(lines):
digits = lib.str_to_int_list(line) digits = lib.str_to_ints(line)
first_digits = [] first_digits = []
while not all(d == 0 for d in digits): while not all(d == 0 for d in digits):
first_digits.append(digits[0]) first_digits.append(digits[0])