Skip to content

Commit e98bcb8

Browse files
committed
Day 06: used mutable data structures to dramatically enhance speed.
1 parent 9da8185 commit e98bcb8

File tree

1 file changed

+57
-89
lines changed

1 file changed

+57
-89
lines changed

src/main/kotlin/day06/day06.kt

+57-89
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
// Advent of Code 2024, Day 06.
22
// By Sebastian Raaphorst, 2024.
33

4+
// NOTE: Trying to use pure FP in this question made part 2 run extremely slowly.
5+
// Mutable data structures are needed to avoid having to continuously copy structures.
6+
47
package day06
58

69
import common.day
@@ -25,129 +28,94 @@ enum class Direction(val delta: Point) {
2528
}
2629
}
2730

28-
typealias Orientation = Pair<Direction, Point>
29-
typealias Orientations = Set<Orientation>
30-
3131
private operator fun Point.plus(other: Point): Point =
3232
(first + other.first) to (second + other.second)
3333

34+
typealias Orientation = Pair<Direction, Point>
35+
3436
data class MapGrid(val rows: Int,
3537
val cols: Int,
3638
val boundaries: Set<Point>) {
37-
fun boundary(point: Point): Boolean =
39+
fun isBoundary(point: Point): Boolean =
3840
point in boundaries
3941

40-
fun outOfBounds(point: Point) =
41-
(point.first !in 0 until rows) || (point.second !in 0 until cols)
42-
43-
// The empty squares where a boundary can be added to the map.
44-
// Note that we must manually remove the guard's starting location.
45-
val boundaryCandidates: Set<Point> =
46-
(0 until rows).flatMap { row ->
47-
(0 until cols).map { col ->
48-
Point(row, col)
49-
}
50-
}.filter { it !in boundaries }.toSet()
42+
fun isOutOfBounds(point: Point): Boolean =
43+
point.first !in 0 until rows || point.second !in 0 until cols
5144
}
5245

53-
data class Guard(private val originalLocation: Point,
54-
private val originalDirection: Direction,
55-
private val map: MapGrid) {
46+
data class Guard(val startPosition: Point,
47+
val map: MapGrid) {
5648
/**
57-
* Continue to move the guard until:
58-
* 1. The guard moves off the board.
59-
* 2. A cycle is detected.
49+
* Simulate the guard's path and return either:
50+
* 1. A set of visited points if it escapes.
51+
* 2. Null if it enters a loop.
6052
*/
6153
fun move(addedPoint: Point? = null): Set<Point>? {
62-
tailrec fun aux(visitedPoints: Set<Point> = emptySet(),
63-
orientations: Orientations = emptySet(),
64-
currentDirection: Direction = originalDirection,
65-
currentLocation: Point = originalLocation): Set<Point>? {
66-
// If we are off the map, then return the number of points.
67-
if (map.outOfBounds(currentLocation))
68-
return visitedPoints
69-
70-
// If the guard has moved at least once and has reached a previous
71-
// orientation, then she is cycling.
72-
val currentOrientation = currentDirection to currentLocation
73-
74-
// Attempt to move the guard.
75-
// If we have already seen this orientation, then we are cycling.
76-
// Return null to indicate this.
77-
if (currentOrientation in orientations) {
78-
return null
79-
}
80-
81-
// Calculate where the guard would go if she kept traveling forward.
82-
val newLocation = currentLocation + currentDirection.delta
83-
84-
// If the guard would hit a boundary, record the current orientation, rotate, and
85-
// do not move to the new location.
86-
if (map.boundary(newLocation) || (addedPoint != null && newLocation == addedPoint)) {
87-
return aux(visitedPoints,
88-
orientations + currentOrientation,
89-
currentDirection.clockwise(),
90-
currentLocation)
91-
}
92-
93-
// Move the guard forward, recording the location we just passed through.
94-
return aux(visitedPoints + currentLocation,
95-
orientations + currentOrientation,
96-
currentDirection,
97-
newLocation)
98-
54+
val visitedPoints = mutableSetOf<Point>()
55+
val visitedStates = mutableSetOf<Orientation>()
56+
57+
var currentPosition = startPosition
58+
var currentDir = Direction.NORTH
59+
60+
while (!map.isOutOfBounds(currentPosition)) {
61+
visitedPoints.add(currentPosition)
62+
63+
// If we return to a state we already visited, we have detected a cycle.
64+
val state = currentDir to currentPosition
65+
if (state in visitedStates) return null
66+
visitedStates.add(state)
67+
68+
// Move forward or turn if hitting a boundary
69+
val nextPosition = currentPosition + currentDir.delta
70+
if (map.isBoundary(nextPosition) || (addedPoint != null && nextPosition == addedPoint))
71+
currentDir = currentDir.clockwise()
72+
else
73+
currentPosition = nextPosition
9974
}
100-
101-
return aux()
75+
return visitedPoints
10276
}
10377
}
10478

10579
/**
106-
* Make a sparse representation of the items in the way of the guard by representing
107-
* the points where there are obstacles.
108-
* The first Point is the guard's starting location.
80+
* Parse the input into a Guard and MapGrid.
10981
*/
11082
fun parseProblem(input: String): Guard {
111-
var location: Point? = null
83+
var startPosition: Point? = null
11284
val barriers = mutableSetOf<Point>()
11385

114-
input.trim().lines()
115-
.withIndex()
116-
.map { (xIdx, row) ->
117-
row.withIndex()
118-
.forEach { (yIdx, symbol) ->
119-
when (symbol) {
120-
'^' -> location = Point(xIdx, yIdx)
121-
'#' -> barriers.add(Point(xIdx, yIdx))
122-
}
123-
}
124-
}
125-
val mapRows = input.lines().size
126-
val colRow = input.lines().first().trim().length
127-
val map = MapGrid(mapRows, colRow, barriers)
128-
return Guard(location ?: error("No guard starting position"),
129-
Direction.NORTH,
130-
map)
131-
}
86+
input.trim().lines().forEachIndexed { x, row ->
87+
row.forEachIndexed { y, ch -> when (ch) {
88+
'^' -> startPosition = Point(x, y)
89+
'#' -> barriers.add(Point(x, y))
90+
} }
91+
}
13292

93+
val rows = input.lines().size
94+
val cols = input.lines().first().length
95+
val map = MapGrid(rows, cols, barriers)
96+
return Guard(startPosition ?: error("No start position found"), map)
97+
}
13398

13499
fun answer1(guard: Guard): Int =
135100
guard.move()?.size ?: error("Could not calculate")
136101

137-
// The only place we can put a single obstruction are on the guard's initial path:
138-
// Any other locations will not obstruct the guard.
139-
fun answer2(guard: Guard): Int =
140-
(guard.move() ?: error("Could not calculate")).count { guard.move(it) == null }
102+
fun answer2(guard: Guard): Int {
103+
val originalPath = guard.move() ?: error("Could not calculate")
141104

105+
// We want the number of nulls, i.e. the number of times the guard gets in a cycle.
106+
return originalPath.count { candidatePoint ->
107+
guard.move(candidatePoint) == null
108+
}
109+
}
142110

143111
fun main() {
144112
val input = parseProblem(readInput({}::class.day()).trim())
145113

146114
println("--- Day 6: Guard Gallivant ---")
147115

148-
// Answer 1: 5208
116+
// Part 1: 5208
149117
println("Part 1: ${answer1(input)}")
150118

151-
// Answer 2: 1972
119+
// Part 2: 1972
152120
println("Part 2: ${answer2(input)}")
153-
}
121+
}

0 commit comments

Comments
 (0)