generated from Jadarma/advent-of-code-kotlin-template
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathY2023D23.kt
154 lines (131 loc) · 6.21 KB
/
Y2023D23.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
package aockt.y2023
import aockt.util.parse
import aockt.util.spacial.Area
import aockt.util.spacial.Direction
import aockt.util.spacial.Direction.*
import aockt.util.spacial.Point
import aockt.util.spacial.move
import aockt.util.spacial.opposite
import aockt.util.validation.assume
import io.github.jadarma.aockt.core.Solution
object Y2023D23 : Solution {
/**
* Information about a single node in the actual Snow Island map.
* @property location The node coordinates.
* @property slope The direction the terrain is slanted in, if at all.
*/
private data class Node(val location: Point, val slope: Direction?)
/**
* A node in the island map which is either an entry point or a junction of 3 or more paths.
* @property location The node coordinates.
* @property neighbors A map from the location of neighboring points of interest to the distance to reach them.
*/
private data class PointOfInterest(val location: Point, val neighbors: Map<Point, Int>)
/**
* A navigation system for trails on an island.
* @param nodes The raw data of the island map.
* @property start The entry point at the top of the map.
* @property end The exit point at the bottom of the map.
*/
private class SnowIslandNavigator(nodes: Iterable<Node>) {
val start: Point
val end: Point
/** The points of interest, indexed by their location. */
private val navigationMap: Map<Point, PointOfInterest>
init {
val originalNodes: Map<Point, Node> = nodes.associateBy { it.location }
val area = Area(originalNodes.keys)
start = Point(area.xRange.first, area.yRange.last)
end = Point(area.xRange.last, area.yRange.first)
check(start in originalNodes) { "Could not determine entry point." }
check(end in originalNodes) { "Could not determine exit point." }
val pointsOfInterest: Set<Point> = buildSet {
add(start)
for (node in originalNodes.values) {
val neighbors = Direction.all.map { node.location.move(it) }.filter { it in originalNodes }
if (neighbors.size >= 3) {
assume(node.slope == null) { "A junction should not have a slope." }
add(node.location)
}
}
add(end)
}
/**
* Walks a trail [from] a point of interest [towards] a direction, returning the location of the next
* point of interest and the amount walked to get there.
* Returns null if a point of interest cannot be reached, such as if trying to walk up a slope, or the
* trail is a dead end.
*/
fun walkTrail(from: Point, towards: Direction): Pair<Point, Int>? {
var trailLength = 0
var trailDirection = towards
var trailLocation = from
while (true) {
trailLength += 1
trailLocation = trailLocation.move(trailDirection)
when (trailLocation) {
!in originalNodes -> return null
in pointsOfInterest -> return trailLocation to trailLength
}
trailDirection = when (val slope = originalNodes.getValue(trailLocation).slope) {
null -> Direction.all.minus(trailDirection.opposite)
.firstOrNull { trailLocation.move(it) in originalNodes }
?: return null
else -> {
if (slope != trailDirection) return null
slope
}
}
}
}
navigationMap = pointsOfInterest.associateWith { poi ->
Direction.all
.mapNotNull { direction -> walkTrail(from = poi, towards = direction) }.toMap()
.let { neighbors -> PointOfInterest(poi, neighbors) }
}
check(pointsOfInterest.all { it in navigationMap }) { "Failed to correctly build navigation map." }
}
private fun dfsMaximize(point: Point, goal: Point, seen: MutableSet<Point>): Int {
if (point == goal) return 0
var maxCost = Int.MIN_VALUE
val node = navigationMap.getValue(point)
seen.add(point)
for (next in node.neighbors) {
if (next.key in seen) continue
maxCost = maxOf(maxCost, dfsMaximize(next.key, goal, seen) + node.neighbors.getValue(next.key))
}
seen.remove(point)
return maxCost
}
/**
* Calculates the length of the longest path from [start] to [finish] that does not pass through the same
* point twice. A negative value means such a path is not possible.
*/
fun scenicRouteLength(start: Point, finish: Point): Int = dfsMaximize(start, finish, mutableSetOf())
}
/**
* Parse the input and return the map of Snow Island.
* @param input The puzzle input.
* @param withSlopes Whether to take in account the slopes or to ignore them.
*/
private fun parseInput(input: String, withSlopes: Boolean): List<Node> = parse {
input
.lines()
.asReversed()
.flatMapIndexed { y, line -> line.mapIndexed { x, c -> Point(x, y) to c } }
.mapNotNull { (point, c) ->
val slope = when (c) {
'>' -> Right
'<' -> Left
'v' -> Down
'^' -> Up
else -> null
}.takeIf { withSlopes }
Node(point, slope).takeUnless { c == '#' }
}
}
/** Create a navigation map from the nodes and return the longest path available between the start and end. */
private fun List<Node>.solve(): Int = SnowIslandNavigator(this).run { scenicRouteLength(start, end) }
override fun partOne(input: String) = parseInput(input, withSlopes = true).solve()
override fun partTwo(input: String) = parseInput(input, withSlopes = false).solve()
}