Day 8: Haunted Wasteland
Megathread guidelines
- Keep top level comments as only solutions, if you want to say something other than a solution put it in a new post. (replies to comments can be whatever)
- Code block support is not fully rolled out yet but likely will be in the middle of the event. Try to share solutions as both code blocks and using something such as https://topaz.github.io/paste/ , pastebin, or github (code blocks to future proof it for when 0.19 comes out and since code blocks currently function in some apps and some instances as well if they are running a 0.19 beta)
FAQ
- What is this?: Here is a post with a large amount of details: https://programming.dev/post/6637268
- Where do I participate?: https://adventofcode.com/
- Is there a leaderboard for the community?: We have a programming.dev leaderboard with the info on how to join in this post: https://programming.dev/post/6631465
Scala3
this is still not 100% general, as it assumes there is only one Z-node along the path for each start. But it doesn’t assume anything else, I think. If you brute force combinations of entries of the zs-arrays, this assumption can be trivially removed, but if multiple paths see lots of Z-nodes, then the runtime will grow exponentially. I don’t know if it’s possible to do this any faster.
def parse(a: List[String]): (List[Char], Map[String, Map[Char, String]]) = def parseNodes(n: List[String]) = n.flatMap{case s"$from = ($l, $r)" => Some(from -> Map('L'->l, 'R'->r)); case _ => None}.toMap a match{case instr::""::nodes => ( instr.toList, parseNodes(nodes) ); case _ => (List(), Map())} def task1(a: List[String]): Long = val (instr, nodes) = parse(a) def go(i: Stream[Char], pos: String, n: Long): Long = if pos != "ZZZ" then go(i.tail, nodes(pos)(i.head), n+1) else n go(instr.cycle, "AAA", 0) // ok so i originally thought this was going to be difficult, so // we parse a lot of info we won't use case class PathInfo(zs: List[Long], loopFrom: Long, loopTo: Long): def stride: Long = loopTo - loopFrom type Cycle = Long def getInfo(instr: List[Char], isEnd: String => Boolean, map: Map[String, Map[Char, String]], start: String): PathInfo = def go(i: Cycle, pos: String, is: List[Char], seen: Map[(Long, String), Cycle], acc: List[Long]): PathInfo = val current: (Long, String) = (is.size % instr.size, pos) val nis = if is.isEmpty then instr else is val nacc = if isEnd(pos) then i::acc else acc seen.get(current) match case Some(l) => PathInfo(acc, l, i) case _ => go(i + 1, map(pos)(nis.head), nis.tail, seen + (current -> i), nacc) go(0, start, instr, Map(), List()) // turns out we don't need to check all the different positions // in each cycle where we are on a Z position, as a) every start // walks into unique cycles with b) exactly one Z position, // and c) the length of the cycle is equivalent to the steps to first // encounter a Z (so a->b->c->Z->c->Z->... is already more complicated, // as the cycle length is 2 but the first encounter is after 3 steps) // anyway let's do some math // this is stolen code def gcd(a: Long, b: Long): Long = if(b ==0) a else gcd(b, a%b) def primePowers(x: Long): Map[Long, Long] = // for each prime p for which p^k divides x: p->p^k def go(r: Long, t: Long, acc: Map[Long, Long]): Map[Long, Long] = if r == 1 then acc else if r % t == 0 then go(r/t, t, acc + (t -> acc.getOrElse(t, 1L)*t)) else go(r, t+1, acc) go(x, 2, Map()) // this algorithm is stolen, but I scalafied the impl def vanillaCRT(congruences: Map[Long, Long]): Long = val N = congruences.keys.product val x = congruences.map((n, y) => y*(N/n)*((N/n) % n)).sum if x <= 0 then x + N else x def generalizedHardcoreCRTThatCouldHaveBeenAnLCMBecauseTheInputIsVeryConvenientlyTrivialButWeWantToDoThisRight(ys: List[Long], xs: List[Long]): Option[Long] = // finds the smallest k s.t. k === y_i (mod x_i) for each i // even when stuff is not nice // pre-check if everything is sufficiently coprime // https://math.stackexchange.com/questions/1644677/what-to-do-if-the-modulus-is-not-coprime-in-the-chinese-remainder-theorem val vars = for ((y1, n1), i1) <- ys.zip(xs).zipWithIndex ((y2, n2), i2) <- ys.zip(xs).zipWithIndex if i2 > i1 yield val g = gcd(n1, n2) y1 % g == y2 % g if !vars.forall(a => a) then None else // we need to split k === y (mod mn) into k === y (mod m) and k === y (mod n) for m, n coprime val congruences = for (x, y) <- xs.zip(ys) (p, pl) <- primePowers(x) yield p -> (y % pl -> pl) // now we eliminate redundant congruences // since our input is trivial, this is essentially // the step in which we solve the task by // accidentaly computing an lcm val r = congruences.groupMap(_._1)(_._2).mapValues(l => l.map(_._2).max -> l.head._1).values.toMap // ok now we meet the preconditions // for doing this Some(vanillaCRT(r)) def task2(a: List[String]): Long = val (instr, nodes) = parse(a) val infos = nodes.keySet.filter(_.endsWith("A")).map(s => getInfo(instr, _.endsWith("Z"), nodes, s)) generalizedHardcoreCRTThatCouldHaveBeenAnLCMBecauseTheInputIsVeryConvenientlyTrivialButWeWantToDoThisRight( infos.map(_.zs.head).toList, infos.map(_.stride).toList ).getOrElse(0) @main def main: Unit = val data = readlines("/home/tim/test/aoc23/aoc23/inputs/day8/task1.txt") for f <- List( task1, task2, ).map(_ andThen println) do f(data)