Skip to content

Commit 0eb1174

Browse files
committed
feat: Solving day 20
1 parent 34e5851 commit 0eb1174

File tree

3 files changed

+522
-0
lines changed

3 files changed

+522
-0
lines changed

docs/day20.md

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
---
2+
url: "https://adventofcode.com/2024/day/20"
3+
---
4+
5+
## Day 20: Race Condition
6+
7+
The Historians are quite pixelated again. This time, a massive, black building looms over you - you're right outside the CPU!
8+
9+
While The Historians get to work, a nearby program sees that you're idle and challenges you to a race. Apparently, you've arrived just in time for the frequently-held race condition festival!
10+
11+
The race takes place on a particularly long and twisting code path; programs compete to see who can finish in the fewest picoseconds. The winner even gets their very own mutex!
12+
13+
They hand you a map of the racetrack (your puzzle input). For example:
14+
15+
```txt
16+
###############
17+
#...#...#.....#
18+
#.#.#.#.#.###.#
19+
#S#...#.#.#...#
20+
#######.#.#.###
21+
#######.#.#...#
22+
#######.#.###.#
23+
###..E#...#...#
24+
###.#######.###
25+
#...###...#...#
26+
#.#####.#.###.#
27+
#.#...#.#.#...#
28+
#.#.#.#.#.#.###
29+
#...#...#...###
30+
###############
31+
```
32+
33+
The map consists of track (`.`) - including the start (`S`) and end (`E`) positions (both of which also count as track) - and walls (`#`).
34+
35+
When a program runs through the racetrack, it starts at the start position. Then, it is allowed to move up, down, left, or right; each such move takes `1` picosecond. The goal is to reach the end position as quickly as possible. In this example racetrack, the fastest time is `84` picoseconds.
36+
37+
Because there is only a single path from the start to the end and the programs all go the same speed, the races used to be pretty boring. To make things more interesting, they introduced a new rule to the races: programs are allowed to cheat.
38+
39+
The rules for cheating are very strict. Exactly once during a race, a program may disable collision for up to 2 picoseconds. This allows the program to pass through walls as if they were regular track. At the end of the cheat, the program must be back on normal track again; otherwise, it will receive a segmentation fault and get disqualified.
40+
41+
So, a program could complete the course in 72 picoseconds (saving 12 picoseconds) by cheating for the two moves marked 1 and 2:
42+
43+
```txt
44+
###############
45+
#...#...12....#
46+
#.#.#.#.#.###.#
47+
#S#...#.#.#...#
48+
#######.#.#.###
49+
#######.#.#...#
50+
#######.#.###.#
51+
###..E#...#...#
52+
###.#######.###
53+
#...###...#...#
54+
#.#####.#.###.#
55+
#.#...#.#.#...#
56+
#.#.#.#.#.#.###
57+
#...#...#...###
58+
###############
59+
```
60+
61+
Or, a program could complete the course in 64 picoseconds (saving 20 picoseconds) by cheating for the two moves marked `1` and `2`:
62+
63+
```txt
64+
###############
65+
#...#...#.....#
66+
#.#.#.#.#.###.#
67+
#S#...#.#.#...#
68+
#######.#.#.###
69+
#######.#.#...#
70+
#######.#.###.#
71+
###..E#...12..#
72+
###.#######.###
73+
#...###...#...#
74+
#.#####.#.###.#
75+
#.#...#.#.#...#
76+
#.#.#.#.#.#.###
77+
#...#...#...###
78+
###############
79+
```
80+
81+
This cheat saves 38 picoseconds:
82+
83+
```txt
84+
###############
85+
#...#...#.....#
86+
#.#.#.#.#.###.#
87+
#S#...#.#.#...#
88+
#######.#.#.###
89+
#######.#.#...#
90+
#######.#.###.#
91+
###..E#...#...#
92+
###.####1##.###
93+
#...###.2.#...#
94+
#.#####.#.###.#
95+
#.#...#.#.#...#
96+
#.#.#.#.#.#.###
97+
#...#...#...###
98+
###############
99+
```
100+
101+
This cheat saves 64 picoseconds and takes the program directly to the end:
102+
103+
```txt
104+
###############
105+
#...#...#.....#
106+
#.#.#.#.#.###.#
107+
#S#...#.#.#...#
108+
#######.#.#.###
109+
#######.#.#...#
110+
#######.#.###.#
111+
###..21...#...#
112+
###.#######.###
113+
#...###...#...#
114+
#.#####.#.###.#
115+
#.#...#.#.#...#
116+
#.#.#.#.#.#.###
117+
#...#...#...###
118+
###############
119+
```
120+
121+
Each cheat has a distinct start position (the position where the cheat is activated, just before the first move that is allowed to go through walls) and end position; cheats are uniquely identified by their start position and end position.
122+
123+
In this example, the total number of cheats (grouped by the amount of time they save) are as follows:
124+
125+
* There are 14 cheats that save 2 picoseconds.
126+
* There are 14 cheats that save 4 picoseconds.
127+
* There are 2 cheats that save 6 picoseconds.
128+
* There are 4 cheats that save 8 picoseconds.
129+
* There are 2 cheats that save 10 picoseconds.
130+
* There are 3 cheats that save 12 picoseconds.
131+
* There is one cheat that saves 20 picoseconds.
132+
* There is one cheat that saves 36 picoseconds.
133+
* There is one cheat that saves 38 picoseconds.
134+
* There is one cheat that saves 40 picoseconds.
135+
* There is one cheat that saves 64 picoseconds.
136+
137+
You aren't sure what the conditions of the racetrack will be like, so to give yourself as many options as possible, you'll need a list of the best cheats. How many cheats would save you at least 100 picoseconds?

src/day20/day20.go

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package day20
2+
3+
import (
4+
"slices"
5+
"strings"
6+
)
7+
8+
const CHEAT_LENGTH_OF_INTEREST = 100
9+
const EMPTY_SPACE = '.'
10+
const END = 'E'
11+
const START = 'S'
12+
const MOVES_TO_USE_CHEAT = 2
13+
const THREAD_POOL = 64
14+
15+
type point struct {
16+
x, y int
17+
}
18+
19+
type maze struct {
20+
start, end point
21+
width, height uint
22+
emptySpaces map[point]bool
23+
}
24+
25+
func (m maze) valid(p point) bool {
26+
return p.x >= 0 && p.y >= 0 && p.x < int(m.width) && p.y < int(m.height)
27+
}
28+
29+
type thread struct {
30+
p point
31+
moves []point
32+
}
33+
34+
func Solve(input string) uint {
35+
m, sol := SolvePuzzle(input)
36+
cheats := Cheats(m, sol)
37+
var cnt uint = 0
38+
for k, v := range cheats {
39+
if k >= CHEAT_LENGTH_OF_INTEREST {
40+
cnt += v
41+
}
42+
}
43+
return cnt
44+
}
45+
46+
func SolvePuzzle(input string) (maze, []point) {
47+
m := parseInput(input)
48+
sol := solve(m)
49+
return m, sol
50+
}
51+
52+
func Cheats(m maze, sol []point) map[uint]uint {
53+
cheats := make(map[uint]uint)
54+
solution := make(map[point]uint)
55+
for i, p := range sol {
56+
solution[p] = uint(i)
57+
}
58+
59+
for i := 1; i < int(m.width-1); i++ {
60+
for j := 1; j < int(m.height-1); j++ {
61+
p := point{x: i, y: j}
62+
if m.emptySpaces[p] || solution[p] > 0 {
63+
continue
64+
}
65+
66+
east, south, west, north := point{x: p.x + 1, y: p.y}, point{x: p.x, y: p.y + 1}, point{x: p.x - 1, y: p.y}, point{x: p.x, y: p.y - 1}
67+
if (solution[east] == 0 || solution[west] == 0) && (solution[south] == 0 || solution[north] == 0) {
68+
continue
69+
}
70+
71+
cheats[max(cheatLength(east, west, solution), cheatLength(south, north, solution))-MOVES_TO_USE_CHEAT] += 1
72+
}
73+
}
74+
75+
return cheats
76+
}
77+
78+
func parseInput(input string) maze {
79+
var m maze
80+
m.emptySpaces = make(map[point]bool)
81+
for j, line := range strings.Split(input, "\n") {
82+
m.width = uint(len(line))
83+
for i, r := range line {
84+
p := point{x: i, y: j}
85+
switch r {
86+
case EMPTY_SPACE:
87+
m.emptySpaces[p] = true
88+
case START:
89+
m.start = p
90+
case END:
91+
m.emptySpaces[p] = true
92+
m.end = p
93+
}
94+
}
95+
m.height = uint(j) + 1
96+
}
97+
return m
98+
}
99+
100+
func solve(m maze) []point {
101+
seenSpaces := make(map[point]uint)
102+
seenSpaces[m.start] = 1
103+
threads := []thread{{p: m.start, moves: []point{m.start}}}
104+
coldStorage := make([]thread, 0)
105+
solution := make([]point, 0)
106+
for {
107+
nextThreads := make([]thread, 0)
108+
for i, t := range threads {
109+
if i >= THREAD_POOL {
110+
coldStorage = append(coldStorage, t)
111+
continue
112+
} else if t.p == m.end {
113+
if len(solution) == 0 || len(t.moves) < len(solution) {
114+
solution = t.moves
115+
}
116+
} else {
117+
nextThreads = append(calcNextMoves(m, t, seenSpaces), nextThreads...)
118+
}
119+
}
120+
121+
if len(nextThreads) == 0 {
122+
if len(coldStorage) > 0 {
123+
x := THREAD_POOL
124+
if len(coldStorage) < x {
125+
x = len(coldStorage)
126+
}
127+
nextThreads = coldStorage[:x]
128+
coldStorage = slices.Delete(coldStorage, 0, x)
129+
} else {
130+
if len(solution) == 0 {
131+
panic("No solution with no more spaces to check")
132+
}
133+
return solution
134+
}
135+
}
136+
for i, t := range nextThreads {
137+
if len(solution) > 0 && len(t.moves) >= len(solution) {
138+
nextThreads = slices.Delete(nextThreads, i, i)
139+
}
140+
if seenSpaces[t.p] == 0 || uint(len(t.moves)) < seenSpaces[t.p] {
141+
seenSpaces[t.p] = uint(len(t.moves))
142+
}
143+
}
144+
threads = nextThreads
145+
}
146+
}
147+
148+
func calcNextMoves(m maze, curr thread, seenSpaces map[point]uint) []thread {
149+
nextMoves := make([]thread, 0)
150+
possibilities := []thread{{p: point{x: curr.p.x + 1, y: curr.p.y}, moves: curr.moves},
151+
{p: point{x: curr.p.x, y: curr.p.y + 1}, moves: curr.moves},
152+
{p: point{x: curr.p.x - 1, y: curr.p.y}, moves: curr.moves},
153+
{p: point{x: curr.p.x, y: curr.p.y - 1}, moves: curr.moves}}
154+
for _, t := range possibilities {
155+
if m.valid(t.p) && m.emptySpaces[t.p] {
156+
if seenSpaces[t.p] == 0 || uint(len(t.moves))+1 < seenSpaces[t.p] {
157+
t.moves = append(t.moves, t.p)
158+
nextMoves = append(nextMoves, t)
159+
}
160+
}
161+
}
162+
return nextMoves
163+
}
164+
165+
func cheatLength(a, b point, solution map[point]uint) uint {
166+
if solution[a] == 0 || solution[b] == 0 {
167+
return 0
168+
}
169+
170+
if solution[a] > solution[b] {
171+
return solution[a] - solution[b]
172+
} else {
173+
return solution[b] - solution[a]
174+
}
175+
}

0 commit comments

Comments
 (0)