Skip to content

Commit bb65c34

Browse files
committed
Clean up 2025 day 10
1 parent 352a574 commit bb65c34

File tree

2 files changed

+101
-62
lines changed

2 files changed

+101
-62
lines changed

src/main/scala/eu/sim642/adventofcode2025/Day10.scala

Lines changed: 66 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -13,71 +13,86 @@ object Day10 {
1313

1414
case class Machine(lights: Lights, buttons: Buttons, joltages: Joltages)
1515

16-
def fewestPresses(machine: Machine): Int = {
17-
val graphSearch = new GraphSearch[Lights] with UnitNeighbors[Lights] with TargetNode[Lights] {
18-
override val startNode: Lights = machine.lights.map(_ => false)
16+
trait Part {
17+
def fewestPresses(machine: Machine): Int
1918

20-
override def unitNeighbors(lights: Lights): IterableOnce[Lights] =
21-
machine.buttons.map(_.foldLeft(lights)((acc, i) => acc.updated(i, !acc(i))))
19+
def sumFewestPresses(machines: Seq[Machine]): Int = machines.map(fewestPresses).sum
20+
}
2221

23-
override val targetNode: Lights = machine.lights
24-
}
22+
object Part1 extends Part {
23+
override def fewestPresses(machine: Machine): Int = {
24+
val graphSearch = new GraphSearch[Lights] with UnitNeighbors[Lights] with TargetNode[Lights] {
25+
override val startNode: Lights = machine.lights.map(_ => false)
2526

26-
BFS.search(graphSearch).target.get._2
27-
}
27+
override def unitNeighbors(lights: Lights): IterableOnce[Lights] =
28+
machine.buttons.map(_.foldLeft(lights)((acc, i) => acc.updated(i, !acc(i))))
2829

29-
def sumFewestPresses(machines: Seq[Machine]): Int = machines.map(fewestPresses).sum
30+
override val targetNode: Lights = machine.lights
31+
}
3032

31-
/*
32-
x0 x1 x2 x3 x4 x5
33-
(3) (1,3) (2) (2,3) (0,2) (0,1)
34-
0: x4 x5 = 3
35-
1: x1 x5 = 5
36-
2: x2 x3 x4 = 4
37-
3: x0 x3 = 7
33+
BFS.search(graphSearch).target.get._2
34+
}
35+
}
36+
37+
trait Part2Solution extends Part
3838

39+
/**
40+
* Solution, which naively finds fewest presses by BFS.
41+
* Does not scale to inputs.
3942
*/
43+
object NaivePart2Solution extends Part2Solution {
44+
override def fewestPresses(machine: Machine): Int = {
45+
val graphSearch = new GraphSearch[Joltages] with UnitNeighbors[Joltages] with TargetNode[Joltages] {
46+
override val startNode: Joltages = machine.joltages.map(_ => 0)
4047

41-
// TODO: optimize
42-
def fewestPresses2(machine: Machine): Int = {
43-
/*val graphSearch = new GraphSearch[Joltages] with UnitNeighbors[Joltages] with TargetNode[Joltages] {
44-
override val startNode: Joltages = machine.joltages.map(_ => 0)
48+
override def unitNeighbors(joltages: Joltages): IterableOnce[Joltages] =
49+
machine.buttons.map(_.foldLeft(joltages)((acc, i) => acc.updated(i, acc(i) + 1)))
4550

46-
override def unitNeighbors(joltages: Joltages): IterableOnce[Joltages] =
47-
machine.buttons.map(_.foldLeft(joltages)((acc, i) => acc.updated(i, acc(i) + 1)))
51+
override val targetNode: Joltages = machine.joltages
52+
}
4853

49-
override val targetNode: Joltages = machine.joltages
54+
BFS.search(graphSearch).target.get._2
5055
}
56+
}
5157

52-
BFS.search(graphSearch).target.get._2*/
53-
54-
val ctx = new Context(Map("model" -> "true").asJava)
55-
import ctx._
56-
val s = mkOptimize()
57-
58-
val buttonVars = machine.buttons.zipWithIndex.map((_, i) => mkIntConst(s"x$i"))
59-
val lhss =
60-
machine.buttons
61-
.lazyZip(buttonVars)
62-
.foldLeft(machine.joltages.map[ArithExpr[IntSort]](i => mkInt(i)))({ case (accs, (button, buttonVar)) =>
63-
button.foldLeft(accs)((accs, i) => accs.updated(i, mkSub(accs(i), buttonVar)))
58+
/**
59+
* Solution, which finds fewest presses via an ILP problem, solved by Z3.
60+
*/
61+
object Z3Part2Solution extends Part2Solution {
62+
/*
63+
x0 x1 x2 x3 x4 x5
64+
(3) (1,3) (2) (2,3) (0,2) (0,1)
65+
0: x4 x5 = 3
66+
1: x1 x5 = 5
67+
2: x2 x3 x4 = 4
68+
3: x0 x3 = 7
69+
*/
70+
71+
override def fewestPresses(machine: Machine): Int = {
72+
val ctx = new Context(Map("model" -> "true").asJava)
73+
import ctx._
74+
val s = mkOptimize()
75+
76+
val buttonPresses = machine.buttons.zipWithIndex.map((_, i) => mkIntConst(s"x$i"))
77+
for (presses <- buttonPresses)
78+
s.Add(mkGe(presses, mkInt(0)))
79+
80+
val totalPresses = buttonPresses.foldLeft[ArithExpr[IntSort]](mkInt(0))(mkAdd(_, _))
81+
s.MkMinimize(totalPresses)
82+
83+
(machine.buttons lazyZip buttonPresses)
84+
.foldLeft(machine.joltages.map[ArithExpr[IntSort]](mkInt))({ case (acc, (button, presses)) =>
85+
button.foldLeft(acc)((acc, i) => acc.updated(i, mkSub(acc(i), presses)))
6486
})
87+
.foreach(joltageLeft =>
88+
s.Add(mkEq(joltageLeft, mkInt(0)))
89+
)
6590

66-
for (lhs <- lhss)
67-
s.Add(mkEq(lhs, mkInt(0)))
68-
69-
for (v <- buttonVars)
70-
s.Add(mkGe(v, mkInt(0)))
71-
72-
val presses = buttonVars.foldLeft[ArithExpr[IntSort]](mkInt(0))((acc, v) => mkAdd(acc, v))
73-
s.MkMinimize(presses)
74-
assert(s.Check() == Status.SATISFIABLE)
75-
//println(s.getModel)
76-
s.getModel.evaluate(presses, false).toString.toInt
91+
assert(s.Check() == Status.SATISFIABLE)
92+
s.getModel.evaluate(totalPresses, false).toString.toInt
93+
}
7794
}
7895

79-
def sumFewestPresses2(machines: Seq[Machine]): Int = machines.map(fewestPresses2).tapEach(println).sum
80-
8196
def parseMachine(s: String): Machine = s match {
8297
case s"[$lightsStr] $buttonsStr {$joltagesStr}" =>
8398
val lights = lightsStr.map(_ == '#').toVector
@@ -91,7 +106,7 @@ object Day10 {
91106
lazy val input: String = scala.io.Source.fromInputStream(getClass.getResourceAsStream("day10.txt")).mkString.trim
92107

93108
def main(args: Array[String]): Unit = {
94-
println(sumFewestPresses(parseMachines(input)))
95-
println(sumFewestPresses2(parseMachines(input)))
109+
println(Part1.sumFewestPresses(parseMachines(input)))
110+
println(Z3Part2Solution.sumFewestPresses(parseMachines(input)))
96111
}
97112
}
Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,52 @@
11
package eu.sim642.adventofcode2025
22

3-
import Day10._
3+
import Day10.*
4+
import Day10Test.*
5+
import org.scalatest.Suites
46
import org.scalatest.funsuite.AnyFunSuite
57

6-
class Day10Test extends AnyFunSuite {
8+
class Day10Test extends Suites(
9+
new Part1Test,
10+
new NaivePart2SolutionTest,
11+
new Z3Part2SolutionTest,
12+
)
13+
14+
object Day10Test {
715

816
val exampleInput =
917
"""[.##.] (3) (1,3) (2) (2,3) (0,2) (0,1) {3,5,4,7}
1018
|[...#.] (0,2,3,4) (2,3) (0,4) (0,1,2) (1,2,3,4) {7,5,12,7,2}
1119
|[.###.#] (0,1,2,3,4) (0,3,4) (0,1,2,4,5) (1,2) {10,11,11,5,10,5}""".stripMargin
1220

13-
test("Part 1 examples") {
14-
assert(sumFewestPresses(parseMachines(exampleInput)) == 7)
15-
}
21+
class Part1Test extends AnyFunSuite {
1622

17-
test("Part 1 input answer") {
18-
assert(sumFewestPresses(parseMachines(input)) == 449)
23+
test("Part 1 examples") {
24+
assert(Part1.sumFewestPresses(parseMachines(exampleInput)) == 7)
25+
}
26+
27+
test("Part 1 input answer") {
28+
assert(Part1.sumFewestPresses(parseMachines(input)) == 449)
29+
}
1930
}
2031

21-
test("Part 2 examples") {
22-
assert(sumFewestPresses2(parseMachines(exampleInput)) == 33)
32+
abstract class Part2SolutionTest(part2Solution: Part2Solution) extends AnyFunSuite {
33+
34+
test("Part 2 examples") {
35+
assert(part2Solution.sumFewestPresses(parseMachines(exampleInput)) == 33)
36+
}
37+
38+
protected val testInput = true
39+
40+
if (testInput) {
41+
test("Part 2 input answer") {
42+
assert(part2Solution.sumFewestPresses(parseMachines(input)) == 17848)
43+
}
44+
}
2345
}
2446

25-
test("Part 2 input answer") {
26-
assert(sumFewestPresses2(parseMachines(input)) == 17848)
47+
class NaivePart2SolutionTest extends Part2SolutionTest(NaivePart2Solution) {
48+
override protected val testInput: Boolean = false
2749
}
50+
51+
class Z3Part2SolutionTest extends Part2SolutionTest(Z3Part2Solution)
2852
}

0 commit comments

Comments
 (0)