generated from Jadarma/advent-of-code-kotlin-template
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathY2023D10.kt
114 lines (96 loc) · 4.51 KB
/
Y2023D10.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
package aockt.y2023
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.parse
import aockt.util.spacial.polygonArea
import io.github.jadarma.aockt.core.Solution
object Y2023D10 : Solution {
/**
* A pipe segment.
* @property location The coordinates where this pipe segment is placed.
* @property flow The directions in which this pipe redirects fluids.
*/
private data class PipeSegment(val location: Point, val flow: Pair<Direction, Direction>) {
init {
require(flow.first != flow.second) { "A pipe cannot have the same output direction as the input." }
}
/**
* Returns which direction would the contents of the pipe go depending on the [incoming] direction.
* If the pipe cannot accept input from that direction, returns `null` instead.
*/
fun redirectFlow(incoming: Direction): Direction? = when (incoming) {
flow.first -> flow.second
flow.second -> flow.first
else -> null
}
}
/**
* A map of the pipe system observed on the floating metal island.
* @property nodes The pipe segments in the maze, indexed by their coordinates.
* @param start The starting location from which to find the loop.
*/
private class PipeMaze(
private val nodes: Map<Point, PipeSegment>,
start: Point,
) {
/** The loop contained in this pipe maze, obtained by following the pipes from the start location. */
val loop: List<PipeSegment> = buildList {
/** From a pipe segment and the current flow direction, find the next segment and its respective output. */
fun Pair<PipeSegment, Direction>.flow(): Pair<PipeSegment, Direction> {
val (node, flow) = this
val nextNode = nodes[node.location.move(flow)]
checkNotNull(nextNode) { "Invalid loop. Pipe diverted outside of bounds." }
val nextFlow = nextNode.redirectFlow(flow.opposite)
checkNotNull(nextFlow) { "Invalid loop. Pipe not connected to valid sink." }
return nextNode to nextFlow
}
var pipe: Pair<PipeSegment, Direction> = nodes.getValue(start).let { it to it.flow.first }
do {
add(pipe.first)
pipe = pipe.flow()
if (size > nodes.size) error("Loop not detected.")
} while (pipe.first.location != start)
}
/** How many points in the maze are fully contained inside the [loop]. Uses the shoelace formula. */
val loopVolume: Long = loop.map { it.location }.polygonArea(includingPerimeter = false)
}
/** Parse the [input] and recreate the [PipeMaze]. */
private fun parseInput(input: String): PipeMaze = parse {
lateinit var startPoint: Point
fun parseNode(x: Int, y: Int, value: Char) = PipeSegment(
location = Point(x, y),
flow = when (value) { // It is relevant that the Up direction is first, where applicable.
'|' -> Up to Down
'-' -> Left to Right
'L' -> Up to Right
'J' -> Up to Left
'7' -> Down to Left
'F' -> Down to Right
else -> error("Unknown pipe type: $value.")
},
)
val rows = input.lines()
require(rows.all { it.length == rows.first().length }) { "Map not a perfect rectangle." }
val pipes = rows
.asReversed()
.asSequence()
.flatMapIndexed { y: Int, line: String -> line.mapIndexed { x, c -> Triple(x, y, c) } }
.onEach { (x, y, c) -> if (c == 'S') startPoint = Point(x, y) }
.filterNot { it.third in ".S" }
.map { (x, y, c) -> parseNode(x, y, c) }
.associateBy { it.location }
val startPipe = listOf(Up, Down, Left, Right)
.filter { direction -> pipes[startPoint.move(direction)]?.redirectFlow(direction.opposite) != null }
.also { require(it.size == 2) { "Cannot determine starting pipe type." } }
.let { (i, o) -> PipeSegment(startPoint, i to o) }
PipeMaze(
nodes = pipes.plus(startPoint to startPipe),
start = startPoint,
)
}
override fun partOne(input: String) = parseInput(input).loop.size / 2
override fun partTwo(input: String) = parseInput(input).loopVolume
}