Skip to content

Commit 08ad317

Browse files
authored
Day 16 2024 (#277)
* Day 16 2024
1 parent ed23883 commit 08ad317

File tree

4 files changed

+254
-0
lines changed

4 files changed

+254
-0
lines changed
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
package me.peckb.aoc._2024.calendar.day16
2+
3+
import me.peckb.aoc._2024.calendar.day16.Direction.E
4+
import me.peckb.aoc._2024.calendar.day16.Direction.N
5+
import me.peckb.aoc._2024.calendar.day16.Direction.S
6+
import me.peckb.aoc._2024.calendar.day16.Direction.W
7+
import javax.inject.Inject
8+
import me.peckb.aoc.generators.InputGenerator.InputGeneratorFactory
9+
import me.peckb.aoc.pathing.Dijkstra
10+
import me.peckb.aoc.pathing.DijkstraNodeWithCost
11+
import java.util.PriorityQueue
12+
13+
class Day16 @Inject constructor(
14+
private val generatorFactory: InputGeneratorFactory,
15+
) {
16+
fun partOne(filename: String) = generatorFactory.forFile(filename).read { input ->
17+
lateinit var start: Room
18+
lateinit var end: Room
19+
20+
val maze = mutableListOf<MutableList<Room>>()
21+
input.forEachIndexed { y, line ->
22+
val row = mutableListOf<Room>()
23+
line.forEachIndexed { x, c ->
24+
when (c) {
25+
'#' -> row.add(Room(y, x, space = Space.FULL))
26+
'.' -> row.add(Room(y, x))
27+
'S' -> row.add(Room(y, x).also { start = it; it.direction = E })
28+
'E' -> row.add(Room(y, x).also { end = it })
29+
}
30+
}
31+
maze.add(row)
32+
}
33+
34+
val solver = MazeDijkstra(maze)
35+
val solutions = solver.solve(start)
36+
37+
solutions[end]
38+
}
39+
40+
fun partTwo(filename: String) = generatorFactory.forFile(filename).read { input ->
41+
lateinit var start: Pair<Int, Int>
42+
lateinit var end: Pair<Int, Int>
43+
44+
val maze = mutableListOf<MutableList<Space>>()
45+
input.forEachIndexed { y, line ->
46+
val row = mutableListOf<Space>()
47+
line.forEachIndexed { x, c ->
48+
when (c) {
49+
'#' -> row.add(Space.FULL)
50+
'.' -> row.add(Space.EMPTY)
51+
'S' -> row.add(Space.EMPTY.also { start = y to x } )
52+
'E' -> row.add(Space.EMPTY.also { end = y to x } )
53+
}
54+
}
55+
maze.add(row)
56+
}
57+
58+
val visitedWithCheapestCost = mutableMapOf<Triple<Int, Int, Direction>, Long>()
59+
val visited = mutableSetOf<SpaceWithPath>()
60+
val toVisit = PriorityQueue<SpaceWithPath>()
61+
62+
toVisit.add(SpaceWithPath(start, end, listOf(start)))
63+
loop@ while(toVisit.isNotEmpty()) {
64+
val current = toVisit.poll()
65+
66+
if (current.loc == end) {
67+
visited.add(current)
68+
continue@loop
69+
}
70+
val myDirection = SpaceWithPath.findDirection(current.path)
71+
val key = Triple(current.loc.first, current.loc.second, myDirection)
72+
if (visitedWithCheapestCost.getOrDefault(key, Long.MAX_VALUE) < current.cost) {
73+
continue@loop
74+
} else {
75+
visitedWithCheapestCost[key] = current.cost
76+
}
77+
78+
val n = (-1 to 0)
79+
val e = (0 to 1)
80+
val s = (1 to 0)
81+
val w = (0 to -1)
82+
83+
listOf(n, s, e, w)
84+
.asSequence()
85+
.map { (yDelta, xDelta) ->
86+
current.loc.first + yDelta to current.loc.second + xDelta
87+
}
88+
.filter { (y, x) ->
89+
y in maze.indices && x in maze[y].indices && maze[y][x] == Space.EMPTY && !current.path.contains(y to x)
90+
}
91+
.forEach { (y, x) ->
92+
toVisit.add(SpaceWithPath(y to x, end, current.path.plus(y to x)))
93+
}
94+
}
95+
96+
val minCostRoutes = visited.filter { it.loc == end }.groupBy { (_, _, path, cost) ->
97+
cost
98+
}.minBy { it.key }
99+
100+
minCostRoutes.value.fold(mutableSetOf<Pair<Int, Int>>()) { acc, next ->
101+
acc.also { it.addAll(next.path) }
102+
}.size
103+
}
104+
105+
data class SpaceWithCost(
106+
val loc: Pair<Int, Int>,
107+
val cost: Long
108+
)
109+
110+
data class SpaceWithPath(
111+
val loc: Pair<Int, Int>,
112+
val end: Pair<Int, Int>,
113+
val path: List<Pair<Int, Int>>,
114+
val cost: Long = findCost(path)
115+
) : Comparable<SpaceWithPath> {
116+
override fun compareTo(other: SpaceWithPath): Int = cost.compareTo(other.cost)
117+
118+
companion object {
119+
fun findDirection(path: List<Pair<Int, Int>>): Direction {
120+
var direction = E
121+
path.windowed(2).lastOrNull()?.let { (s, d) ->
122+
val deltas = (d.first - s.first) to (d.second - s.second)
123+
124+
direction = when (deltas) {
125+
(-1 to 0) -> N
126+
(0 to 1) -> E
127+
(1 to 0) -> S
128+
(0 to -1) -> W
129+
else -> throw IllegalStateException("Unknown Deltas: $deltas")
130+
}
131+
}
132+
return direction
133+
}
134+
135+
fun findCost(path: List<Pair<Int, Int>>): Long {
136+
var direction = E
137+
return path.windowed(2).sumOf { (s, d) ->
138+
val deltas = (d.first - s.first) to (d.second - s.second)
139+
140+
val newDirection = when (deltas) {
141+
(-1 to 0) -> N
142+
(0 to 1) -> E
143+
(1 to 0) -> S
144+
(0 to -1) -> W
145+
else -> throw IllegalStateException("Unknown Deltas: $deltas")
146+
}
147+
148+
(direction.turns(newDirection) + 1).also { direction = newDirection }
149+
}
150+
}
151+
}
152+
}
153+
}
154+
155+
class MazeDijkstra(private val maze: MutableList<MutableList<Room>>) : Dijkstra<Room, Long, RoomWithCost> {
156+
override fun Room.withCost(cost: Long) = RoomWithCost(this, cost).withMaze(maze)
157+
override fun Long.plus(cost: Long) = this + cost
158+
override fun maxCost() = Long.MAX_VALUE
159+
override fun minCost() = 0L
160+
}
161+
162+
data class RoomWithCost(val room: Room, val cost: Long) : DijkstraNodeWithCost<Room, Long> {
163+
private lateinit var maze: MutableList<MutableList<Room>>
164+
165+
fun withMaze(maze: MutableList<MutableList<Room>>) = apply { this.maze = maze }
166+
167+
override fun compareTo(other: DijkstraNodeWithCost<Room, Long>): Int = cost.compareTo(other.cost())
168+
169+
override fun neighbors(): List<DijkstraNodeWithCost<Room, Long>> {
170+
val myDirection = room.direction
171+
val n = (-1 to 0) to Direction.N
172+
val e = (0 to 1) to E
173+
val s = (1 to 0) to Direction.S
174+
val w = (0 to -1) to Direction.W
175+
176+
return listOf(n, s, e, w).mapNotNull { (loc, dir) ->
177+
val (y, x) = room.y + loc.first to room.x + loc.second
178+
if (y !in maze.indices || x !in maze[y].indices) { null }
179+
else if (maze[y][x].space == Space.FULL) { null }
180+
else {
181+
val cost = myDirection.turns(dir) + 1
182+
RoomWithCost(Room(y, x).also { it.direction = dir }, cost).withMaze(maze)
183+
}
184+
}
185+
}
186+
187+
override fun node(): Room = room
188+
189+
override fun cost(): Long = cost
190+
191+
companion object {
192+
private const val TURN_COST = 1000
193+
}
194+
}
195+
196+
data class Room(
197+
val y: Int,
198+
val x: Int,
199+
val space: Space = Space.EMPTY
200+
) {
201+
var direction: Direction? = null
202+
}
203+
204+
enum class Space { FULL, EMPTY }
205+
206+
enum class Direction { N, E, S, W }
207+
208+
private fun Direction?.turns(direction: Direction): Long {
209+
if (this == null) return 0
210+
if (this == direction) return 0
211+
212+
if (this == Direction.N && direction == Direction.S ||
213+
this == Direction.S && direction == Direction.N ||
214+
this == E && direction == Direction.W ||
215+
this == Direction.W && direction == E
216+
) return 2000
217+
218+
return 1000
219+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
## [Day 16: Reindeer Maze](https://adventofcode.com/2024/day/16)

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import me.peckb.aoc._2024.calendar.day12.Day12Test
1515
import me.peckb.aoc._2024.calendar.day13.Day13Test
1616
import me.peckb.aoc._2024.calendar.day14.Day14Test
1717
import me.peckb.aoc._2024.calendar.day15.Day15Test
18+
import me.peckb.aoc._2024.calendar.day16.Day16Test
1819
import javax.inject.Singleton
1920
import me.peckb.aoc.DayComponent
2021
import me.peckb.aoc.InputModule
@@ -38,4 +39,5 @@ internal interface TestDayComponent : DayComponent {
3839
fun inject(day13Test: Day13Test)
3940
fun inject(day14Test: Day14Test)
4041
fun inject(day15Test: Day15Test)
42+
fun inject(day16Test: Day16Test)
4143
}
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.day16
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 Day16Test {
11+
@Inject
12+
lateinit var day16: Day16
13+
14+
@BeforeEach
15+
fun setup() {
16+
DaggerTestDayComponent.create().inject(this)
17+
}
18+
19+
@Test
20+
fun testDay16PartOne() {
21+
assertEquals(66404, day16.partOne(DAY_16))
22+
}
23+
24+
@Test
25+
fun testDay16PartTwo() {
26+
assertEquals(433, day16.partTwo(DAY_16))
27+
}
28+
29+
companion object {
30+
private const val DAY_16: String = "advent-of-code-input/2024/day16.input"
31+
}
32+
}

0 commit comments

Comments
 (0)