From cd12ca069f5595654ff9a95e296a26da43361e47 Mon Sep 17 00:00:00 2001 From: gergo Date: Tue, 20 Dec 2022 19:29:00 +0100 Subject: [PATCH] day 16 (quicker solution for part one) --- src/main/kotlin/me/gergo/Aoc16.kt | 117 +++++++----------------------- 1 file changed, 28 insertions(+), 89 deletions(-) diff --git a/src/main/kotlin/me/gergo/Aoc16.kt b/src/main/kotlin/me/gergo/Aoc16.kt index 9612380..b328275 100644 --- a/src/main/kotlin/me/gergo/Aoc16.kt +++ b/src/main/kotlin/me/gergo/Aoc16.kt @@ -1,7 +1,10 @@ package me.gergo import java.io.File -import kotlin.math.max + +// TIL: +// - Modeling these branching / optimization problems is MUCH more readable and faster when done with state -> newState Sets (rather than DFS / +// recursive functions), and it also allows one to log the state space complexity easily fun main() { val valves = File("src/main/resources/input16.txt").readLines() @@ -9,109 +12,45 @@ fun main() { val valvesByName = valves.associateBy(Valve::name) val neighbors = valves.associateWith { v -> v.tunnelsTo.map { valvesByName[it]!! } } - // Part One - dynamic programming, memoization - data class MemoKey(val valve: Valve, val opened: Set, val minute: Int) - - val memo = mutableMapOf() - - fun findMaxPressure(current: Valve, opened: Set, minute: Int): Int { - val key = MemoKey(current, opened, minute) - var max = memo[key] - if (max == null) { - val pressureReleased = opened.sumOf(Valve::rate) - max = pressureReleased + if (minute == 30) 0 - else { - max( - if (current.rate > 0 && !opened.contains(current)) // Stay here and open current valve (if the valve is not damaged) - findMaxPressure(current, opened.plus(current), minute + 1) else 0, - neighbors[current]!!.maxOf { // ...or don't open the valve and move to a neighbor - findMaxPressure(it, opened, minute + 1) - } - ) - } - memo[key] = max - } - return max - } - // FUK. I spent HOURS wondering why the sample data passes and my puzzle input does not... "You start at valve AA", NOT the first valve :| val aa = valves.first { it.name == "AA" } - val result1 = findMaxPressure(aa, emptySet(), 1) - println("Max pressure released : $result1") - - // Part Two - data class MemoKey2( - val me: Valve, - val eli: Valve, - val opened: Long, - val openedRate: Int, - val meVisited: Long, - val eliVisited: Long, - val minute: Int - ) { - fun meCanOpen() = me.rate > 0 && !opened.isBitSet(me.index) - fun eliCanOpen() = eli.rate > 0 && !opened.isBitSet(eli.index) - - fun meNeighbors() = neighbors[me]!! - fun eliNeighbors() = neighbors[eli]!! - - fun meMovesEliOpens(neighbor: Valve) = - MemoKey2(neighbor, eli, opened.setBit(eli.index), openedRate + eli.rate, meVisited.setBit(me.index), eliVisited, minute + 1) - - fun eliMovesMeOpens(neighbor: Valve) = - MemoKey2(me, neighbor, opened.setBit(me.index), openedRate + me.rate, meVisited, eliVisited.setBit(eli.index), minute + 1) + // Part One + var states = setOf(ValveState(aa, emptySet(), 0)) + for (i in 1..30) { - fun weBothOpen() = - MemoKey2(me, eli, opened.setBit(me.index).setBit(eli.index), openedRate + me.rate + eli.rate, meVisited, eliVisited, minute + 1) - - fun weBothMove(n1: Valve, n2: Valve) = - MemoKey2(n1, n2, opened, openedRate, meVisited.setBit(me.index), eliVisited.setBit(eli.index), minute + 1) - } - - val memo2 = mutableMapOf() - - fun permutations(k: MemoKey2) = sequence { - if (k.meCanOpen() && k.eliCanOpen() && k.me != k.eli) yield(k.weBothOpen()) // Both valves can be opened - if (k.meCanOpen()) - for (n in k.eliNeighbors()) { // My valve can be opened, Eli can move - if (k.meVisited.isBitSet(n.index)) continue // Eli shouldn't go where I went - yield(k.eliMovesMeOpens(n)) - } - if (k.eliCanOpen()) - for (n in k.meNeighbors()) { // Eli's valve can be opened, I can move - if (k.eliVisited.isBitSet(n.index)) continue // I shouldn't go where Eli went - yield(k.meMovesEliOpens(n)) - } - for (mn in k.meNeighbors()) { // We both move - if (k.eliVisited.isBitSet(mn.index)) continue // I shouldn't go where Eli went - for (en in k.eliNeighbors()) { - if (k.meVisited.isBitSet(en.index)) continue // Eli shouldn't go where I went - yield(k.weBothMove(mn, en)) + val newStates = HashSet(states.size * 2) + for (state in states) { + if (state.shouldOpen()) newStates.add(state.open()) + for (n in neighbors[state.atValve]!!) { + newStates.add(state.moveTo(n)) } } + println("Minute: $i, states: ${newStates.size}") + states = newStates } - fun findMaxPressureWithElephant(key: MemoKey2): Int { - if (key.minute == 26) return key.openedRate // We're done! + val result1 = states.maxOf { it.totalPressureReleased } + println("Max pressure released: $result1") +} - val cachedMax = memo2[key] - if (cachedMax != null) return cachedMax +private data class ValveState(val atValve: Valve, val opened: Set, val totalPressureReleased: Int) { - val max = key.openedRate + (permutations(key).map(::findMaxPressureWithElephant).maxOrNull() ?: 0) - memo2[key] = max + fun moveTo(v: Valve): ValveState { + val pressureReleased = opened.sumOf(Valve::rate) + return ValveState(v, opened, totalPressureReleased + pressureReleased) + } - if (memo2.size % 1000000 == 0) println("Memo entries: ${memo2.size}") - return max + fun shouldOpen(): Boolean { + return atValve.rate > 0 && !opened.contains(atValve) } - val result2 = findMaxPressureWithElephant(MemoKey2(aa, aa, 0L, 0, 0L, 0L, 1)) - println("Max pressure released with Eli: $result2") + fun open(): ValveState { + val pressureReleased = opened.sumOf(Valve::rate) + return ValveState(atValve, opened.plus(atValve), totalPressureReleased + pressureReleased) + } } -private fun Long.isBitSet(index: Int): Boolean = this and (1L shl index) != 0L -private fun Long.setBit(index: Int): Long = this or (1L shl index) - private data class Valve(val index: Int, val name: String, val rate: Int, val tunnelsTo: List) private val ValveTunnelFormat = Regex("Valve (\\w+) has flow rate=(\\d+); tunnels? leads? to valves? (.*)")