Skip to content

Commit 0fe17f7

Browse files
committed
Extract and test solutions in 2024 day 21
1 parent c1f3b32 commit 0fe17f7

File tree

2 files changed

+157
-123
lines changed

2 files changed

+157
-123
lines changed

src/main/scala/eu/sim642/adventofcode2024/Day21.scala

Lines changed: 120 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -31,145 +31,154 @@ object Day21 {
3131
'<' -> Pos(-1, 0),
3232
)
3333

34-
case class State(directionalPoss: List[Pos], numericPos: Pos, input: Code) {
35-
36-
def numericPress(button: Char): Option[State] = button match {
37-
case 'A' =>
38-
val newButton = numericKeypad(numericPos)
39-
Some(copy(input = input + newButton))
40-
case _ =>
41-
val offset = directionalOffsets(button)
42-
val newNumericPos = numericPos + offset
43-
if (numericKeypad.containsPos(newNumericPos) && numericKeypad(newNumericPos) != ' ')
44-
Some(copy(numericPos = newNumericPos))
45-
else
46-
None // out of keypad
47-
}
34+
trait Solution {
35+
def shortestSequenceLength(code: Code, directionalKeypads: Int): Long
4836

49-
def directionalPress(button: Char): Option[State] = directionalPoss match {
50-
case Nil => numericPress(button)
51-
case directionalPos :: newDirectionalPoss =>
52-
button match {
53-
case 'A' =>
54-
val newButton = directionalKeypad(directionalPos)
55-
copy(directionalPoss = newDirectionalPoss).directionalPress(newButton).map(newState =>
56-
newState.copy(directionalPoss = directionalPos :: newState.directionalPoss)
57-
)
58-
case _ =>
59-
val offset = directionalOffsets(button)
60-
val newDirectionalPos = directionalPos + offset
61-
if (directionalKeypad.containsPos(newDirectionalPos) && directionalKeypad(newDirectionalPos) != ' ')
62-
Some(copy(directionalPoss = newDirectionalPos :: newDirectionalPoss))
63-
else
64-
None // out of keypad
65-
}
37+
def codeComplexity(code: Code, directionalKeypads: Int): Long = {
38+
val numericPart = code.dropRight(1).toInt
39+
shortestSequenceLength(code, directionalKeypads) * numericPart
6640
}
6741

68-
def userPress(button: Char): Option[State] = directionalPress(button)
42+
def sumCodeComplexity(codes: Seq[Code], directionalKeypads: Int): Long = codes.map(codeComplexity(_, directionalKeypads)).sum
6943
}
7044

71-
def shortestSequenceLength(code: Code): Int = {
72-
73-
val graphSearch = new GraphSearch[State] with UnitNeighbors[State] {
74-
override val startNode: State = State(List.fill(2)(directionalKeypad.posOf('A')), numericKeypad.posOf('A'), "")
45+
object NaiveSolution extends Solution {
46+
47+
case class State(directionalPoss: List[Pos], numericPos: Pos, input: Code) {
48+
49+
def numericPress(button: Char): Option[State] = button match {
50+
case 'A' =>
51+
val newButton = numericKeypad(numericPos)
52+
Some(copy(input = input + newButton))
53+
case _ =>
54+
val offset = directionalOffsets(button)
55+
val newNumericPos = numericPos + offset
56+
if (numericKeypad.containsPos(newNumericPos) && numericKeypad(newNumericPos) != ' ')
57+
Some(copy(numericPos = newNumericPos))
58+
else
59+
None // out of keypad
60+
}
7561

76-
override def unitNeighbors(state: State): IterableOnce[State] = "<v>^A".iterator.flatten(state.userPress).filter(s => code.startsWith(s.input))
62+
def directionalPress(button: Char): Option[State] = directionalPoss match {
63+
case Nil => numericPress(button)
64+
case directionalPos :: newDirectionalPoss =>
65+
button match {
66+
case 'A' =>
67+
val newButton = directionalKeypad(directionalPos)
68+
copy(directionalPoss = newDirectionalPoss).directionalPress(newButton).map(newState =>
69+
newState.copy(directionalPoss = directionalPos :: newState.directionalPoss)
70+
)
71+
case _ =>
72+
val offset = directionalOffsets(button)
73+
val newDirectionalPos = directionalPos + offset
74+
if (directionalKeypad.containsPos(newDirectionalPos) && directionalKeypad(newDirectionalPos) != ' ')
75+
Some(copy(directionalPoss = newDirectionalPos :: newDirectionalPoss))
76+
else
77+
None // out of keypad
78+
}
79+
}
7780

78-
override def isTargetNode(state: State, dist: Int): Boolean = state.input == code
81+
def userPress(button: Char): Option[State] = directionalPress(button)
7982
}
8083

81-
BFS.search(graphSearch).target.get._2
82-
}
84+
override def shortestSequenceLength(code: Code, directionalKeypads: Int): Long = {
8385

86+
val graphSearch = new GraphSearch[State] with UnitNeighbors[State] {
87+
override val startNode: State = State(List.fill(directionalKeypads)(directionalKeypad.posOf('A')), numericKeypad.posOf('A'), "")
8488

85-
// copied & modified from 2024 day 10
86-
// TODO: extract to library?
87-
def pathSearch[A](graphSearch: GraphSearch[A] & UnitNeighbors[A]): GraphSearch[List[A]] & UnitNeighbors[List[A]] = {
88-
new GraphSearch[List[A]] with UnitNeighbors[List[A]] {
89-
override val startNode: List[A] = List(graphSearch.startNode)
89+
override def unitNeighbors(state: State): IterableOnce[State] = "<v>^A".iterator.flatten(state.userPress).filter(s => code.startsWith(s.input))
9090

91-
override def unitNeighbors(node: List[A]): IterableOnce[List[A]] =
92-
graphSearch.unitNeighbors(node.head).iterator.map(_ :: node)
91+
override def isTargetNode(state: State, dist: Int): Boolean = state.input == code
92+
}
9393

94-
override def isTargetNode(node: List[A], dist: Int): Boolean = graphSearch.isTargetNode(node.head, dist)
94+
BFS.search(graphSearch).target.get._2
9595
}
9696
}
9797

98-
private def keypadPaths(keypad: Grid[Char]): Map[(Char, Char), Set[Code]] = {
99-
val box = Box(Pos.zero, Pos(keypad(0).size - 1, keypad.size - 1))
100-
(for {
101-
startPos <- box.iterator
102-
if keypad(startPos) != ' '
103-
targetPos <- box.iterator
104-
if keypad(targetPos) != ' '
105-
} yield {
106-
val graphSearch = new GraphSearch[Pos] with UnitNeighbors[Pos] with TargetNode[Pos] {
107-
override val startNode: Pos = startPos
108-
109-
override def unitNeighbors(pos: Pos): IterableOnce[Pos] =
110-
Pos.axisOffsets.map(pos + _).filter(keypad.containsPos).filter(keypad(_) != ' ')
111-
112-
override val targetNode: Pos = targetPos
113-
}
114-
(keypad(targetPos), keypad(startPos)) -> // flipped because paths are reversed
115-
SimultaneousBFS.search(pathSearch(graphSearch))
116-
.nodes
117-
.filter(_.head == targetPos)
118-
.map(poss =>
119-
(poss lazyZip poss.tail)
120-
.map({ case (p2, p1) => directionalOffsets.find(_._2 == p1 - p2).get._1 })
121-
.mkString
122-
)
123-
.toSet
124-
}).toMap
125-
}
98+
object DynamicProgrammingSolution extends Solution {
99+
100+
// copied & modified from 2024 day 10
101+
// TODO: extract to library?
102+
def pathSearch[A](graphSearch: GraphSearch[A] & UnitNeighbors[A]): GraphSearch[List[A]] & UnitNeighbors[List[A]] = {
103+
new GraphSearch[List[A]] with UnitNeighbors[List[A]] {
104+
override val startNode: List[A] = List(graphSearch.startNode)
126105

127-
private val numericPaths: Map[(Char, Char), Set[Code]] = keypadPaths(numericKeypad)
128-
private val directionalPaths: Map[(Char, Char), Set[Code]] = keypadPaths(directionalKeypad)
129-
130-
//println(numericPaths)
131-
132-
def shortestSequenceLength2(code: Code, directionalKeypads: Int, i: Int = 0): Long = {
133-
134-
val memo = mutable.Map.empty[(Code, Int), Long]
135-
136-
def helper(code: Code, i: Int): Long = {
137-
memo.getOrElseUpdate((code, i), {
138-
//assert(directionalKeypads == 0)
139-
code.foldLeft(('A', 0L))({ case ((prev, length), cur) =>
140-
val newLength =
141-
(for {
142-
path <- if (i == 0) numericPaths((prev, cur)) else directionalPaths((prev, cur))
143-
path2 = path + 'A'
144-
len =
145-
if (i == directionalKeypads)
146-
path2.length.toLong
147-
else
148-
helper(path2, i + 1)
149-
} yield len).min
150-
(cur, length + newLength)
151-
})._2
152-
})
106+
override def unitNeighbors(node: List[A]): IterableOnce[List[A]] =
107+
graphSearch.unitNeighbors(node.head).iterator.map(_ :: node)
108+
109+
override def isTargetNode(node: List[A], dist: Int): Boolean = graphSearch.isTargetNode(node.head, dist)
110+
}
153111
}
154112

155-
helper(code, 0)
156-
}
113+
private def keypadPaths(keypad: Grid[Char]): Map[(Char, Char), Set[Code]] = {
114+
val box = Box(Pos.zero, Pos(keypad(0).size - 1, keypad.size - 1))
115+
(for {
116+
startPos <- box.iterator
117+
if keypad(startPos) != ' '
118+
targetPos <- box.iterator
119+
if keypad(targetPos) != ' '
120+
} yield {
121+
val graphSearch = new GraphSearch[Pos] with UnitNeighbors[Pos] with TargetNode[Pos] {
122+
override val startNode: Pos = startPos
123+
124+
override def unitNeighbors(pos: Pos): IterableOnce[Pos] =
125+
Pos.axisOffsets.map(pos + _).filter(keypad.containsPos).filter(keypad(_) != ' ')
126+
127+
override val targetNode: Pos = targetPos
128+
}
129+
(keypad(targetPos), keypad(startPos)) -> // flipped because paths are reversed
130+
SimultaneousBFS.search(pathSearch(graphSearch))
131+
.nodes
132+
.filter(_.head == targetPos)
133+
.map(poss =>
134+
(poss lazyZip poss.tail)
135+
.map({ case (p2, p1) => directionalOffsets.find(_._2 == p1 - p2).get._1 })
136+
.mkString
137+
)
138+
.toSet
139+
}).toMap
140+
}
157141

142+
private val numericPaths: Map[(Char, Char), Set[Code]] = keypadPaths(numericKeypad)
143+
private val directionalPaths: Map[(Char, Char), Set[Code]] = keypadPaths(directionalKeypad)
144+
145+
override def shortestSequenceLength(code: Code, directionalKeypads: Int): Long = {
146+
val memo = mutable.Map.empty[(Code, Int), Long]
147+
148+
def helper(code: Code, i: Int): Long = {
149+
memo.getOrElseUpdate((code, i), {
150+
//assert(directionalKeypads == 0)
151+
code.foldLeft(('A', 0L))({ case ((prev, length), cur) =>
152+
val newLength =
153+
(for {
154+
path <- if (i == 0) numericPaths((prev, cur)) else directionalPaths((prev, cur))
155+
path2 = path + 'A'
156+
len =
157+
if (i == directionalKeypads)
158+
path2.length.toLong
159+
else
160+
helper(path2, i + 1)
161+
} yield len).min
162+
(cur, length + newLength)
163+
})._2
164+
})
165+
}
158166

159-
def codeComplexity(code: Code, directionalKeypads: Int): Long = {
160-
val numericPart = code.dropRight(1).toInt
161-
shortestSequenceLength2(code, directionalKeypads) * numericPart
167+
helper(code, 0)
168+
}
162169
}
163170

164-
def sumCodeComplexity(codes: Seq[Code], directionalKeypads: Int): Long = codes.map(codeComplexity(_, directionalKeypads)).sum
165-
166171
def parseCodes(input: String): Seq[Code] = input.linesIterator.toSeq
167172

168173
lazy val input: String = scala.io.Source.fromInputStream(getClass.getResourceAsStream("day21.txt")).mkString.trim
169174

175+
val part1DirectionalKeypads = 2
176+
val part2DirectionalKeypads = 25
177+
170178
def main(args: Array[String]): Unit = {
171-
println(sumCodeComplexity(parseCodes(input), 2))
172-
println(sumCodeComplexity(parseCodes(input), 25))
179+
import DynamicProgrammingSolution._
180+
println(sumCodeComplexity(parseCodes(input), part1DirectionalKeypads))
181+
println(sumCodeComplexity(parseCodes(input), part2DirectionalKeypads))
173182

174183
// part 2: 1301407762 - too low (Int overflowed in shortestSequenceLength2)
175184
}
Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
package eu.sim642.adventofcode2024
22

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

6-
class Day21Test extends AnyFunSuite {
8+
class Day21Test extends Suites(
9+
new NaiveSolutionTest,
10+
new DynamicProgrammingSolutionTest,
11+
)
12+
13+
object Day21Test {
714

815
val exampleInput =
916
"""029A
@@ -12,19 +19,37 @@ class Day21Test extends AnyFunSuite {
1219
|456A
1320
|379A""".stripMargin
1421

15-
test("Part 1 examples") {
16-
assert(shortestSequenceLength2("029A", 0) == "<A^A>^^AvvvA".length)
17-
assert(shortestSequenceLength2("029A", 1) == "v<<A>>^A<A>AvA<^AA>A<vAAA>^A".length)
18-
assert(shortestSequenceLength2("029A", 2) == "<vA<AA>>^AvAA<^A>A<v<A>>^AvA^A<vA>^A<v<A>^A>AAvA^A<v<A>A>^AAAvA<^A>A".length)
22+
abstract class SolutionTest(solution: Solution) extends AnyFunSuite {
23+
import solution._
1924

20-
assert(sumCodeComplexity(parseCodes(exampleInput), 2) == 126384)
21-
}
25+
test("Part 1 examples") {
26+
assert(shortestSequenceLength("029A", 0) == "<A^A>^^AvvvA".length)
27+
assert(shortestSequenceLength("029A", 1) == "v<<A>>^A<A>AvA<^AA>A<vAAA>^A".length)
28+
assert(shortestSequenceLength("029A", 2) == "<vA<AA>>^AvAA<^A>A<v<A>>^AvA^A<vA>^A<v<A>^A>AAvA^A<v<A>A>^AAAvA<^A>A".length)
29+
30+
assert(sumCodeComplexity(parseCodes(exampleInput), part1DirectionalKeypads) == 126384)
31+
}
32+
33+
test("Part 1 input answer") {
34+
assert(sumCodeComplexity(parseCodes(input), part1DirectionalKeypads) == 157892)
35+
}
2236

23-
test("Part 1 input answer") {
24-
assert(sumCodeComplexity(parseCodes(input), 2) == 157892)
37+
protected val testPart2: Boolean = true
38+
39+
if (testPart2) {
40+
test("Part 2 examples") {
41+
assert(sumCodeComplexity(parseCodes(exampleInput), part2DirectionalKeypads) == 154115708116294L) // not in text
42+
}
43+
44+
test("Part 2 input answer") {
45+
assert(sumCodeComplexity(parseCodes(input), part2DirectionalKeypads) == 197015606336332L)
46+
}
47+
}
2548
}
2649

27-
test("Part 2 input answer") {
28-
assert(sumCodeComplexity(parseCodes(input), 25) == 197015606336332L)
50+
class NaiveSolutionTest extends SolutionTest(NaiveSolution) {
51+
override protected val testPart2: Boolean = false
2952
}
53+
54+
class DynamicProgrammingSolutionTest extends SolutionTest(DynamicProgrammingSolution)
3055
}

0 commit comments

Comments
 (0)