1
1
// Advent of Code 2024, Day 06.
2
2
// By Sebastian Raaphorst, 2024.
3
3
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
+
4
7
package day06
5
8
6
9
import common.day
@@ -25,129 +28,94 @@ enum class Direction(val delta: Point) {
25
28
}
26
29
}
27
30
28
- typealias Orientation = Pair <Direction , Point >
29
- typealias Orientations = Set <Orientation >
30
-
31
31
private operator fun Point.plus (other : Point ): Point =
32
32
(first + other.first) to (second + other.second)
33
33
34
+ typealias Orientation = Pair <Direction , Point >
35
+
34
36
data class MapGrid (val rows : Int ,
35
37
val cols : Int ,
36
38
val boundaries : Set <Point >) {
37
- fun boundary (point : Point ): Boolean =
39
+ fun isBoundary (point : Point ): Boolean =
38
40
point in boundaries
39
41
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
51
44
}
52
45
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 ) {
56
48
/* *
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 .
60
52
*/
61
53
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
99
74
}
100
-
101
- return aux()
75
+ return visitedPoints
102
76
}
103
77
}
104
78
105
79
/* *
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.
109
81
*/
110
82
fun parseProblem (input : String ): Guard {
111
- var location : Point ? = null
83
+ var startPosition : Point ? = null
112
84
val barriers = mutableSetOf<Point >()
113
85
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
+ }
132
92
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
+ }
133
98
134
99
fun answer1 (guard : Guard ): Int =
135
100
guard.move()?.size ? : error(" Could not calculate" )
136
101
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" )
141
104
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
+ }
142
110
143
111
fun main () {
144
112
val input = parseProblem(readInput({}::class .day()).trim())
145
113
146
114
println (" --- Day 6: Guard Gallivant ---" )
147
115
148
- // Answer 1: 5208
116
+ // Part 1: 5208
149
117
println (" Part 1: ${answer1(input)} " )
150
118
151
- // Answer 2: 1972
119
+ // Part 2: 1972
152
120
println (" Part 2: ${answer2(input)} " )
153
- }
121
+ }
0 commit comments