Skip to content

Commit c4de0f0

Browse files
authored
Add 2023/23 py
1 parent 1dd1c0f commit c4de0f0

File tree

9 files changed

+264
-0
lines changed

9 files changed

+264
-0
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"ndist",
3232
"nidx",
3333
"npos",
34+
"nposs",
3435
"nprev",
3536
"nrow",
3637
"pdir",

2023/23/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 23
2+
Here are my solutions to this puzzle.
3+
4+
* Problem instructions: [adventofcode.com/2023/day/23](https://adventofcode.com/2023/day/23)
5+
* Input: [adventofcode.com/2023/day/23/input](https://adventofcode.com/2023/day/23/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/23/input
10+
```
11+
12+
or run `fetch_input.sh` in this directory.

2023/23/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/23/input"
5+
test "$?" -eq 0 && echo "Fetched input" && exit 0 || echo "Failed to fetch input" && exit 1

2023/23/input1.0

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#.#####################
2+
#.......#########...###
3+
#######.#########.#.###
4+
###.....#.>.>.###.#.###
5+
###v#####.#v#.###.#.###
6+
###.>...#.#.#.....#...#
7+
###v###.#.#.#########.#
8+
###...#.#.#.......#...#
9+
#####.#.#.#######.#.###
10+
#.....#.#.#.......#...#
11+
#.#####.#.#.#########v#
12+
#.#...#...#...###...>.#
13+
#.#.#v#######v###.###v#
14+
#...#.>.#...>.>.#.###.#
15+
#####v#.#.###v#.#.###.#
16+
#.....#...#...#.#.#...#
17+
#.#########.###.#.#.###
18+
#...###...#...#...#.###
19+
###.###.#.###v#####v###
20+
#...#...#.#.>.>.#.>.###
21+
#.###.###.#.###.#.#v###
22+
#.....###...###...#...#
23+
#####################.#

2023/23/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/23

2023/23/output1.0

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

2023/23/output2.0

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

2023/23/part1.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#!/usr/bin/env python3
2+
# The graph seems to be DAG.
3+
import fileinput
4+
from copy import deepcopy
5+
6+
import sys
7+
sys.setrecursionlimit(3500)
8+
9+
10+
SYM_PATH = "."
11+
SYM_FOREST = "#"
12+
SYM_START = "S"
13+
14+
DIRECTION_DELTAS = [
15+
("^", -1, 0),
16+
("v", 1, 0),
17+
("<", 0, -1),
18+
(">", 0, 1),
19+
]
20+
21+
def read_map():
22+
map = [l.rstrip("\n") for l in fileinput.input()]
23+
return map, (0, 1), (len(map) - 1, len(map[0]) - 2)
24+
25+
def print_map(map, pos_start):
26+
for row in range(len(map)):
27+
line = []
28+
for col in range(len(map[0])):
29+
if (row, col) == pos_start:
30+
line.append(SYM_START)
31+
else:
32+
line.append(map[row][col])
33+
print("".join(line))
34+
35+
36+
def can_visit(map, pos, dir):
37+
row, col = pos
38+
return 0 <= row < len(map) and 0 <= col < len(map[0]) and map[row][col] in [SYM_PATH, dir]
39+
40+
41+
# Recursive DFS brute-force
42+
def longest_path(map, pos_end, pos, visited=set()):
43+
if pos == pos_end:
44+
return 0
45+
46+
paths = [float('-inf')]
47+
for dir, dr, dc in DIRECTION_DELTAS:
48+
npos = pos[0] + dr, pos[1] + dc
49+
if npos not in visited and can_visit(map, npos, dir):
50+
visited.add(npos)
51+
paths.append(1 + longest_path(map, pos_end, npos, visited))
52+
visited.remove(npos)
53+
return max(paths)
54+
55+
56+
57+
def main():
58+
map, pos_start, pos_end = read_map()
59+
# print_map(map, pos_start)
60+
61+
longest = longest_path(map, pos_end, pos_start)
62+
print(longest)
63+
64+
if __name__ == '__main__':
65+
main()

2023/23/part2.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
#!/usr/bin/env python3
2+
# Allowing any direction of the slopes seems to make this an undirected cyclic graph meaning it's NP-hard to find a longest path. Ref: https://en.wikipedia.org/wiki/Longest_path_problem
3+
# Solution: compress the graph by connecting single-direction paths so that the problem space becomes brut-force'able!
4+
import fileinput
5+
from collections import defaultdict, deque
6+
7+
SYM_PATH = "."
8+
SYM_FOREST = "#"
9+
SYM_START = "S"
10+
11+
DIRECTION_DELTAS = [
12+
("^", -1, 0),
13+
("v", 1, 0),
14+
("<", 0, -1),
15+
(">", 0, 1),
16+
]
17+
18+
DIR_OPPOSITE = {
19+
"^": "v",
20+
"v": "^",
21+
"<": ">",
22+
">": "<",
23+
}
24+
25+
def read_map():
26+
map = [l.rstrip("\n") for l in fileinput.input()]
27+
return map, (0, 1), (len(map) - 1, len(map[0]) - 2)
28+
29+
30+
def print_map(map, pos_start):
31+
for row in range(len(map)):
32+
line = []
33+
for col in range(len(map[0])):
34+
if (row, col) == pos_start:
35+
line.append(SYM_START)
36+
else:
37+
line.append(map[row][col])
38+
print("".join(line))
39+
40+
41+
def can_visit(map, pos, dir):
42+
row, col = pos
43+
sym_visitable = [SYM_PATH, dir, DIR_OPPOSITE[dir]]
44+
return 0 <= row < len(map) and 0 <= col < len(map[0]) and map[row][col] in sym_visitable
45+
46+
47+
def neighbors_of(map, pos):
48+
nposs = set()
49+
for dir, dr, dc in DIRECTION_DELTAS:
50+
npos = pos[0] + dr, pos[1] + dc
51+
if can_visit(map, npos, dir):
52+
nposs.add(npos)
53+
return nposs
54+
55+
56+
def construct_edges(map, pos_start, pos_end):
57+
edges = defaultdict(set) # pos -> [(npos, len)]
58+
stack = deque([pos_start]) # (pos, pos_prev)
59+
visited = set()
60+
61+
while stack:
62+
pos = stack.pop()
63+
if pos in visited:
64+
continue
65+
visited.add(pos)
66+
67+
for npos in neighbors_of(map, pos):
68+
edges[pos].add((npos, 1))
69+
edges[npos].add((pos, 1))
70+
stack.append(npos)
71+
return edges
72+
73+
74+
75+
def compact_edges(edges):
76+
positions = list(edges.keys())
77+
for pos in positions:
78+
if pos not in edges:
79+
continue
80+
81+
pos_edges = edges[pos]
82+
if len(pos_edges) != 2:
83+
continue
84+
85+
(npos1, len1), (npos2, len2) = pos_edges
86+
edges[npos1].remove((pos, len1))
87+
edges[npos2].remove((pos, len2))
88+
edges[npos1].add((npos2, len1 + len2))
89+
edges[npos2].add((npos1, len1 + len2))
90+
del edges[pos]
91+
92+
93+
def compress_graph(map, pos_start, pos_end):
94+
edges = construct_edges(map, pos_start, pos_end)
95+
compact_edges(edges)
96+
return edges
97+
98+
99+
# Recursive DFS brute-force on compressed graph.
100+
def longest_path(edges, pos_end, pos, visited=set()):
101+
if pos == pos_end:
102+
return 0
103+
104+
paths = [float('-inf')]
105+
for npos, length in edges[pos]:
106+
if npos not in visited:
107+
visited.add(npos)
108+
paths.append(length + longest_path(edges, pos_end, npos, visited))
109+
visited.remove(npos)
110+
return max(paths)
111+
112+
# DFS brute-force on compressed graph.
113+
# How to do with a stack: add a "undo" node to the stack that removes the
114+
# current not from visited set when we're done exploring the subpath,
115+
# so that this node can be found by another path. h/t # https://www.reddit.com/r/adventofcode/comments/18oy4pc/comment/kekhj7f/
116+
def longest_path_stack(edges, pos_start, pos_end):
117+
end_dists = []
118+
stack = deque([(pos_start, 0)]) # (pos, dist)
119+
visited = set() # pos
120+
121+
while stack:
122+
pos, dist = stack.pop()
123+
124+
if dist is None: # The undo-visited position enables us to explore a path involving this position again.
125+
visited.remove(pos)
126+
continue
127+
128+
if pos == pos_end:
129+
end_dists.append(dist)
130+
continue
131+
132+
if pos in visited:
133+
continue
134+
135+
visited.add(pos)
136+
stack.append((pos, None)) # Add undo- node to stack.
137+
138+
for npos, ndist in edges[pos]:
139+
stack.append((npos, dist + ndist))
140+
return max(end_dists)
141+
142+
143+
144+
def main():
145+
map, pos_start, pos_end = read_map()
146+
edges = compress_graph(map, pos_start, pos_end)
147+
148+
longest = longest_path(edges, pos_end, pos_start)
149+
# longest = longest_path_stack(edges, pos_end, pos_start)
150+
print(longest)
151+
152+
153+
if __name__ == '__main__':
154+
main()

0 commit comments

Comments
 (0)