Skip to content

Commit fa652c5

Browse files
committed
Simplify day 21 solution
1 parent ba027c9 commit fa652c5

File tree

1 file changed

+15
-232
lines changed

1 file changed

+15
-232
lines changed

day21/src/day21.scala

Lines changed: 15 additions & 232 deletions
Original file line numberDiff line numberDiff line change
@@ -1,224 +1,12 @@
11
import scala.io.Source
22
import scala.collection.mutable
33

4-
case class Vec2(x: Int, y: Int) {
5-
def neighbors = (-1 to 1).flatMap { dy =>
6-
(-1 to 1)
7-
.filter { dx => (dx != 0) ^ (dy != 0) }
8-
.map { dx => new Vec2(x + dx, y + dy) }
9-
}
10-
11-
def +(that: Vec2) = Vec2(x + that.x, y + that.y)
12-
13-
def manhattanDist(that: Vec2) = (x - that.x).abs + (y - that.y).abs
14-
}
15-
16-
enum PadType:
17-
case Num, Dir
18-
19-
val PAD_LAYOUTS = Map(
20-
(PadType.Num, Map(
21-
(Vec2(0, 0), '7'), (Vec2(1, 0), '8'), (Vec2(2, 0), '9'),
22-
(Vec2(0, 1), '4'), (Vec2(1, 1), '5'), (Vec2(2, 1), '6'),
23-
(Vec2(0, 2), '1'), (Vec2(1, 2), '2'), (Vec2(2, 2), '3'),
24-
(Vec2(1, 3), '0'), (Vec2(2, 3), 'A'),
25-
)),
26-
(PadType.Dir, Map(
27-
(Vec2(1, 0), '^'), (Vec2(2, 0), 'A'),
28-
(Vec2(0, 1), '<'), (Vec2(1, 1), 'v'), (Vec2(2, 1), '>'),
29-
)),
30-
)
31-
32-
extension (ptype: PadType) {
33-
def layout = PAD_LAYOUTS(ptype)
34-
35-
def locate(c: Char) = ptype.layout.find(_._2 == c).get._1
36-
37-
def shortestPath(startPos: Vec2, endPos: Vec2): String =
38-
case class Node(pos: Vec2, index: Int, program: String = "") extends Ordered[Node] {
39-
def cost = program.length * 1_000_000 + turns * 1_000 + index
40-
41-
def turns = program.zip(program.tail).count { case (c1, c2) => c1 != c2 }
42-
43-
def compare(that: Node): Int = that.cost compare cost // Intentionally reversed for min-heap
44-
}
45-
46-
val queue = mutable.PriorityQueue[Node]()
47-
val visited = mutable.Set[Vec2]()
48-
var index = 0
49-
50-
queue.enqueue(Node(startPos, index))
51-
52-
while !queue.isEmpty do
53-
val node = queue.dequeue()
54-
if node.pos == endPos then
55-
// Annoying edge cases, what rule do they follow?
56-
return Map(
57-
(">>^", "^>>"),
58-
("vv<", "<vv"),
59-
("vv>v", ">vvv"),
60-
(">>v", "v>>"),
61-
("vvv<", "<vvv"),
62-
("vv<", "<vv"),
63-
("<^<", "^<<"),
64-
("^^<", "<^^"),
65-
("^^^<", "<^^^"),
66-
("<v<", "v<<"),
67-
("<^^", "^<^"),
68-
("<^^^", "^^^<"),
69-
("v>>", ">>v"),
70-
("^>>", ">>^"),
71-
).getOrElse(node.program, node.program)
72-
.appended('A')
73-
74-
for action <- List('<', '^', 'v', '>') do
75-
val dir = DIRECTIONS(action)
76-
val neigh = node.pos + dir
77-
if layout.contains(neigh) && !visited.contains(neigh) then
78-
visited.add(neigh)
79-
queue.enqueue(Node(neigh, index, node.program.appended(action)))
80-
index += 1
81-
82-
throw RuntimeException("No shortest program found")
83-
84-
def shortestPaths: Map[(Char, Char), String] =
85-
layout.flatMap { case (p1, a1) => layout.map { case (p2, a2) => ((a1, a2), shortestPath(p1, p2)) } }.toMap
86-
}
87-
88-
val DIRECTIONS = Map(
89-
('<', Vec2(-1, 0)),
90-
('>', Vec2( 1, 0)),
91-
('^', Vec2( 0, -1)),
92-
('v', Vec2( 0, 1)),
93-
)
94-
95-
val ACTIONS = List('A') ++ DIRECTIONS.keySet
96-
97-
case class Pad(ptype: PadType, pos: Vec2) {
98-
def layout = ptype.layout
99-
100-
def activate: Char = layout(pos)
101-
102-
def isValid = layout.contains(pos)
103-
104-
def perform(action: Char): (Option[Char], Pad) =
105-
action match
106-
case 'A' => (Some(activate), this)
107-
case _ => (None, Pad(ptype, pos + DIRECTIONS(action)))
108-
}
109-
110-
object Pad {
111-
def apply(ptype: PadType): Pad = Pad(ptype, ptype.locate('A'))
112-
}
113-
114-
def shortestProgram(robots: Int, goal: String): String =
115-
case class State(pads: List[Pad] = List.fill(robots)(Pad(PadType.Dir)) :+ Pad(PadType.Num), output: String = "") {
116-
def perform(action: Char) =
117-
for
118-
(newPads, outAction) <- pads.foldLeft[Option[(List[Pad], Option[Char])]](Some((List(), Some(action)))) { (acc, pad) =>
119-
acc.flatMap { case (pads, action) =>
120-
action match
121-
case Some(action) =>
122-
for
123-
(newAction, newPad) <- Some(pad.perform(action))
124-
if newPad.isValid
125-
yield (pads :+ newPad, newAction)
126-
case None => Some((pads :+ pad, None))
127-
}
128-
}
129-
yield
130-
val newOutput = outAction.map(output.appended(_)).getOrElse(output)
131-
State(newPads, newOutput)
132-
}
133-
134-
case class Node(state: State = State(), program: String = "") extends Ordered[Node] {
135-
def compare(that: Node): Int = that.program.length compare program.length // Intentionally reversed for min-heap
136-
}
137-
138-
// Your run-of-the-mill Dijkstra implementation
139-
140-
val queue = mutable.PriorityQueue[Node]()
141-
val visited = mutable.Set[State]()
142-
143-
val startState = State()
144-
val start = Node(startState)
145-
queue.enqueue(start)
146-
visited.add(startState)
147-
148-
while !queue.isEmpty do
149-
val node = queue.dequeue()
150-
if node.state.output == goal then
151-
return node.program
152-
153-
if node.state.output.length < goal.length then
154-
for
155-
action <- ACTIONS
156-
newState <- node.state.perform(action)
157-
do
158-
if !visited.contains(newState) then
159-
visited.add(newState)
160-
queue.enqueue(Node(newState, node.program.appended(action)))
161-
162-
throw RuntimeException("No shortest program found")
163-
164-
def shortestProgramLength(robots: Int, goal: String): Int =
165-
case class State(pos: Vec2 = PadType.Num.locate('A'), dPos: Vec2 = PadType.Dir.locate('A'), output: String = "")
166-
167-
case class Node(state: State, total: Int = 0) extends Ordered[Node] {
168-
def compare(that: Node): Int = that.total compare total // Intentionally reversed for min-heap
169-
}
170-
171-
def cost(robots: Int, pos: Vec2, dPos: Vec2, action: Char): (Int, Vec2) =
172-
if robots <= 0 then
173-
(1, dPos)
174-
else
175-
// Only considering robots = 1 for now
176-
val targetDPos = PadType.Dir.locate(action)
177-
val steps = dPos.manhattanDist(targetDPos) + 1 // needs 'A' press
178-
// println(s"$dPos -> $targetDPos ('${PAD_LAYOUTS(PadType.Dir)(dPos)}' ${steps} -> '${PAD_LAYOUTS(PadType.Dir)(targetDPos)}')")
179-
(steps, targetDPos)
180-
181-
// Your run-of-the-mill Dijkstra implementation (this time on the numpad)
182-
183-
val queue = mutable.PriorityQueue[Node]()
184-
val visited = mutable.Set[State]()
185-
186-
val startState = State()
187-
val start = Node(startState)
188-
queue.enqueue(start)
189-
visited.add(startState)
190-
191-
while !queue.isEmpty do
192-
val node = queue.dequeue()
193-
if node.state.output == goal then
194-
return node.total
195-
196-
if node.state.output.length < goal.length then
197-
for
198-
action <- ACTIONS
199-
do
200-
val newPos = node.state.pos + DIRECTIONS.get(action).getOrElse(Vec2(0, 0))
201-
if PAD_LAYOUTS(PadType.Num).contains(newPos) then
202-
val (c, newDPos) = cost(robots, node.state.pos, node.state.dPos, action)
203-
val newOutput = if action == 'A' then node.state.output.appended(PAD_LAYOUTS(PadType.Num)(node.state.pos)) else node.state.output
204-
val newState = State(newPos, newDPos, newOutput)
205-
if !visited.contains(newState) then
206-
visited.add(newState)
207-
queue.enqueue(Node(newState, node.total + c))
208-
209-
throw RuntimeException("No shortest program found")
210-
211-
def solve(robots: Int, goals: List[String], func: (Int, String) => Long): Long =
212-
goals.map { goal =>
213-
val shortest = func(robots, goal)
214-
shortest * goal.dropRight(1).toLong
215-
}.sum
216-
217-
// Algorithm/approach is effectively a Scala port of
4+
// The algorithm/approach is effectively a Scala port of
2185
// https://www.reddit.com/r/adventofcode/comments/1hj2odw/comment/m36j01x For
2196
// some reason my Dijkstra-based shortest-path constructions didn't yield the
220-
// specific/right ordering, so I eventually just ended up using the hardcoded
221-
// map too. Most of the experiments can be found in earlier commits.
7+
// specific/right ordering, so, after long (and frustrating) experimentation, I
8+
// eventually just ended up using that hardcoded map too. Most of the
9+
// experiments can be found in earlier commits.
22210

22311
val SHORTEST_PATHS = Map(
22412
(('A', '0'), "<A"),
@@ -355,7 +143,7 @@ val SHORTEST_PATHS = Map(
355143

356144
val memo = mutable.Map[(Int, String), Long]()
357145

358-
def shortestProgramLengthClever(robots: Int, goal: String): Long =
146+
def shortestProgramLength(robots: Int, goal: String): Long =
359147
val key = (robots, goal)
360148
if memo.contains(key) then
361149
return memo(key)
@@ -369,29 +157,24 @@ def shortestProgramLengthClever(robots: Int, goal: String): Long =
369157
length += moveCount(robots, current, next)
370158
current = next
371159
length
160+
372161
memo(key) = result
373162
result
374163

375164
def moveCount(robots: Int, current: Char, next: Char): Long =
376165
if current == next then
377166
1
378167
else
379-
shortestProgramLengthClever(robots - 1, SHORTEST_PATHS((current, next)))
168+
shortestProgramLength(robots - 1, SHORTEST_PATHS((current, next)))
169+
170+
def solve(robots: Int, goals: List[String]): Long =
171+
goals.map { goal =>
172+
val shortest = shortestProgramLength(robots, goal)
173+
shortest * goal.dropRight(1).toLong
174+
}.sum
380175

381176
@main def main(path: String) =
382177
val goals = Source.fromFile(path).getLines.toList
383-
// println(s"Part 1: ${solve(2, goals)}")
384-
// println(s"Part 2: ${solve(25, goals)}")
385-
386-
// for (k, p) <- SHORTEST_PATHS.toList.sorted do
387-
// if k._1 != k._2 && p != SHORTEST_PATHS_2(k) then
388-
// println(s"(\"$p\", \"${SHORTEST_PATHS_2(k)}\")")
389-
390-
println(s"Part 1: ${solve(2, goals, shortestProgramLengthClever)}")
391-
println(s"Part 2: ${solve(25, goals, shortestProgramLengthClever)}")
392-
393-
// for i <- (0 to 3) do
394-
// println(s"${solve(i, goals, { (r, g) => shortestProgram(r, g).length })} vs ${solve(i, goals, shortestProgramLength)} vs ${solve(i, goals, shortestProgramLengthClever)}")
395178

396-
// for c <- ('0' to '5') do
397-
// println(s"$c -> ${(0 to 3).map { i => shortestProgram(makeState(i), s"$c").length }}")
179+
println(s"Part 1: ${solve(2, goals)}")
180+
println(s"Part 2: ${solve(25, goals)}")

0 commit comments

Comments
 (0)