@@ -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 }
0 commit comments