|
| 1 | +package me.peckb.aoc._2025.calendar.day11 |
| 2 | + |
| 3 | +import javax.inject.Inject |
| 4 | +import me.peckb.aoc.generators.InputGenerator.InputGeneratorFactory |
| 5 | + |
| 6 | +class Day11 @Inject constructor( |
| 7 | + private val generatorFactory: InputGeneratorFactory, |
| 8 | +) { |
| 9 | + fun partOne(filename: String) = generatorFactory.forFile(filename).readAs(::server) { input -> |
| 10 | + val serverMap = input.plus(Server("out")).associateBy { it.id } |
| 11 | + |
| 12 | + countAllPaths(serverMap, serverMap["you"]!!) |
| 13 | + } |
| 14 | + |
| 15 | + fun partTwo(filename: String) = generatorFactory.forFile(filename).readAs(::server) { input -> |
| 16 | + val serverMap = input.plus(Server("out")).associateBy { it.id } |
| 17 | + |
| 18 | + countAllPaths( |
| 19 | + serverMap = serverMap, |
| 20 | + start = serverMap["svr"]!!, |
| 21 | + mandatoryStops = setOf(serverMap["dac"]!!, serverMap["fft"]!!) |
| 22 | + ) |
| 23 | + } |
| 24 | + |
| 25 | + fun countAllPaths( |
| 26 | + serverMap: Map<String, Server>, |
| 27 | + start: Server, |
| 28 | + end: Server = serverMap["out"]!!, |
| 29 | + mandatoryStops: Set<Server> = emptySet(), |
| 30 | + ): Long { |
| 31 | + // Cache needs to include which mandatory stops have been visited |
| 32 | + val stepsCount = mutableMapOf<Pair<Server, Set<Server>>, Long>() |
| 33 | + // mutable state of our current path |
| 34 | + val currentPath = mutableSetOf<Server>() |
| 35 | + |
| 36 | + fun dfs(current: Server, stopsHit: Set<Server> = emptySet()): Long { |
| 37 | + // AoC is nice, and we don't have loops but uh ... just in case someone on reddit gets squirrelly with their data |
| 38 | + if (current in currentPath) return 0 |
| 39 | + |
| 40 | + // Cache check needs to consider which mandatory stops were visited |
| 41 | + val cacheKey = current to stopsHit |
| 42 | + |
| 43 | + // if we have already found the steps for when we hit this node after already touching |
| 44 | + // some / all of the mandatory stops we need to hit then we can bail out |
| 45 | + // since we only update the cache after we have bottomed out |
| 46 | + stepsCount[cacheKey]?.let { return it } |
| 47 | + |
| 48 | + // If this is stop is a mandatory stop, we don't want to update the |
| 49 | + // steps passed in, so keep it in a new variable |
| 50 | + val updatedStopsHit = if (current in mandatoryStops) { stopsHit + current } else { stopsHit } |
| 51 | + |
| 52 | + val steps = when (current) { |
| 53 | + // Only count if we're at the end AND we've visited all required nodes! |
| 54 | + end if mandatoryStops.size == updatedStopsHit.size -> 1 |
| 55 | + // At end but missing required nodes |
| 56 | + end -> 0 |
| 57 | + // not at the end yet, so keep going... |
| 58 | + else -> { |
| 59 | + // update our current path so our kids know where we have been |
| 60 | + currentPath.add(current) |
| 61 | + |
| 62 | + // find out how many steps we have |
| 63 | + val steps = serverMap[current.id]!!.nextServers.sumOf { dfs(serverMap[it]!!, updatedStopsHit) } |
| 64 | + |
| 65 | + // current path is mutable so don't forget to pop ourselves back off the stack! |
| 66 | + currentPath.remove(current) |
| 67 | + |
| 68 | + steps |
| 69 | + } |
| 70 | + } |
| 71 | + |
| 72 | + // Cache result with the visited required nodes |
| 73 | + stepsCount[cacheKey] = steps |
| 74 | + |
| 75 | + return steps |
| 76 | + } |
| 77 | + |
| 78 | + return dfs(start) |
| 79 | + } |
| 80 | + |
| 81 | + private fun server(line: String): Server { |
| 82 | + val (id, serverList) = line.split(": ") |
| 83 | + return Server(id, (serverList.split(" "))) |
| 84 | + } |
| 85 | +} |
| 86 | + |
| 87 | +data class Server(val id: String, val nextServers: List<String> = emptyList()) { |
| 88 | + override fun toString(): String = id |
| 89 | +} |
0 commit comments