Skip to content

Commit 0b278d9

Browse files
authored
Day 20 2024 (#283)
1 parent ae9fffd commit 0b278d9

File tree

5 files changed

+147
-1
lines changed

5 files changed

+147
-1
lines changed

src/main/kotlin/me/peckb/aoc/_2024/calendar/day16/Day16.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,10 @@ data class Room(
158158
enum class Space { FULL, EMPTY }
159159

160160
enum class Direction(val yDelta: Int, val xDelta: Int) {
161-
N(-1, 0), E(0, 1), S(1, 0), W(0, -1)
161+
N(-1, 0),
162+
E(0, 1),
163+
S(1, 0),
164+
W(0, -1)
162165
}
163166

164167
private fun Direction?.turnCost(direction: Direction): Long {
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package me.peckb.aoc._2024.calendar.day20
2+
3+
import javax.inject.Inject
4+
import me.peckb.aoc.generators.InputGenerator.InputGeneratorFactory
5+
import me.peckb.aoc.pathing.GenericIntDijkstra
6+
import kotlin.math.abs
7+
8+
class Day20 @Inject constructor(
9+
private val generatorFactory: InputGeneratorFactory,
10+
) {
11+
fun partOne(filename: String) = generatorFactory.forFile(filename).read { input ->
12+
findCheats(input, 2)
13+
}
14+
15+
fun partTwo(filename: String) = generatorFactory.forFile(filename).read { input ->
16+
findCheats(input, 20)
17+
}
18+
19+
private fun findCheats(input: Sequence<String>, maxCheatTime: Int): Int {
20+
lateinit var end: Location
21+
22+
val maze = mutableListOf<MutableList<Space>>()
23+
input.forEachIndexed { y, line ->
24+
val row = mutableListOf<Space>()
25+
line.forEachIndexed { x, c ->
26+
when (c) {
27+
'#' -> row.add(Space.FULL)
28+
'.' -> row.add(Space.EMPTY)
29+
'S' -> row.add(Space.EMPTY)
30+
'E' -> row.add(Space.EMPTY.also { end = Location(y, x) } )
31+
}
32+
}
33+
maze.add(row)
34+
}
35+
36+
val solver = object : GenericIntDijkstra<Location>() {}
37+
38+
val costs = solver.solve(end.withArea(maze))
39+
var cheats = 0
40+
41+
val explorationRange = (-maxCheatTime .. maxCheatTime)
42+
val explorationDeltas = explorationRange.flatMapIndexed { y, yStep ->
43+
explorationRange.mapIndexedNotNull { x, xStep ->
44+
val stepCount = abs(yStep) + abs(xStep)
45+
if (stepCount <= maxCheatTime) {
46+
yStep to xStep
47+
} else {
48+
null
49+
}
50+
}
51+
}
52+
53+
costs.entries.forEach { (cheatStart, _) ->
54+
val reachableEmptySpaces = mutableSetOf<Location>()
55+
56+
val curY = cheatStart.y
57+
val curX = cheatStart.x
58+
59+
explorationDeltas.forEach { (yStep, xStep) ->
60+
val y = curY + yStep
61+
val x = curX + xStep
62+
63+
if (y in maze.indices && x in maze[y].indices && maze[y][x] == Space.EMPTY) {
64+
reachableEmptySpaces.add(Location(y, x))
65+
}
66+
}
67+
68+
reachableEmptySpaces.forEach { endSpace ->
69+
val costAtCheatStart = costs[cheatStart]!!
70+
val costAtCheatEnd = costs[endSpace]!!
71+
val distanceTravelled = cheatStart.distanceFrom(endSpace)
72+
73+
val timeSaved = costAtCheatStart - costAtCheatEnd - distanceTravelled
74+
if (timeSaved >= 100) { cheats++ }
75+
}
76+
}
77+
78+
return cheats
79+
}
80+
}
81+
82+
data class Location(val y: Int, val x: Int) : GenericIntDijkstra.DijkstraNode<Location> {
83+
fun distanceFrom(other: Location): Int {
84+
return abs(other.y - y) + abs(other.x - x)
85+
}
86+
87+
lateinit var area: MutableList<MutableList<Space>>
88+
89+
fun withArea(area: MutableList<MutableList<Space>>) = apply { this.area = area }
90+
91+
override fun neighbors(): Map<Location, Int> {
92+
return Direction.entries.mapNotNull { d ->
93+
val (newY, newX) = d.yDelta + y to d.xDelta + x
94+
if (newY in area.indices && newX in area[newY].indices && area[newY][newX] == Space.EMPTY) {
95+
Location(newY, newX).withArea(area)
96+
} else { null }
97+
}.associateWith { 1 }
98+
}
99+
}
100+
101+
enum class Space { FULL, EMPTY }
102+
103+
enum class Direction(val yDelta: Int, val xDelta: Int) {
104+
N(-1, 0),
105+
E(0, 1),
106+
S(1, 0),
107+
W(0, -1);
108+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
## [Day 20: Race Condition](https://adventofcode.com/2024/day/20)

src/test/kotlin/me/peckb/aoc/_2024/TestDayComponent.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import me.peckb.aoc._2024.calendar.day16.Day16Test
1919
import me.peckb.aoc._2024.calendar.day17.Day17Test
2020
import me.peckb.aoc._2024.calendar.day18.Day18Test
2121
import me.peckb.aoc._2024.calendar.day19.Day19Test
22+
import me.peckb.aoc._2024.calendar.day20.Day20Test
2223
import javax.inject.Singleton
2324
import me.peckb.aoc.DayComponent
2425
import me.peckb.aoc.InputModule
@@ -46,4 +47,5 @@ internal interface TestDayComponent : DayComponent {
4647
fun inject(day17Test: Day17Test)
4748
fun inject(day18Test: Day18Test)
4849
fun inject(day19Test: Day19Test)
50+
fun inject(day20Test: Day20Test)
4951
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package me.peckb.aoc._2024.calendar.day20
2+
3+
import javax.inject.Inject
4+
5+
import me.peckb.aoc._2024.DaggerTestDayComponent
6+
import org.junit.jupiter.api.Assertions.assertEquals
7+
import org.junit.jupiter.api.BeforeEach
8+
import org.junit.jupiter.api.Test
9+
10+
internal class Day20Test {
11+
@Inject
12+
lateinit var day20: Day20
13+
14+
@BeforeEach
15+
fun setup() {
16+
DaggerTestDayComponent.create().inject(this)
17+
}
18+
19+
@Test
20+
fun testDay20PartOne() {
21+
assertEquals(1317, day20.partOne(DAY_20))
22+
}
23+
24+
@Test
25+
fun testDay20PartTwo() {
26+
assertEquals(982474, day20.partTwo(DAY_20))
27+
}
28+
29+
companion object {
30+
private const val DAY_20: String = "advent-of-code-input/2024/day20.input"
31+
}
32+
}

0 commit comments

Comments
 (0)