From f7cd11733a3b8d7d723095d0e15ff9db8640db1d Mon Sep 17 00:00:00 2001 From: Juris Date: Thu, 26 Dec 2024 12:21:04 +0200 Subject: [PATCH] Refactoring --- .../jurisk/adventofcode/y2024/Advent24.scala | 71 +++++++++++++------ .../collections/immutable/SetOfTwo.scala | 10 ++- .../adventofcode/y2024/Advent24Spec.scala | 53 ++++++++++++++ 3 files changed, 111 insertions(+), 23 deletions(-) diff --git a/scala2/src/main/scala/jurisk/adventofcode/y2024/Advent24.scala b/scala2/src/main/scala/jurisk/adventofcode/y2024/Advent24.scala index 1c4d231d..61fe1e13 100644 --- a/scala2/src/main/scala/jurisk/adventofcode/y2024/Advent24.scala +++ b/scala2/src/main/scala/jurisk/adventofcode/y2024/Advent24.scala @@ -21,7 +21,7 @@ import scala.util.Random // Notes: // - I actually solved this by simplifying the output DOT file and then finding irregularities manually. -// - Later, I tried to apply a genetic algorithm, but failed to get this to converge. +// - I tried to apply a genetic algorithm, but failed to get this to converge. object Advent24 extends IOApp.Simple { private val InputBits = 45 private val OutputBits = InputBits + 1 @@ -90,7 +90,7 @@ object Advent24 extends IOApp.Simple { } sealed trait Wire extends Product with Serializable - private object Wire { + object Wire { final case class X(i: Int) extends Wire { override def toString: String = f"x$i%02d" } @@ -135,19 +135,19 @@ object Advent24 extends IOApp.Simple { } final case class Connections private (map: Map[Wire, Connection]) { - val allWires: Set[Wire] = map.flatMap { case (k, v) => + val allWires: Set[Wire] = map.flatMap { case (k, v) => Set(k, v.a, v.b) }.toSet - private val allOutputs: Set[Wire] = map.keySet + val allOutputs: Set[Wire] = map.keySet def foreach(f: Connection => Unit): Unit = map.values foreach f - private def errorsOnAddition: Option[Int] = + def errorsOnAddition: Option[Int] = // We care more about `errorsBitByBit`, but since they didn't catch everything, we also care about `errorsOnRandomAddition` - isValid.option(128 * errorsBitByBit + errorsOnRandomAddition) + isValid.option(4096 * errorsBitByBit + errorsOnRandomAddition) private def errorsOnRandomAddition: Int = { - val Samples = 16 + val Samples = 8 (for { _ <- 0 until Samples r = Values.randomXY @@ -194,7 +194,7 @@ object Advent24 extends IOApp.Simple { }.sum } - private def isValid: Boolean = topologicallySortedWires.isDefined + def isValid: Boolean = topologicallySortedWires.isDefined private val topologicallySortedWires: Option[List[Wire]] = { val edges = map.toSeq.flatMap { case (out, c) => @@ -230,22 +230,50 @@ object Advent24 extends IOApp.Simple { new Connections(newMap) } - def fix: (Connections, Set[SetOfTwo[Wire]]) = { - @tailrec + def applySwaps(swaps: Set[SetOfTwo[Wire]]): Connections = + swaps.foldLeft(this) { case (current, swap) => + current.swapOutputs(swap) + } + + private def errorScore(swaps: Set[SetOfTwo[Wire]]): Int = { + val swapped = applySwaps(swaps) + swapped.errorsOnAddition.orFail("Failed to get errors") + } + + def bestSwaps: Set[SetOfTwo[Wire]] = { def f( - current: Connections, currentScore: Int, currentSwaps: Set[SetOfTwo[Wire]], - ): (Connections, Set[SetOfTwo[Wire]]) = { + ): Set[SetOfTwo[Wire]] = { + def backtracking = { + println("Unexpected: No more improvements, trying to backtrack") + val selected = currentSwaps.toIndexedSeq + .combinations(3) + .map(_.toSet) + .filter(applySwaps(_).isValid) + .minBy(attempt => errorScore(attempt)) + val adjusted = applySwaps(selected) + f( + adjusted.errorsOnAddition.orFail("Failed to get errors"), + selected, + ) + } + println(s"Current score: $currentScore, Current swaps: $currentSwaps") if (currentScore == 0) { - (current, currentSwaps) + val ExpectedSwaps = 4 + if (currentSwaps.size == ExpectedSwaps) { + currentSwaps + } else { + backtracking + } } else { // Note: The swaps for our data are: // 1. hbk <-> z14 // 2. kvn <-> z18 // 3. dbb <-> z23 // 4. cvh <-> tfn + val current = applySwaps(currentSwaps) val candidates = current.allOutputs.toIndexedSeq (for { aIdx <- candidates.indices @@ -258,19 +286,18 @@ object Advent24 extends IOApp.Simple { if swapped.isValid } yield (swap, swapped)) .map { case (swap, c) => - (c, c.errorsOnAddition.orFail("Failed to get errors"), swap) + (c.errorsOnAddition.orFail("Failed to get errors"), swap) } - .minBy { case (_, score, _) => score } match { - case (c, score, swap) if score < currentScore => - f(c, score, currentSwaps + swap) - case _ => - println("No more improvements") - (current, currentSwaps) + .minBy { case (score, _) => score } match { + case (score, swap) if score < currentScore => + f(score, currentSwaps + swap) + case _ => + backtracking } } } - f(this, errorsOnAddition.orFail("Failed"), Set.empty) + f(errorsOnAddition.orFail("Failed"), Set.empty) } } @@ -373,7 +400,7 @@ object Advent24 extends IOApp.Simple { def part2(data: Input): String = { val (_, connections) = data - val (_, swaps) = connections.fix + val swaps = connections.bestSwaps swaps .flatMap(_.toSet) diff --git a/scala2/src/main/scala/jurisk/collections/immutable/SetOfTwo.scala b/scala2/src/main/scala/jurisk/collections/immutable/SetOfTwo.scala index cba69bb7..ad87a42b 100644 --- a/scala2/src/main/scala/jurisk/collections/immutable/SetOfTwo.scala +++ b/scala2/src/main/scala/jurisk/collections/immutable/SetOfTwo.scala @@ -10,7 +10,15 @@ final case class SetOfTwo[T](private val underlying: Set[T]) { def toSet: Set[T] = underlying - override def toString: String = tupleInArbitraryOrder.toString() + override def toString: String = { + val (a, b) = tupleInArbitraryOrder + val aStr = a.toString + val bStr = b.toString + val both = Set(aStr, bStr) + val lowest = both.min + val highest = both.max + s"($lowest, $highest)" + } def mapUnsafe[B](f: T => B): SetOfTwo[B] = SetOfTwo(toSet.map(f)) def map[B](f: T => B): Set[B] = toSet.map(f) diff --git a/scala2/src/test/scala/jurisk/adventofcode/y2024/Advent24Spec.scala b/scala2/src/test/scala/jurisk/adventofcode/y2024/Advent24Spec.scala index 8d90c4e8..a4dba770 100644 --- a/scala2/src/test/scala/jurisk/adventofcode/y2024/Advent24Spec.scala +++ b/scala2/src/test/scala/jurisk/adventofcode/y2024/Advent24Spec.scala @@ -1,9 +1,15 @@ package jurisk.adventofcode.y2024 import Advent24._ +import cats.implicits.{catsSyntaxFoldableOps0, catsSyntaxOptionId} +import jurisk.collections.immutable.SetOfTwo +import jurisk.utils.Parsing.StringOps import org.scalatest.freespec.AnyFreeSpec import org.scalatest.matchers.should.Matchers._ +import scala.annotation.tailrec +import scala.util.Random + class Advent24Spec extends AnyFreeSpec { private def testData = parseFile(fileName("-test-00")) private def realData = parseFile(fileName("")) @@ -22,5 +28,52 @@ class Advent24Spec extends AnyFreeSpec { "real" ignore { part2(realData) shouldEqual "cvh,dbb,hbk,kvn,tfn,z14,z18,z23" } + + // Not fully successful... It is often stuck in local minima. + "can fix arbitrary adders" ignore { + val fixed = { + val (_, input) = realData + input.applySwaps( + Set( + SetOfTwo("hbk", "z14"), + SetOfTwo("kvn", "z18"), + SetOfTwo("dbb", "z23"), + SetOfTwo("cvh", "tfn"), + ).map(s => SetOfTwo(s.map(Wire.parse))) + ) + } + fixed.errorsOnAddition shouldEqual 0.some + + val outputs = fixed.allOutputs.toIndexedSeq + + @tailrec + def findValidSwaps(n: Int): Set[SetOfTwo[Wire]] = { + val candidateSwaps = Random + .shuffle(outputs) + .toList + .grouped(2) + .map { + case List(a, b) => SetOfTwo(a, b) + case _ => "Unexpected".fail + } + .take(n) + .toSet + val adjusted = fixed.applySwaps(candidateSwaps) + if (adjusted.isValid) { + candidateSwaps + } else { + findValidSwaps(n) + } + } + + val selectedSwaps = findValidSwaps(4) + + println(s"Selected random swaps: $selectedSwaps") + val wrongAgain = fixed.applySwaps(selectedSwaps) + val resultSwaps = wrongAgain.bestSwaps + resultSwaps shouldEqual selectedSwaps + val result = wrongAgain.applySwaps(resultSwaps) + result.errorsOnAddition shouldEqual 0.some + } } }