Skip to content

Commit fc2d5ac

Browse files
authored
Add 2023/21 py
1 parent 1048b76 commit fc2d5ac

File tree

11 files changed

+298
-1
lines changed

11 files changed

+298
-1
lines changed

.vscode/settings.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,24 @@
1919
},
2020

2121
"cSpell.words": [
22+
"adjm",
2223
"adventofcode",
2324
"BSLASH",
2425
"cloc",
2526
"csym",
2627
"dcount",
2728
"multitime",
29+
"ncol",
2830
"ndir",
2931
"ndist",
32+
"nidx",
3033
"npos",
3134
"nprev",
35+
"nrow",
3236
"pdir",
3337
"ppos",
34-
"pprev"
38+
"pprev",
39+
"starte",
40+
"Vandermonde"
3541
],
3642
}

2023/21/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Advent of Code - 2023 Day 21
2+
Here are my solutions to this puzzle.
3+
4+
* Problem instructions: [adventofcode.com/2023/day/21](https://adventofcode.com/2023/day/21)
5+
* Input: [adventofcode.com/2023/day/21/input](https://adventofcode.com/2023/day/21/input)
6+
7+
Fetch input by exporting `$AOC_SESSION` in your shell and then:
8+
```bash
9+
curl -OJLsb session=$AOC_SESSION adventofcode.com/2023/day/21/input
10+
```
11+
12+
or run `fetch_input.sh` in this directory.

2023/21/fetch_input.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/usr/bin/env sh
2+
# Make sure $AOC_SESSION is exported before running this script.
3+
4+
curl --remote-name --remote-header-name --silent --fail -A 'https://erikw.me/contact' --cookie "session=$AOC_SESSION" "https://adventofcode.com/2023/day/21/input"
5+
test "$?" -eq 0 && echo "Fetched input" && exit 0 || echo "Failed to fetch input" && exit 1

2023/21/input1.0

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
...........
2+
.....###.#.
3+
.###.##..#.
4+
..#.#...#..
5+
....#.#....
6+
.##..S####.
7+
.##..#...#.
8+
.......##..
9+
.##.#.####.
10+
.##..##.##.
11+
...........

2023/21/instructions.url

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[InternetShortcut]
2+
URL = https://adventofcode.com/2023/day/21

2023/21/output1.0

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
16

2023/21/output2.0

Whitespace-only changes.

2023/21/part1.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#!/usr/bin/env python3
2+
import fileinput
3+
from pprint import pprint
4+
from collections import deque
5+
6+
# STEPS = 1
7+
# STEPS = 6
8+
STEPS = 64
9+
10+
SYM_START = "S"
11+
SYM_PLOT = "."
12+
SYM_ROCK = "#"
13+
14+
NEIGHBOR_DELTAS = [
15+
(-1, 0),
16+
(1, 0),
17+
(0, -1),
18+
(0, 1),
19+
]
20+
21+
def read_map():
22+
map = []
23+
pos_start = None
24+
for i, line in enumerate(fileinput.input()):
25+
row = list(line.rstrip("\n"))
26+
try:
27+
col_start = row.index(SYM_START)
28+
except ValueError:
29+
pass
30+
else:
31+
row[col_start] = SYM_PLOT
32+
pos_start = (i, col_start)
33+
map.append(row)
34+
return map, pos_start
35+
36+
def print_map(map):
37+
for row in map:
38+
print("".join(row))
39+
40+
def positions_steps_away(map, pos_start, steps_target):
41+
que = deque([(*pos_start, 0)])
42+
visited = set()
43+
pos_targets = set()
44+
45+
while que:
46+
elem = que.popleft()
47+
if elem in visited:
48+
continue
49+
visited.add(elem)
50+
row, col, steps = elem
51+
52+
if steps == steps_target:
53+
pos_targets.add((row, col))
54+
continue
55+
56+
for dr, dc in NEIGHBOR_DELTAS:
57+
nrow, ncol = row + dr, col + dc
58+
if not (0 <= nrow < len(map) and 0 <= ncol < len(map[0]) and map[nrow][ncol] == SYM_PLOT):
59+
continue
60+
que.append((nrow, ncol, steps + 1))
61+
62+
return len(pos_targets)
63+
64+
65+
66+
67+
def main():
68+
map, pos_start = read_map()
69+
garden_positions = positions_steps_away(map, pos_start, STEPS)
70+
print(garden_positions)
71+
72+
if __name__ == '__main__':
73+
main()

2023/21/part1_matrix.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
#!/usr/bin/env python3
2+
import fileinput
3+
from pprint import pprint
4+
import scipy.sparse as sparse
5+
6+
# STEPS = 1
7+
# STEPS = 6
8+
STEPS = 64
9+
10+
SYM_START = "S"
11+
SYM_PLOT = "."
12+
SYM_ROCK = "#"
13+
14+
NEIGHBOR_DELTAS = [
15+
(-1, 0),
16+
(1, 0),
17+
(0, -1),
18+
(0, 1),
19+
]
20+
21+
def read_map():
22+
map = []
23+
pos_start = None
24+
for i, line in enumerate(fileinput.input()):
25+
row = list(line.rstrip("\n"))
26+
try:
27+
col_start = row.index(SYM_START)
28+
except ValueError:
29+
pass
30+
else:
31+
row[col_start] = SYM_PLOT
32+
pos_start = (i, col_start)
33+
map.append(row)
34+
return map, pos_start
35+
36+
def print_map(map):
37+
for row in map:
38+
print("".join(row))
39+
40+
def adjacency_matrix(map):
41+
num_poss = len(map) * len(map[0])
42+
# adj = sparse.csr_matrix((num_poss, num_poss))
43+
adj = sparse.dok_matrix((num_poss, num_poss))
44+
for row in range(len(map)):
45+
for col in range(len(map[0])):
46+
if map[row][col] != SYM_PLOT:
47+
continue
48+
for dr, dc in NEIGHBOR_DELTAS:
49+
nrow, ncol = row + dr, col + dc
50+
if 0 <= nrow < len(map) and 0 <= ncol < len(map[0]) and map[nrow][ncol] == SYM_PLOT:
51+
adj_idx = row * len(map[0]) + col
52+
adj_nidx = nrow * len(map[0]) + ncol
53+
adj[adj_idx, adj_nidx] = 1
54+
adj[adj_nidx, adj_idx] = 1
55+
return adj
56+
57+
58+
59+
60+
def main():
61+
map, pos_start = read_map()
62+
63+
# With an adjacency matrix A and then A^k:
64+
# cell (x,y) in A^k means the number of ways we can end up in y starting form x with k steps.
65+
# Thus we are looking for sum(A^k[start_plot]), meaning the sum of all positions we can end up at after k steps starting from the start plot.
66+
# Ref: https://math.stackexchange.com/a/207835/147723
67+
68+
map_adjm = adjacency_matrix(map)
69+
map_adjm_pow = sparse.linalg.matrix_power(map_adjm, STEPS)
70+
adj_start_idx = pos_start[0] * len(map[0]) + pos_start[1]
71+
72+
garden_positions = map_adjm_pow.count_nonzero(axis=1)[adj_start_idx]
73+
print(garden_positions)
74+
75+
if __name__ == '__main__':
76+
main()

2023/21/part2.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
#!/usr/bin/env python3
2+
import fileinput
3+
from collections import deque
4+
from math import ceil
5+
import numpy as np
6+
7+
STEPS = 26501365
8+
9+
SYM_START = "S"
10+
SYM_PLOT = "."
11+
SYM_ROCK = "#"
12+
13+
NEIGHBOR_DELTAS = [
14+
(-1, 0),
15+
(1, 0),
16+
(0, -1),
17+
(0, 1),
18+
]
19+
20+
21+
def read_map():
22+
map = []
23+
pos_start = None
24+
for i, line in enumerate(fileinput.input()):
25+
row = list(line.rstrip("\n"))
26+
try:
27+
col_start = row.index(SYM_START)
28+
except ValueError:
29+
pass
30+
else:
31+
row[col_start] = SYM_PLOT
32+
pos_start = (i, col_start)
33+
map.append(row)
34+
return map, pos_start
35+
36+
37+
def positions_steps_away(map, pos_start, steps_target):
38+
que = deque([(*pos_start, 0)])
39+
visited = set()
40+
pos_targets = set()
41+
42+
while que:
43+
elem = que.popleft()
44+
if elem in visited:
45+
continue
46+
visited.add(elem)
47+
row, col, steps = elem
48+
49+
if steps == steps_target:
50+
pos_targets.add((row, col))
51+
continue
52+
53+
for dr, dc in NEIGHBOR_DELTAS:
54+
nrow, ncol = row + dr, col + dc
55+
if not (0 <= nrow < len(map) and 0 <= ncol < len(map[0]) and map[nrow][ncol] == SYM_PLOT):
56+
continue
57+
que.append((nrow, ncol, steps + 1))
58+
59+
return len(pos_targets)
60+
61+
62+
# Expand map to cover the max number steps in one straight line from the start position (assuming start position in the middle of the grid).
63+
def expand_map(map, pos_start, max_steps):
64+
times = ceil(max_steps / (len(map)/2))
65+
mape = []
66+
for _ in range(times):
67+
for row in map:
68+
mape.append(times * row)
69+
return mape, (pos_start[0] + len(map) * (times//2), pos_start[1] + len(map[0]) * (times//2))
70+
71+
def main():
72+
map, pos_start = read_map()
73+
74+
# h/t https://www.reddit.com/r/adventofcode/comments/18nevo3/comment/keao6r4/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button
75+
# As STEPS = 202300 * len(map) + 65
76+
# or: 26501365 = 202300 * 131 + 65
77+
n = STEPS // len(map) # 202300
78+
r = STEPS % len(map) # 65
79+
# it suggests a recurrence on period 131 * n + 65
80+
81+
# As of the visual shape of the input data, it looks like we could fit a quadratic function to it y = ax^2 + bx + c
82+
# To fit a function, we can solve with linear matrix equations with A * x = b <=> x = A^-1 * b where A is coefficient matrix.
83+
84+
# First get some data points for x=[0, 1, 2]
85+
ans = []
86+
for x in range(3):
87+
steps = x * len(map) + r
88+
mape, pos_starte = expand_map(map, pos_start, steps)
89+
ans.append(positions_steps_away(mape, pos_starte, steps))
90+
b = np.array(ans)
91+
92+
# A is the Vandermonde matrix with each row the geometric progression (regression?) [x^2 x^1 x^0] for x:s (0, 1, 2) (same x's used to calculate y1, y2, y3 above)
93+
# The vandermonde matrix usually goes in the other direction with each row [x^0 x^1 x^2]. Having it reverse just makes the solution vector x's elements appear in the same order we want to use it.
94+
# See 'polynomial interpolation' at https://en.wikipedia.org/wiki/Vandermonde_matrix#Applications
95+
# [ 0^2 0^1 0^0 ] [ 0 0 1 ]
96+
# [ 1^2 1^1 1^0 ] = [ 1 1 1 ]
97+
# [ 2^2 2^1 2^0 ] [ 4 2 1 ]
98+
A = np.matrix([[0, 0, 1], [1, 1, 1], [4, 2, 1]])
99+
100+
# Solve for solution vector x!
101+
x = np.linalg.solve(A, b).astype(np.int64)
102+
103+
# Now we have found the coefficients for the quadratic function, namely x.
104+
# Thus we can calculate the number of garden_positions for the give period n we want (according to earlier reasoning)
105+
garden_positions = x[0] * n**2 + x[1] * n + x[2]
106+
print(garden_positions)
107+
108+
if __name__ == '__main__':
109+
main()

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
# Runtime Dependencies
22
graphviz
3+
numpy
34
python-mermaid
45
termcolor
6+
scipy
57
z3-solver
68

79
# Development Tools

0 commit comments

Comments
 (0)