Skip to content

Commit b9839e7

Browse files
committed
new post: day 4
1 parent 6095ea6 commit b9839e7

File tree

1 file changed

+208
-0
lines changed

1 file changed

+208
-0
lines changed

Diff for: content/posts/2024-12-06-advent-of-code-day-4.md

+208
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
---
2+
title: "Advent of Code: Day 4"
3+
date: 2024-12-06T11:44:39+01:00
4+
tags:
5+
- dev
6+
- devops
7+
---
8+
9+
Link to [Day #4](https://adventofcode.com/2024/day/4) puzzle.
10+
11+
<!--more-->
12+
13+
It's a pretty typical 2D matrix search problem, or a graph search problem, if
14+
you will.
15+
16+
The problem is naturally unraveled into the following searches:
17+
18+
- horizontally
19+
- horizontally, reversed
20+
- vertically
21+
- vertically, reversed
22+
- diagonally, all 4 directions (NW, NE, SW, SE)
23+
24+
It's possible to write a single pair of for loops that addresses the general
25+
case. The (classic) idea is to think of all 8 compass directions to move along
26+
the matrix:
27+
28+
- (1, 0)
29+
- (-1, 0)
30+
- (0, 1)
31+
- (0, -1)
32+
- (1, 1)
33+
- (-1, -1)
34+
- (-1, 1)
35+
- (1, -1)
36+
37+
Within the inner iteration, change `x += dx` and `y += dy` (or `i += di`, `j +=
38+
dj`, naming is hard). I did this many times in C++ though, and I want to write
39+
elegant Python code.
40+
41+
Therefore I came up with the following solution instead, with nested list
42+
comprehensions:
43+
44+
```python
45+
def search_horizontal(matrix, keyword):
46+
return sum((True for row in matrix for i in range(len(row) - len(keyword) + 1) if "".join(row[i:i + len(keyword)]) in [keyword, keyword[::-1]]))
47+
```
48+
49+
It follows the same principle as the original intent, however it leverages
50+
list slices so that we can omit the `dx/dy` step.
51+
52+
The vertical search is pretty straightforward: it is just a matter of running
53+
the horizontal search in the transposed matrix (`zip(*matrix)`).
54+
55+
I must confess that using `zip` to transpose matrices always felt magical and a
56+
mere coincidence that it just works™. Ruby has a `.transpose` method, which is
57+
more readable.
58+
59+
For the diagonal search, I couldn't think of an elegant list comprehension
60+
manner to address it. Is it even possible to "2D slice" in Python?
61+
62+
After-the-fact I decided to ask ChatGPT, and it is indeed possible, but it
63+
requires NumPy:
64+
65+
> If a is 2-D, returns the diagonal of a with the given offset, i.e., the
66+
> collection of elements of the form a[i, i+offset]. If a has more than two
67+
> dimensions, then the axes specified by axis1 and axis2 are used to determine
68+
> the 2-D sub-array whose diagonal is returned. The shape of the resulting array
69+
> can be determined by removing axis1 and axis2 and appending an index to the
70+
> right equal to the size of the resulting diagonals.
71+
72+
The method call resembles `numpy.array([[1, 2], [3, 4]]).diagonal(offset=1)`,
73+
perhaps with the aid of `.flip()` to account for the other direction.
74+
75+
Anyway, my plain diagonal search is:
76+
77+
```python
78+
def search_diagonal(matrix, keyword):
79+
rows = len(matrix)
80+
cols = len(matrix[0])
81+
82+
count = 0
83+
84+
for i in range(rows):
85+
for j in range(cols):
86+
if i + len(keyword) <= rows and j + len(keyword) <= cols:
87+
if "".join(matrix[i + k][j + k] for k in range(len(keyword))) in [keyword, keyword[::-1]]:
88+
count += 1
89+
if i + len(keyword) <= rows and j - len(keyword) >= -1:
90+
if "".join(matrix[i + k][j - k] for k in range(len(keyword))) in [keyword, keyword[::-1]]:
91+
count += 1
92+
93+
return count
94+
```
95+
96+
Part two is fundamentally a different problem.
97+
98+
One way to address it is to search for all `'A'` characters, and then look
99+
around its "edges" to see if they contain exactly two `'M'` and two `'S'`, and
100+
that they are properly arranged:
101+
102+
```python
103+
def search_double_mas(matrix):
104+
rows = len(matrix)
105+
cols = len(matrix[0])
106+
107+
count = 0
108+
109+
for i in range(1, rows - 1):
110+
for j in range(1, cols - 1):
111+
if matrix[i][j] != 'A':
112+
continue
113+
114+
# look at a QWERTY keyboard to make sense of these variable names
115+
q = matrix[i - 1][j - 1]
116+
e = matrix[i - 1][j + 1]
117+
z = matrix[i + 1][j - 1]
118+
c = matrix[i + 1][j + 1]
119+
edges = [q, e, z, c]
120+
121+
if edges.count('M') != 2 or edges.count('S') != 2:
122+
continue
123+
124+
if q == e or q == z:
125+
count += 1
126+
127+
return count
128+
```
129+
130+
I couldn't find an opportunity for reuse of the solution from part one.
131+
132+
The full solution:
133+
134+
```python
135+
#!/usr/bin/env python3
136+
import sys
137+
138+
def search_horizontal(matrix, keyword):
139+
return sum((True for row in matrix for i in range(len(row) - len(keyword) + 1) if "".join(row[i:i + len(keyword)]) in [keyword, keyword[::-1]]))
140+
141+
def search_vertical(matrix, keyword):
142+
return search_horizontal(zip(*matrix), keyword)
143+
144+
def search_diagonal(matrix, keyword):
145+
rows = len(matrix)
146+
cols = len(matrix[0])
147+
148+
count = 0
149+
150+
for i in range(rows):
151+
for j in range(cols):
152+
if i + len(keyword) <= rows and j + len(keyword) <= cols:
153+
if "".join(matrix[i + k][j + k] for k in range(len(keyword))) in [keyword, keyword[::-1]]:
154+
count += 1
155+
if i + len(keyword) <= rows and j - len(keyword) >= -1:
156+
if "".join(matrix[i + k][j - k] for k in range(len(keyword))) in [keyword, keyword[::-1]]:
157+
count += 1
158+
159+
return count
160+
161+
162+
def search_double_mas(matrix):
163+
rows = len(matrix)
164+
cols = len(matrix[0])
165+
166+
count = 0
167+
168+
for i in range(1, rows - 1):
169+
for j in range(1, cols - 1):
170+
if matrix[i][j] != 'A':
171+
continue
172+
173+
# look at a QWERTY keyboard to make sense of these variable names
174+
q = matrix[i - 1][j - 1]
175+
e = matrix[i - 1][j + 1]
176+
z = matrix[i + 1][j - 1]
177+
c = matrix[i + 1][j + 1]
178+
edges = [q, e, z, c]
179+
180+
if edges.count('M') != 2 or edges.count('S') != 2:
181+
continue
182+
183+
if q == e or q == z:
184+
count += 1
185+
186+
return count
187+
188+
189+
190+
def main():
191+
with open(sys.argv[1]) as input:
192+
lines = input.read().splitlines()
193+
194+
keyword = "XMAS"
195+
196+
# ['abcd', 'efgh', 'ijkl'] -> [['a', 'b', 'c', 'd'], ['e', 'f', 'g', 'h'], ['i', 'j', 'k', 'l']]
197+
matrix = [list(line) for line in lines]
198+
199+
# part one
200+
print(search_horizontal(matrix, keyword) + search_vertical(matrix, keyword) + search_diagonal(matrix, keyword))
201+
202+
# part two
203+
print(search_double_mas(matrix))
204+
205+
206+
if __name__ == '__main__':
207+
main()
208+
```

0 commit comments

Comments
 (0)