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+
47package day06
58
69import 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-
3131private operator fun Point.plus (other : Point ): Point =
3232 (first + other.first) to (second + other.second)
3333
34+ typealias Orientation = Pair <Direction , Point >
35+
3436data 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 */
11082fun 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
13499fun 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
143111fun 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