Skip to content

Commit deb1b7b

Browse files
committedDec 16, 2024
✨ Add 2024 day 16 solutions
1 parent ea30af6 commit deb1b7b

File tree

5 files changed

+303
-1
lines changed

5 files changed

+303
-1
lines changed
 

‎.vscode/settings.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"editor.renderWhitespace": "trailing",
1111
"prettier.prettierPath": "./node_modules/prettier/index.cjs",
1212
"typescript.tsdk": "node_modules\\typescript\\lib",
13-
"editor.defaultColorDecorators": true,
13+
"editor.defaultColorDecorators": "auto",
1414
"markdownlint.config": {
1515
"default": true,
1616
"MD026": { "punctuation": ".,;:。,;:!" },

‎2024/16/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
This was my biggest blunder so far this year. I went down a very unnecessary rabbit hole of building a node graph out of the map because I didn't realize my pathfinder in part 2 was looping. My pathfinder in part 1 worked great, and I could have adapted it just fine to find all paths, but I stupidly thought I had to rewrite it.
2+
3+
After getting my part 2 answer, I rewrote my solution to use the node graph since it's a lot faster, and found along the way 2 issues with my graph building involving multiple paths between nodes, and nodes that lead to themselves.

‎2024/16/index.test.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { expect, test } from 'bun:test'
2+
import { getPart1Answer, getPart2Answer, part1Examples, part2Examples } from '.'
3+
import { logAnswer } from '@scripts/log'
4+
5+
const inputText = await Bun.file(`${import.meta.dir}/input.txt`).text()
6+
7+
test('solutions', () => {
8+
console.log(' 🌟 Part 1 answer:', getAndLogAnswer(1))
9+
part1Examples.forEach(([i, a]) => expect(`${getPart1Answer(i, true)}`).toBe(`${a}`))
10+
console.log('🌟🌟 Part 2 answer:', getAndLogAnswer(2))
11+
part2Examples.forEach(([i, a]) => expect(`${getPart2Answer(i, true)}`).toBe(`${a}`))
12+
expect(1).toBe(1) // Ensures that console logs always run
13+
})
14+
15+
function getAndLogAnswer(part: 1 | 2) {
16+
console.time(`P${part}`)
17+
const answer = `${(part === 1 ? getPart1Answer : getPart2Answer)(inputText)}`
18+
console.timeEnd(`P${part}`)
19+
logAnswer(import.meta.dir, part, answer)
20+
return answer
21+
}

‎2024/16/index.ts

+262
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
import {
2+
addXY,
3+
Direction,
4+
flipDir,
5+
getNeighbors,
6+
NEIGHBOR_XY_4WAY,
7+
toGrid,
8+
toXY,
9+
XY,
10+
} from '../util'
11+
12+
const parseInput = (input: string) => {
13+
let startXY: XY | undefined
14+
let endXY: XY | undefined
15+
const map = input
16+
.trim()
17+
.split('\n')
18+
.map((line, y) =>
19+
line.split('').map((v, x) => {
20+
if (v === 'S') {
21+
startXY = [x, y]
22+
return 0
23+
}
24+
if (v === 'E') {
25+
endXY = [x, y]
26+
return 0
27+
}
28+
return v === '.' ? 0 : 1
29+
})
30+
)
31+
if (!startXY || !endXY) throw 'start/end not found!'
32+
return { map, startXY, endXY }
33+
}
34+
35+
const buildNodeGraph = (map: number[][], startGrid: string, endGrid: string) => {
36+
const nodeGridList: Set<string> = new Set([startGrid, endGrid])
37+
for (let y = 1; y < map.length - 1; y++) {
38+
const row = map[y]
39+
for (let x = 1; x < row.length - 1; x++) {
40+
if (row[x] === 1) continue
41+
const openNeighbors = getNeighbors(x, y).filter(([nx, ny]) => map[ny][nx] === 0)
42+
if (openNeighbors.length > 2) nodeGridList.add(toGrid(x, y))
43+
}
44+
}
45+
const nodeGraph: NodeGraph = new Map()
46+
nodeGridList.forEach((iGrid) => nodeGraph.set(iGrid, new Map()))
47+
const openNodes: Set<string> = new Set([startGrid])
48+
const closedNodes: Set<string> = new Set()
49+
while (openNodes.size > 0) {
50+
const fromNode = [...openNodes][0]
51+
const fromNodeXY = toXY(fromNode)
52+
const fromNetworkNode = nodeGraph.get(fromNode)!
53+
const exploredDirs = [...fromNetworkNode].map(([, { startDir }]) => startDir)
54+
openNodes.delete(fromNode)
55+
closedNodes.add(fromNode)
56+
for (let startDir = 0; startDir < 4; startDir++) {
57+
if (exploredDirs.includes(startDir)) continue
58+
const firstStepXY = addXY(fromNodeXY, NEIGHBOR_XY_4WAY[startDir])
59+
const firstStepTile = map[firstStepXY[1]][firstStepXY[0]]
60+
if (firstStepTile === 1) continue
61+
let currentDir = startDir
62+
let currentXY = firstStepXY
63+
let steps = 1
64+
let turns = 0
65+
let intersectionFound = false
66+
const grids = [fromNode, toGrid(...firstStepXY)]
67+
while (!intersectionFound) {
68+
let nextStepFound = false
69+
for (let turn = -1; turn <= 1; turn++) {
70+
const nextDir = (currentDir + 4 + turn) % 4
71+
const nextStepXY = addXY(currentXY, NEIGHBOR_XY_4WAY[nextDir])
72+
const nextStepGrid = toGrid(...nextStepXY)
73+
if (nodeGraph.has(nextStepGrid)) {
74+
if (nextStepGrid === fromNode) break
75+
steps++
76+
if (turn !== 0) turns++
77+
const cost = turns * 1000 + steps
78+
grids.push(nextStepGrid)
79+
fromNetworkNode.set(`${nextStepGrid}~${startDir}`, {
80+
startDir,
81+
cost,
82+
endDir: nextDir,
83+
grids,
84+
})
85+
intersectionFound = true
86+
if (!closedNodes.has(nextStepGrid)) openNodes.add(nextStepGrid)
87+
break
88+
}
89+
const nextStepTile = map[nextStepXY[1]][nextStepXY[0]]
90+
if (nextStepTile === 1) continue
91+
nextStepFound = true
92+
steps++
93+
if (turn !== 0) turns++
94+
currentDir = nextDir
95+
currentXY = nextStepXY
96+
grids.push(nextStepGrid)
97+
break
98+
}
99+
if (!nextStepFound) {
100+
// dead end
101+
break
102+
}
103+
}
104+
}
105+
}
106+
while (true) {
107+
let nodesRemoved = 0
108+
nodeGraph.forEach((node, nodeGrid) => {
109+
if (nodeGrid === startGrid || nodeGrid === endGrid) return
110+
if (node.size === 1) {
111+
nodeGraph.forEach((compareNode) => {
112+
compareNode.forEach((_, hash) => {
113+
if (hash.split('~')[0] === nodeGrid) compareNode.delete(hash)
114+
})
115+
})
116+
nodeGraph.delete(nodeGrid)
117+
nodesRemoved++
118+
}
119+
})
120+
if (nodesRemoved === 0) break
121+
}
122+
return nodeGraph
123+
}
124+
125+
type PathNode = {
126+
grid: string
127+
hash: string
128+
dir: number
129+
score: number
130+
grids: string[]
131+
}
132+
133+
const findPath = (map: number[][], start: XY, end: XY) => {
134+
const startGrid = toGrid(...start)
135+
const endGrid = toGrid(...end)
136+
const network = buildNodeGraph(map, startGrid, endGrid)
137+
let current: PathNode = {
138+
grid: startGrid,
139+
hash: `${startGrid}:${Direction.right}`,
140+
dir: Direction.right,
141+
score: 0,
142+
grids: [startGrid],
143+
}
144+
const openTiles: Map<string, PathNode> = new Map([[current.hash, current]])
145+
const bestHashScores: Map<string, [number, Set<string>]> = new Map()
146+
let lowestScore = Infinity
147+
let bestGrids: Set<string> = new Set()
148+
while (openTiles.size > 0) {
149+
openTiles.delete(current.hash)
150+
const networkNode = network.get(current.grid)!
151+
for (const [nextNodeHash, nextNodeInfo] of networkNode) {
152+
const [nextNodeGrid] = nextNodeHash.split('~')
153+
if (nextNodeInfo.startDir === flipDir(current.dir)) continue
154+
const cost = nextNodeInfo.cost
155+
const turnCost = nextNodeInfo.startDir === current.dir ? 0 : 1000
156+
let score = current.score + cost + turnCost
157+
const nextNodeEndHash = `${nextNodeGrid}~${nextNodeInfo.endDir}`
158+
const bestHashScore = bestHashScores.get(nextNodeEndHash)
159+
const grids = new Set([...current.grids, ...nextNodeInfo.grids])
160+
if (bestHashScore) {
161+
if (bestHashScore[0] < score) {
162+
continue
163+
} else if (bestHashScore[0] === score) {
164+
bestHashScore[1].forEach((g) => grids.add(g))
165+
}
166+
}
167+
bestHashScores.set(nextNodeEndHash, [score, grids])
168+
if (nextNodeGrid === endGrid) {
169+
if (score < lowestScore) {
170+
lowestScore = score
171+
bestGrids = new Set(grids)
172+
} else if (score === lowestScore) {
173+
grids.forEach((grid) => bestGrids.add(grid))
174+
}
175+
break
176+
}
177+
const toNode: PathNode = {
178+
grid: nextNodeGrid,
179+
hash: nextNodeEndHash,
180+
dir: nextNodeInfo.endDir,
181+
score,
182+
grids: [...grids],
183+
}
184+
openTiles.set(toNode.hash, toNode)
185+
}
186+
current = getLowestScoreTile(openTiles)
187+
}
188+
return [lowestScore, bestGrids] as [number, Set<string>]
189+
}
190+
191+
function getLowestScoreTile(list: Map<string, PathNode>) {
192+
let best
193+
for (const [, value] of list) {
194+
if (!best || value.score < best.score) {
195+
best = value
196+
}
197+
}
198+
return best!
199+
}
200+
201+
export const getPart1Answer: Answer = (input, example = false) => {
202+
const { map, startXY, endXY } = parseInput(input)
203+
const [lowestScore] = findPath(map, startXY, endXY)
204+
return lowestScore
205+
}
206+
207+
export const part1Examples: Example[] = [
208+
[
209+
`###############
210+
#.......#....E#
211+
#.#.###.#.###.#
212+
#.....#.#...#.#
213+
#.###.#####.#.#
214+
#.#.#.......#.#
215+
#.#.#####.###.#
216+
#...........#.#
217+
###.#.#####.#.#
218+
#...#.....#.#.#
219+
#.#.#.###.#.#.#
220+
#.....#...#.#.#
221+
#.###.#.#.#.#.#
222+
#S..#.....#...#
223+
###############`,
224+
'7036',
225+
],
226+
[
227+
`#################
228+
#...#...#...#..E#
229+
#.#.#.#.#.#.#.#.#
230+
#.#.#.#...#...#.#
231+
#.#.#.#.###.#.#.#
232+
#...#.#.#.....#.#
233+
#.#.#.#.#.#####.#
234+
#.#...#.#.#.....#
235+
#.#.#####.#.###.#
236+
#.#.#.......#...#
237+
#.#.###.#####.###
238+
#.#.#...#.....#.#
239+
#.#.#.#####.###.#
240+
#.#.#.........#.#
241+
#.#.#.#########.#
242+
#S#.............#
243+
#################`,
244+
'11048',
245+
],
246+
]
247+
248+
type NodeGraph = Map<
249+
string,
250+
Map<string, { startDir: number; cost: number; endDir: number; grids: string[] }>
251+
>
252+
253+
export const getPart2Answer: Answer = (input, example = false) => {
254+
const { map, startXY, endXY } = parseInput(input)
255+
const [, bestPathGrids] = findPath(map, startXY, endXY)
256+
return bestPathGrids.size
257+
}
258+
259+
export const part2Examples: Example[] = [
260+
[part1Examples[0][0], '45'],
261+
[part1Examples[1][0], '64'],
262+
]

‎2024/util.ts

+16
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,22 @@ export const DIRS: XY[] = [
99
[-1, 0],
1010
]
1111

12+
export enum Direction {
13+
up = 0,
14+
right = 1,
15+
down = 2,
16+
left = 3,
17+
}
18+
19+
export const flipDir = (dir: Direction) =>
20+
dir === Direction.up
21+
? Direction.down
22+
: dir === Direction.left
23+
? Direction.right
24+
: dir === Direction.down
25+
? Direction.up
26+
: Direction.left
27+
1228
export const NEIGHBOR_XY_4WAY: XY[] = [
1329
[0, -1],
1430
[1, 0],

0 commit comments

Comments
 (0)
Please sign in to comment.