Day 17. What a mess.
This commit is contained in:
parent
3a8a2d79a9
commit
095bbd4bc7
29
src/Utils.kt
29
src/Utils.kt
@ -2,8 +2,8 @@ import java.io.File
|
||||
import java.math.BigInteger
|
||||
import java.security.MessageDigest
|
||||
|
||||
fun Int.gcd(b: Int): Int = if (b == 0) this else b.gcd(this % b)
|
||||
fun Long.gcd(b: Long): Long = if (b == 0L) this else b.gcd(this % b)
|
||||
tailrec fun Int.gcd(b: Int): Int = if (b == 0) this else b.gcd(this % b)
|
||||
tailrec fun Long.gcd(b: Long): Long = if (b == 0L) this else b.gcd(this % b)
|
||||
fun Int.lcm(b: Int): Int = this / gcd(b) * b
|
||||
fun Long.lcm(b: Long): Long = this / gcd(b) * b
|
||||
fun Iterable<Int>.lcm(): Int = reduce(Int::lcm)
|
||||
@ -37,6 +37,7 @@ fun listOfIntegerLists(input: List<String>): ArrayList<List<Int>> {
|
||||
|
||||
data class RelPos(val dc: Int, val dr: Int) {
|
||||
fun translate(c: Int, r: Int) = RelPos(c + dc, r + dr)
|
||||
fun translate(rp: RelPos) = RelPos(rp.dc + dc, rp.dr + dr)
|
||||
}
|
||||
|
||||
class CharGrid {
|
||||
@ -147,6 +148,10 @@ class CharGrid {
|
||||
relposes.map { get(c + it.dc, r + it.dr) }
|
||||
}
|
||||
|
||||
fun matchRelative(c: Int, r: Int, relposes: Iterable<RelPos>, predicate: (char: Char) -> Boolean): List<RelPos> =
|
||||
relposes.filter { predicate(get(c + it.dc, r + it.dr)) }
|
||||
|
||||
|
||||
fun findMatches(predicate: (char: Char) -> Boolean): List<Pair<Int, Int>> {
|
||||
val matches = ArrayList<Pair<Int, Int>>()
|
||||
for (r in 0 until height) {
|
||||
@ -170,6 +175,26 @@ class CharGrid {
|
||||
fun debug() {
|
||||
data.joinToString("\n") { it.concatToString() }.println()
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as CharGrid
|
||||
|
||||
if (width != other.width) return false
|
||||
if (height != other.height) return false
|
||||
if (!data.contentDeepEquals(other.data)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = width
|
||||
result = 31 * result + height
|
||||
result = 31 * result + data.contentDeepHashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
182
src/aoc2023/Day17.kt
Normal file
182
src/aoc2023/Day17.kt
Normal file
@ -0,0 +1,182 @@
|
||||
package aoc2023
|
||||
|
||||
import CharGrid
|
||||
import RelPos
|
||||
import println
|
||||
import readInput
|
||||
import java.util.*
|
||||
import kotlin.math.min
|
||||
|
||||
/*
|
||||
--- Day 17: Clumsy Crucible ---
|
||||
The lava starts flowing rapidly once the Lava Production Facility is operational. As you leave, the reindeer offers you a parachute, allowing you to quickly reach Gear Island.
|
||||
As you descend, your bird's-eye view of Gear Island reveals why you had trouble finding anyone on your way up: half of Gear Island is empty, but the half below you is a giant factory city!
|
||||
You land near the gradually-filling pool of lava at the base of your new lavafall. Lavaducts will eventually carry the lava throughout the city, but to make use of it immediately, Elves are loading it into large crucibles on wheels.
|
||||
The crucibles are top-heavy and pushed by hand. Unfortunately, the crucibles become very difficult to steer at high speeds, and so it can be hard to go in a straight line for very long.
|
||||
To get Desert Island the machine parts it needs as soon as possible, you'll need to find the best way to get the crucible from the lava pool to the machine parts factory. To do this, you need to minimize heat loss while choosing a route that doesn't require the crucible to go in a straight line for too long.
|
||||
Fortunately, the Elves here have a map (your puzzle input) that uses traffic patterns, ambient temperature, and hundreds of other parameters to calculate exactly how much heat loss can be expected for a crucible entering any particular city block.
|
||||
For example:
|
||||
2413432311323
|
||||
3215453535623
|
||||
3255245654254
|
||||
3446585845452
|
||||
4546657867536
|
||||
1438598798454
|
||||
4457876987766
|
||||
3637877979653
|
||||
4654967986887
|
||||
4564679986453
|
||||
1224686865563
|
||||
2546548887735
|
||||
4322674655533
|
||||
|
||||
Each city block is marked by a single digit that represents the amount of heat loss if the crucible enters that block. The starting point, the lava pool, is the top-left city block; the destination, the machine parts factory, is the bottom-right city block. (Because you already start in the top-left block, you don't incur that block's heat loss unless you leave that block and then return to it.)
|
||||
Because it is difficult to keep the top-heavy crucible going in a straight line for very long, it can move at most three blocks in a single direction before it must turn 90 degrees left or right. The crucible also can't reverse direction; after entering each city block, it may only turn left, continue straight, or turn right.
|
||||
One way to minimize heat loss is this path:
|
||||
2>>34^>>>1323
|
||||
32v>>>35v5623
|
||||
32552456v>>54
|
||||
3446585845v52
|
||||
4546657867v>6
|
||||
14385987984v4
|
||||
44578769877v6
|
||||
36378779796v>
|
||||
465496798688v
|
||||
456467998645v
|
||||
12246868655<v
|
||||
25465488877v5
|
||||
43226746555v>
|
||||
|
||||
This path never moves more than three consecutive blocks in the same direction and incurs a heat loss of only 102.
|
||||
Directing the crucible from the lava pool to the machine parts factory, but not moving more than three consecutive blocks in the same direction, what is the least heat loss it can incur?
|
||||
|
||||
*/
|
||||
fun main() {
|
||||
|
||||
val inlineTestInput = """
|
||||
2413432311323
|
||||
3215453535623
|
||||
3255245654254
|
||||
3446585845452
|
||||
4546657867536
|
||||
1438598798454
|
||||
4457876987766
|
||||
3637877979653
|
||||
4654967986887
|
||||
4564679986453
|
||||
1224686865563
|
||||
2546548887735
|
||||
4322674655533
|
||||
"""
|
||||
|
||||
var best = Int.MAX_VALUE
|
||||
var grid = CharGrid(arrayOf(charArrayOf(' ')))
|
||||
var bestHeat = IntArray(0)
|
||||
|
||||
data class LavaContext(val c: Int, val r: Int, val dir: RelPos, val dirCount: Int, val heatloss: Int, val parent: LavaContext? = null) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as LavaContext
|
||||
|
||||
if (c != other.c) return false
|
||||
if (r != other.r) return false
|
||||
if (dir != other.dir) return false
|
||||
if (dirCount != other.dirCount) return false
|
||||
if (heatloss != other.heatloss) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = c
|
||||
result = 31 * result + r
|
||||
result = 31 * result + dir.hashCode()
|
||||
result = 31 * result + dirCount
|
||||
result = 31 * result + heatloss
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
val contexts = PriorityQueue(compareBy<LavaContext> { it.heatloss - it.c - it.r })
|
||||
val visitCache = HashSet<LavaContext>()
|
||||
|
||||
fun astar(minSame: Int, maxSame: Int) {
|
||||
while (contexts.isNotEmpty()) {
|
||||
do {
|
||||
val ctx = contexts.remove()!!
|
||||
if (ctx.heatloss >= best) break
|
||||
val newHeatLoss = ctx.heatloss + grid[ctx.c, ctx.r].digitToInt()
|
||||
if (newHeatLoss > best) break
|
||||
if (ctx.c == grid.width - 1 && ctx.r == grid.height - 1) {
|
||||
if (ctx.dirCount >= minSame) {
|
||||
best = best.coerceAtMost(newHeatLoss)
|
||||
val path = grid.copyOf()
|
||||
var bt: LavaContext? = ctx
|
||||
while (bt != null) {
|
||||
path[bt.c, bt.r] = '*'
|
||||
bt = bt.parent
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
val p = ctx.r * grid.width + ctx.c
|
||||
if (newHeatLoss - 10 > bestHeat[p]) break
|
||||
bestHeat[p] = min(bestHeat[p], newHeatLoss)
|
||||
val newDirs = (grid.matchRelative(ctx.c, ctx.r, CharGrid.PLUS_POS) { it != '#' }
|
||||
.filter { ctx.dir.dc != -it.dc || ctx.dir.dr != -it.dr })
|
||||
newDirs.forEach {
|
||||
val take = ((it == ctx.dir) && (ctx.dirCount + 1 < maxSame)) ||
|
||||
((it != ctx.dir) && (ctx.dirCount + 1 > minSame))
|
||||
if (take) {
|
||||
val newDirCount = if (it == ctx.dir) ctx.dirCount + 1 else 0
|
||||
val newCtx = LavaContext(ctx.c + it.dc, ctx.r + it.dr, it, newDirCount, newHeatLoss, ctx)
|
||||
if (!visitCache.contains(newCtx)) {
|
||||
visitCache.add(newCtx)
|
||||
contexts.add(newCtx)
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (false)
|
||||
}
|
||||
}
|
||||
|
||||
fun part1(input: List<String>): Int {
|
||||
grid = CharGrid(input, '#')
|
||||
best = (grid.width + grid.height) * 9
|
||||
bestHeat = IntArray(grid.width * grid.height) { best }
|
||||
val context = LavaContext(0, 0, RelPos(0, 0), 1, -grid[0, 0].digitToInt())
|
||||
visitCache.clear()
|
||||
contexts.clear()
|
||||
contexts.add(context)
|
||||
astar(0, 3)
|
||||
return best
|
||||
}
|
||||
|
||||
fun part2(input: List<String>): Int {
|
||||
grid = CharGrid(input, '#')
|
||||
best = (grid.width + grid.height) * 9
|
||||
bestHeat = IntArray(grid.width * grid.height) { best }
|
||||
val context = LavaContext(0, 0, RelPos(0, 0), 4, -grid[0, 0].digitToInt())
|
||||
visitCache.clear()
|
||||
contexts.clear()
|
||||
contexts.add(context)
|
||||
astar(3, 10)
|
||||
return best
|
||||
}
|
||||
|
||||
// test if implementation meets criteria from the description, like:
|
||||
val testInput = inlineTestInput.trim().reader().readLines()
|
||||
//val testInput = readInput("aoc2023/Day17_test")
|
||||
val testInputPart1Result = part1(testInput)
|
||||
println("Part 1 Test: $testInputPart1Result")
|
||||
val testInputPart2Result = part2(testInput)
|
||||
println("Part 2 Test: $testInputPart2Result")
|
||||
check(testInputPart1Result == 102)
|
||||
check(testInputPart2Result == 94)
|
||||
|
||||
val input = readInput("aoc2023/Day17")
|
||||
part1(input).println()
|
||||
part2(input).println()
|
||||
}
|
Loading…
Reference in New Issue
Block a user