Skip to content

Commit 476a503

Browse files
committed
feat: day 21
1 parent a0974d6 commit 476a503

File tree

4 files changed

+332
-3
lines changed

4 files changed

+332
-3
lines changed

src/main/kotlin/cz/glubo/adventofcode/Main.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ import cz.glubo.adventofcode.y2024.day2.y2024day2part1
7070
import cz.glubo.adventofcode.y2024.day2.y2024day2part2
7171
import cz.glubo.adventofcode.y2024.day20.y2024day20part1
7272
import cz.glubo.adventofcode.y2024.day20.y2024day20part2
73+
import cz.glubo.adventofcode.y2024.day21.y2024day21part1
74+
import cz.glubo.adventofcode.y2024.day21.y2024day21part2
7375
import cz.glubo.adventofcode.y2024.day3.y2024day3part1
7476
import cz.glubo.adventofcode.y2024.day3.y2024day3part2
7577
import cz.glubo.adventofcode.y2024.day4.y2024day4part1
@@ -275,6 +277,8 @@ fun main(args: Array<String>) {
275277
"2024day19p2" to InputToLongCommand { y2024day19part2(it) },
276278
"2024day20p1" to InputToLongCommand { y2024day20part1(it, 100) },
277279
"2024day20p2" to InputToLongCommand { y2024day20part2(it, 100) },
280+
"2024day21p1" to InputToLongCommand { y2024day21part1(it) },
281+
"2024day21p2" to InputToLongCommand { y2024day21part2(it) },
278282
)
279283

280284
val cmd = CommandLine(MyHelpCommand())

src/main/kotlin/cz/glubo/adventofcode/utils/Utils.kt

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,14 @@ enum class Direction(
124124
RIGHT -> UP
125125
}
126126

127+
fun toCommandChar() =
128+
when (this) {
129+
UP -> '^'
130+
DOWN -> 'v'
131+
LEFT -> '<'
132+
RIGHT -> '>'
133+
}
134+
127135
companion object {
128136
fun fromCommandChar(commandChar: Char) =
129137
when (commandChar) {
@@ -262,6 +270,8 @@ fun IntRange.split(boundary: Int): Pair<IntRange?, IntRange?> =
262270

263271
fun Int?.orMax() = this ?: Int.MAX_VALUE
264272

265-
fun <T> List<T>.isSame(other: List<T>) = (this.size == other.size) && this.indices.all {
266-
this[it] == other[it]
267-
}
273+
fun <T> List<T>.isSame(other: List<T>) =
274+
(this.size == other.size) &&
275+
this.indices.all {
276+
this[it] == other[it]
277+
}
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
package cz.glubo.adventofcode.y2024.day21
2+
3+
import cz.glubo.adventofcode.utils.Direction
4+
import cz.glubo.adventofcode.utils.Grid
5+
import cz.glubo.adventofcode.utils.IVec2
6+
import cz.glubo.adventofcode.utils.input.Input
7+
import cz.glubo.adventofcode.y2024.day21.Tile.A
8+
import cz.glubo.adventofcode.y2024.day21.Tile.DOWN
9+
import cz.glubo.adventofcode.y2024.day21.Tile.LAVA
10+
import cz.glubo.adventofcode.y2024.day21.Tile.LEFT
11+
import cz.glubo.adventofcode.y2024.day21.Tile.N0
12+
import cz.glubo.adventofcode.y2024.day21.Tile.N1
13+
import cz.glubo.adventofcode.y2024.day21.Tile.N2
14+
import cz.glubo.adventofcode.y2024.day21.Tile.N3
15+
import cz.glubo.adventofcode.y2024.day21.Tile.N4
16+
import cz.glubo.adventofcode.y2024.day21.Tile.N5
17+
import cz.glubo.adventofcode.y2024.day21.Tile.N6
18+
import cz.glubo.adventofcode.y2024.day21.Tile.N7
19+
import cz.glubo.adventofcode.y2024.day21.Tile.N8
20+
import cz.glubo.adventofcode.y2024.day21.Tile.N9
21+
import cz.glubo.adventofcode.y2024.day21.Tile.RIGHT
22+
import cz.glubo.adventofcode.y2024.day21.Tile.UP
23+
import io.klogging.noCoLogger
24+
import kotlinx.coroutines.flow.toList
25+
import java.util.PriorityQueue
26+
27+
val logger = noCoLogger({}.javaClass.toString())
28+
29+
enum class Tile(
30+
var char: Char,
31+
) {
32+
UP('^'),
33+
DOWN('v'),
34+
LEFT('<'),
35+
RIGHT('>'),
36+
A('A'),
37+
N0('0'),
38+
N1('1'),
39+
N2('2'),
40+
N3('3'),
41+
N4('4'),
42+
N5('5'),
43+
N6('6'),
44+
N7('7'),
45+
N8('8'),
46+
N9('9'),
47+
LAVA('#'),
48+
;
49+
50+
companion object {
51+
fun fromChar(c: Char) = Tile.entries.first { it.char == c }
52+
}
53+
}
54+
55+
data class Path(
56+
val moves: List<Direction>,
57+
val sum: IVec2,
58+
) : Comparable<Path> {
59+
override fun compareTo(other: Path) = this.moves.size.compareTo(other.moves.size)
60+
61+
fun add(direction: Direction) =
62+
Path(
63+
moves + direction,
64+
sum + direction.vector,
65+
)
66+
67+
fun asString() =
68+
moves.joinToString("") {
69+
it.toCommandChar().toString()
70+
}
71+
}
72+
73+
class Layer(
74+
val keyboard: Grid<Tile>,
75+
val lowerLayer: Layer?,
76+
val layerName: String = "layer",
77+
) {
78+
private val pathCache = mutableMapOf<Pair<IVec2, IVec2>, List<Path>>()
79+
private val movesCache = mutableMapOf<String, List<String>>()
80+
private val shortestCache = mutableMapOf<String, Long>()
81+
82+
fun getShortestLength(input: String): Long =
83+
shortestCache.getOrPut(input) {
84+
input
85+
.split("A")
86+
.dropLast(1)
87+
.sumOf { innerMovelet ->
88+
val movelet = "${innerMovelet}A"
89+
logger.debug { "$layerName movelet: $movelet" }
90+
91+
getMovesForInput(movelet).minOf { moves ->
92+
lowerLayer?.getShortestLength(moves) ?: moves.length.toLong()
93+
}
94+
}
95+
}
96+
97+
fun getMovesForInput(input: String): List<String> =
98+
movesCache.getOrPut(input) {
99+
logger.debug { "$layerName hydrating: $input" }
100+
var position = getPosition(A)
101+
var outputs = listOf("")
102+
103+
input.forEach { c ->
104+
var targetPos =
105+
getPosition(
106+
Tile.fromChar(c),
107+
)
108+
109+
val moves = shortestPaths(position, targetPos)
110+
outputs =
111+
outputs.flatMap { o ->
112+
moves
113+
.map { path ->
114+
o + path.asString()
115+
}
116+
}
117+
118+
outputs = outputs.map { it + "A" }
119+
position = targetPos
120+
}
121+
logger.debug { "$layerName hydrated $input: $outputs" }
122+
return@getOrPut outputs
123+
}
124+
125+
private fun shortestPaths(
126+
position: IVec2,
127+
targetPos: IVec2,
128+
): List<Path> =
129+
pathCache.getOrPut(position to targetPos) {
130+
val paths = PriorityQueue<Path>()
131+
132+
paths.add(Path(listOf(), IVec2(0, 0)))
133+
134+
val result = mutableListOf<Path>()
135+
136+
fun shortestLengthSoFar() = (result.firstOrNull()?.moves?.size ?: Int.MAX_VALUE)
137+
do {
138+
val path = paths.poll()
139+
val headPos = position + path.sum
140+
val head = keyboard[headPos]
141+
when {
142+
path.moves.size > shortestLengthSoFar() -> Unit
143+
headPos == targetPos -> result.add(path)
144+
head == null -> Unit
145+
head == LAVA -> Unit
146+
else ->
147+
paths.addAll(
148+
Direction.entries.map {
149+
path.add(it)
150+
},
151+
)
152+
}
153+
} while (paths.isNotEmpty())
154+
155+
return@getOrPut result
156+
}
157+
158+
private fun getPosition(t: Tile) =
159+
keyboard.allIVec2().first {
160+
keyboard[it] == t
161+
}
162+
}
163+
164+
private fun numericGrid(): Grid<Tile> {
165+
val grid =
166+
Grid(
167+
width = 3,
168+
height = 4,
169+
fields =
170+
buildList {
171+
repeat(3 * 4) {
172+
add(LAVA)
173+
}
174+
}.toMutableList(),
175+
)
176+
grid[IVec2(0, 0)] = N7
177+
grid[IVec2(1, 0)] = N8
178+
grid[IVec2(2, 0)] = N9
179+
grid[IVec2(0, 1)] = N4
180+
grid[IVec2(1, 1)] = N5
181+
grid[IVec2(2, 1)] = N6
182+
grid[IVec2(0, 2)] = N1
183+
grid[IVec2(1, 2)] = N2
184+
grid[IVec2(2, 2)] = N3
185+
grid[IVec2(0, 3)] = LAVA
186+
grid[IVec2(1, 3)] = N0
187+
grid[IVec2(2, 3)] = A
188+
return grid
189+
}
190+
191+
private fun arrowGrid(): Grid<Tile> {
192+
val grid =
193+
Grid<Tile>(
194+
width = 3,
195+
height = 2,
196+
fields =
197+
buildList {
198+
repeat(3 * 2) {
199+
add(LAVA)
200+
}
201+
}.toMutableList(),
202+
)
203+
grid[IVec2(0, 0)] = LAVA
204+
grid[IVec2(1, 0)] = UP
205+
grid[IVec2(2, 0)] = A
206+
grid[IVec2(0, 1)] = LEFT
207+
grid[IVec2(1, 1)] = DOWN
208+
grid[IVec2(2, 1)] = RIGHT
209+
return grid
210+
}
211+
212+
fun buildLayers(n: Int): Layer {
213+
var currentLayer: Layer? = null
214+
215+
repeat(n) { i ->
216+
currentLayer =
217+
Layer(
218+
arrowGrid(),
219+
currentLayer,
220+
"arrows-$i",
221+
)
222+
}
223+
224+
return Layer(numericGrid(), currentLayer, "numeric")
225+
}
226+
227+
suspend fun y2024day21part1(input: Input): Long {
228+
logger.info("year 2024 day 21 part 1")
229+
val layers = buildLayers(2)
230+
return input
231+
.lineFlow()
232+
.toList()
233+
.sumOf { line ->
234+
getIntValue(line) * layers.getShortestLength(line)
235+
}
236+
}
237+
238+
suspend fun y2024day21part2(input: Input): Long {
239+
logger.info("year 2024 day 21 part 2")
240+
val layers = buildLayers(25)
241+
return input
242+
.lineFlow()
243+
.toList()
244+
.sumOf { line ->
245+
getIntValue(line) * layers.getShortestLength(line)
246+
}
247+
}
248+
249+
private fun getIntValue(line: String) =
250+
line
251+
.dropWhile { it == '0' }
252+
.removeSuffix("A")
253+
.toInt()
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package cz.glubo.adventofcode.y2024.day21
2+
3+
import cz.glubo.adventofcode.utils.input.TestInput
4+
import io.kotest.core.spec.style.StringSpec
5+
import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder
6+
import io.kotest.matchers.shouldBe
7+
8+
/**
9+
* https://adventofcode.com/2024/day/21
10+
*/
11+
class Day21Test :
12+
StringSpec({
13+
14+
"Numeric Keyboard example" {
15+
val output = buildLayers(0).getMovesForInput("029A")
16+
output shouldContainExactlyInAnyOrder
17+
listOf(
18+
"<A^A>^^AvvvA",
19+
"<A^A^>^AvvvA",
20+
"<A^A^^>AvvvA",
21+
)
22+
}
23+
24+
"Numeric Keyboard example a" {
25+
val output = buildLayers(0).getShortestLength("029A")
26+
output shouldBe "<A^A>^^AvvvA".length
27+
}
28+
29+
"asdasd" {
30+
}
31+
32+
"Layer Keyboard example" {
33+
val output = buildLayers(1).getShortestLength("029A")
34+
output shouldBe "v<<A>>^A<A>AvA<^AA>A<vAAA>^A".length
35+
}
36+
"Layer 2 Keyboard example" {
37+
val output = buildLayers(2).getShortestLength("029A")
38+
output shouldBe "<vA<AA>>^AvAA<^A>A<v<A>>^AvA^A<vA>^A<v<A>^A>AAvA^A<v<A>A>^AAAvA<^A>A".length
39+
}
40+
41+
"day21 example part 1 matches" {
42+
y2024day21part1(
43+
TestInput(
44+
"""
45+
029A
46+
""".trimIndent(),
47+
),
48+
) shouldBe 68 * 29
49+
50+
y2024day21part1(
51+
TestInput(
52+
"""
53+
029A
54+
980A
55+
179A
56+
456A
57+
379A
58+
""".trimIndent(),
59+
),
60+
) shouldBe 126384
61+
}
62+
})

0 commit comments

Comments
 (0)