Coverage Summary for Class: PipeMap (day10p2)
Class |
Method, %
|
Line, %
|
PipeMap |
81.8%
(9/11)
|
96.7%
(88/91)
|
PipeMap$enlargeMap$1 |
100%
(1/1)
|
100%
(1/1)
|
PipeMap$markRegions$1 |
100%
(1/1)
|
100%
(1/1)
|
PipeMap$markRegionsOnly$3 |
100%
(1/1)
|
100%
(1/1)
|
Total |
85.7%
(12/14)
|
96.8%
(91/94)
|
package day10p2
import day10p2.Direction.*
fun String.toPipeMap(): PipeMap {
val startCoordinate = findStartCoordinate()
val lines = lines()
val startPipeType = lines.calcPipeType(startCoordinate)
return PipeMap(
pipes = lines.mapIndexed { row, line ->
line.mapIndexed { column, ch ->
val coordinate = Coordinate(row, column)
if (coordinate == startCoordinate) startPipeType else ch.toPipeType()
}
},
startCoordinate = startCoordinate
)
}
data class PipeMap(val pipes: List<List<PipeType>>, val startCoordinate: Coordinate) {
val numberOfRows = pipes.size
val numberOfColumns = pipes[0].size
val reachMap: Array<Array<Long?>>
val numOfStepsToFarthestPointInLoop: Long
init {
val (rm, numOfSteps) = calcReachMap()
reachMap = rm
numOfStepsToFarthestPointInLoop = numOfSteps
}
operator fun get(coordinate: Coordinate): PipeType = pipes[coordinate.row][coordinate.column]
fun findNumberOfStepsToFarthestPointInLoop(): Long = numOfStepsToFarthestPointInLoop
private fun calcReachMap(): Pair<Array<Array<Long?>>, Long> {
val reachMap = Array(numberOfRows) { arrayOfNulls<Long>(numberOfColumns) }
val queue = mutableListOf(startCoordinate)
reachMap[startCoordinate.row][startCoordinate.column] = 0L
while (queue.isNotEmpty()) {
val coordinate = queue.removeFirst()!!
val numOfSteps = reachMap[coordinate.row][coordinate.column]!! + 1
for (direction in pipes[coordinate.row][coordinate.column].directions) {
val newCoordinate = direction(coordinate)
if (newCoordinate.row < 0 || newCoordinate.row >= this.numberOfRows) continue
if (newCoordinate.column < 0 || newCoordinate.column >= this.numberOfColumns) continue
val existingNumOfSteps = reachMap[newCoordinate.row][newCoordinate.column]
if (existingNumOfSteps != null) {
if (existingNumOfSteps != numOfSteps) continue
return reachMap to existingNumOfSteps
}
reachMap[newCoordinate.row][newCoordinate.column] = numOfSteps
queue.add(newCoordinate)
}
}
error("Didn't find farthest point")
}
fun enlargeMap(): String {
val result = Array(numberOfRows * 2) { Array(numberOfColumns * 2) { '.' } }
for (row in 0 until numberOfRows) {
for (column in 0 until numberOfColumns) {
val reach = reachMap[row][column]
val newRow = row * 2
val newColumn = column * 2
if (reach == null) {
result[newRow][newColumn] = '.'
result[newRow][newColumn + 1] = '.'
result[newRow + 1][newColumn] = '.'
continue
}
val pipeType = pipes[row][column]
if (Coordinate(row, column) == startCoordinate) {
result[newRow][newColumn] = 'S'
} else {
result[newRow][newColumn] = pipeType.ch
}
if (pipeType.directions.contains(east)) {
result[newRow][newColumn + 1] = PipeType.`-`.ch
}
if (pipeType.directions.contains(south)) {
result[newRow + 1][newColumn] = PipeType.`|`.ch
}
}
}
return result.joinToString("\n") { it.joinToString("") }
}
fun markRegionsOnly(): String {
val result = Array(numberOfRows) { Array(numberOfColumns) { '.' } }
val visitedCoordinates = mutableSetOf<Coordinate>()
for (row in 0 until numberOfRows) {
for (column in 0 until numberOfColumns) {
val reach = reachMap[row][column]
val currentCoordinate = Coordinate(row, column)
if (reach != null) {
if (currentCoordinate == startCoordinate) {
result[row][column] = 'S'
} else {
result[row][column] = pipes[row][column].ch
}
continue
}
if (visitedCoordinates.contains(currentCoordinate)) continue
val queue = mutableListOf(currentCoordinate)
val regionCoordinates = mutableSetOf<Coordinate>()
var endOfMapReached = false
while (queue.isNotEmpty()) {
val coordinate = queue.removeFirst()
if (regionCoordinates.contains(coordinate)) continue
if (coordinate.row < 0 || coordinate.row >= this.numberOfRows
|| coordinate.column < 0 || coordinate.column >= this.numberOfColumns
) {
endOfMapReached = true
continue
}
val r = reachMap[coordinate.row][coordinate.column]
if (r != null) continue
regionCoordinates.add(coordinate)
Direction.entries.forEach { queue.add(it(coordinate)) }
}
regionCoordinates.forEach { result[it.row][it.column] = if (endOfMapReached) 'O' else 'I' }
visitedCoordinates.addAll(regionCoordinates)
}
}
return result.joinToString("\n") { it.joinToString("") }
}
fun markRegions(): String {
val enlargedAndMarkedMap = enlargeMap().toPipeMap().markRegionsOnly().lines()
val result = Array(numberOfRows) { Array(numberOfColumns) { '.' } }
for (row in 0 until numberOfRows) {
for (column in 0 until numberOfColumns) {
val reach = reachMap[row][column]
if (reach != null) {
if (Coordinate(row, column) == startCoordinate) {
result[row][column] = 'S'
} else {
result[row][column] = pipes[row][column].ch
}
continue
}
result[row][column] = enlargedAndMarkedMap[2 * row][2 * column]
}
}
return result.joinToString("\n") { it.joinToString("") }
}
fun countNumberOfEnclosedTiles(): Int = markRegions().count { it == 'I' }
}
fun List<String>.calcPipeType(coordinate: Coordinate): PipeType {
val directions = Direction.entries.asSequence().filter { direction ->
val newCoordinate = direction(coordinate)
if (newCoordinate.row < 0 || newCoordinate.row >= this.size) return@filter false
val line = this[newCoordinate.row]
if (newCoordinate.column < 0 || newCoordinate.column >= line.length) return@filter false
val pipeType = line[newCoordinate.column].toPipeType()
pipeType.directions.any { it.reverse == direction }
}.toSet()
val candidates = PipeType.entries.filter { it.directions == directions }
require(candidates.size == 1)
return candidates.first()
}
fun String.findStartCoordinate(): Coordinate = lineSequence().flatMapIndexed { row: Int, line: String ->
line.mapIndexed { column: Int, char: Char -> Coordinate(row, column) to char }
}.first { (_, char) -> char == 'S' }.first
fun Char.toPipeType(): PipeType = when (this) {
'|' -> PipeType.`|`
'-' -> PipeType.`-`
'L' -> PipeType.L
'J' -> PipeType.J
'7' -> PipeType.`7`
'F' -> PipeType.F
else -> PipeType.dot
}
enum class PipeType(val ch: Char, val directions: Set<Direction>) {
`|`('|', setOf(north, south)),
`-`('-', setOf(east, west)),
L('L', setOf(north, east)),
J('J', setOf(north, west)),
`7`('7', setOf(south, west)),
F('F', setOf(south, east)),
dot('.', emptySet());
}
enum class Direction {
north {
override operator fun invoke(coordinate: Coordinate) = coordinate.copy(row = coordinate.row - 1)
override val reverse: Direction get() = south
},
south {
override operator fun invoke(coordinate: Coordinate) = coordinate.copy(row = coordinate.row + 1)
override val reverse: Direction get() = north
},
east {
override operator fun invoke(coordinate: Coordinate) = coordinate.copy(column = coordinate.column + 1)
override val reverse: Direction get() = west
},
west {
override operator fun invoke(coordinate: Coordinate) = coordinate.copy(column = coordinate.column - 1)
override val reverse: Direction get() = east
};
abstract operator fun invoke(coordinate: Coordinate): Coordinate
abstract val reverse: Direction
}
data class Coordinate(val row: Int, val column: Int)