Skip to content

Commit 773be03

Browse files
authored
Merge pull request #23 from sraaphorst/D12
Day 12 complete.
2 parents 90a0583 + ebf5783 commit 773be03

File tree

7 files changed

+209
-7
lines changed

7 files changed

+209
-7
lines changed
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// Advent of Code 2024
2+
// By Sebastian Raaphorst, 2024.
3+
4+
package common.gridalgorithms
5+
6+
import common.intpos2d.*
7+
8+
data class Region(val area: Int, val perimeter: Int, val edges: Int)
9+
10+
fun findRegions(grid: Grid<Char>): List<Region> {
11+
val visited = mutableSetOf<IntPos2D>()
12+
val rows = grid.size
13+
val cols = grid[0].size
14+
15+
fun neighbours(pos: IntPos2D): List<IntPos2D> =
16+
Direction.entries.map { dir ->
17+
pos + dir.delta
18+
}.filter { pos -> pos.first in 0 until rows && pos.second in 0 until cols }
19+
20+
fun floodFill(start: IntPos2D): Region {
21+
val stack = mutableListOf(start)
22+
var area = 0
23+
var perimeter = 0
24+
val symbol = grid[start.first][start.second]
25+
var corners = 0
26+
27+
while (stack.isNotEmpty()) {
28+
val pos = stack.removeLast()
29+
if (pos !in visited) {
30+
visited.add(pos)
31+
area++
32+
33+
// Calculate perimeter: count neighbors that are not part of the same region,
34+
// as well as the walls that are out of bounds.
35+
val neighbours = neighbours(pos)
36+
val localPerimeter = neighbours.count { pos2 ->
37+
grid[pos2.first][pos2.second] != symbol
38+
}
39+
val outOfBounds = Direction.entries.map { dir -> pos + dir.delta }
40+
.count { pos2 -> pos2.first < 0 || pos2.first >= rows
41+
|| pos2.second < 0 || pos2.second >= cols }
42+
perimeter += localPerimeter + outOfBounds
43+
44+
// Calculate the corners, which will ultimately give us the number of
45+
// edges. Every corner is a shift in direction, indicating an edge.
46+
corners += Diagonals.count { (d1, d2) ->
47+
val side1 = grid[pos + d1.delta]
48+
val side2 = grid[pos + d2.delta]
49+
val corner = grid[pos + d1.delta + d2.delta]
50+
51+
// Two cases:
52+
// 1. The symbol here is different from the corners:
53+
// ? B
54+
// B A
55+
// 2. The symbol is the same as the sides but different from the corner:
56+
// B A
57+
// A A
58+
(symbol != side1 && symbol != side2) ||
59+
(symbol == side1 && symbol == side2 && symbol != corner)
60+
}
61+
62+
// Add valid neighbors to the stack
63+
stack.addAll(
64+
neighbours(pos).filter { pos2 ->
65+
grid[pos2.first][pos2.second] == symbol && pos2 !in visited
66+
}
67+
)
68+
}
69+
}
70+
71+
// area is A
72+
// perimeter is b
73+
return Region(area, perimeter, corners)
74+
}
75+
76+
// Iterate over the grid and find all regions
77+
return (0 until rows).flatMap { x ->
78+
(0 until cols).mapNotNull { y ->
79+
if (IntPos2D(x, y) !in visited) floodFill(x to y) else null
80+
}
81+
}
82+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Advent of Code 2024
2+
// By Sebastian Raaphorst, 2024.
3+
4+
package common.gridalgorithms
5+
6+
import common.intpos2d.Direction
7+
import common.intpos2d.*
8+
9+
typealias Grid<T> = List<List<T>>
10+
11+
operator fun <T> Grid<T>.contains(pos: IntPos2D): Boolean =
12+
pos.first in this.indices && pos.second in this[pos.first].indices
13+
14+
operator fun <T> Grid<T>.get(pos: IntPos2D): T? =
15+
if (pos in this) this[pos.first][pos.second] else null
16+
17+
fun <T> Grid<T>.neighbourPositions(pos: IntPos2D): Set<IntPos2D> =
18+
Direction.entries.map { pos + it.delta }
19+
.filter { it in this }
20+
.toSet()
21+
22+
// Get the value neighbours, and not the position neighbours.
23+
fun <T> Grid<T>.neighbourValues(pos: IntPos2D): Set<T> =
24+
Direction.entries.mapNotNull { this[pos + it.delta] }.toSet()

src/main/kotlin/common/intpos2d/intpos2d.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,14 @@ enum class Direction(val delta: IntPos2D) {
4747
WEST -> EAST
4848
}
4949
}
50+
51+
//fun IntPos2D.neighbours(rows: Int, cols: Int): List<IntPos2D> =
52+
// Direction.entries.map { this + it.delta }
53+
// .filter { it.first in 0 until rows && it.second in 0 until rows }
54+
55+
val Diagonals: Set<Pair<Direction, Direction>> = setOf(
56+
Pair(Direction.NORTH, Direction.WEST),
57+
Pair(Direction.WEST, Direction.SOUTH),
58+
Pair(Direction.SOUTH, Direction.EAST),
59+
Pair(Direction.EAST, Direction.NORTH)
60+
)

src/main/kotlin/common/parsing/parsing.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
package common.parsing
55

6+
import common.gridalgorithms.Grid
7+
68
val WhitespaceParser = Regex("""\s+""")
79

810
/**
@@ -24,10 +26,18 @@ fun <C1, C2> parseColumns(input: String,
2426
* The lines must be separated by a newline, and the entries within the line with whitespace.
2527
* The grid can be ragged.
2628
*/
27-
fun <T> parseGrid(input: String, toElem: (String) -> T): List<List<T>> =
29+
fun <T> parseGrid(input: String, toElem: (String) -> T): Grid<T> =
2830
input.lines()
2931
.filter(String::isNotBlank)
3032
.map { line ->
3133
line.trim()
3234
.split(WhitespaceParser)
3335
.map { toElem(it) } }
36+
37+
/**
38+
* Parse a grid that is just rows of chars.
39+
*/
40+
fun parseCharGrid(input: String): List<List<Char>> =
41+
input.lines()
42+
.filter(String::isNotBlank)
43+
.map { line -> line.trim().map { it } }

src/main/kotlin/day10/day10.kt

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package day10
55

66
import common.aocreader.fetchAdventOfCodeInput
7+
import common.gridalgorithms.*
78
import common.intpos2d.*
89
import common.runner.timedFunction
910

@@ -15,8 +16,8 @@ private fun parse(input: String): List<List<Int>> =
1516
.map { line -> line.trim().toList().map { it.digitToIntOrNull() ?: -1 } }
1617

1718
private fun findTrails(grid: List<List<Int>>): Map<IntPos2D, Trails> {
18-
val height = grid.size
19-
val width = grid[0].size
19+
val rows = grid.size
20+
val cols = grid[0].size
2021

2122
val zeros = grid.flatMapIndexed { rowIdx, row ->
2223
row.mapIndexedNotNull { colIdx, height ->
@@ -32,10 +33,8 @@ private fun findTrails(grid: List<List<Int>>): Map<IntPos2D, Trails> {
3233
if (currHeight == 9) return setOf(trailSoFar)
3334

3435
// Try all the valid neighbours.
35-
val neighbours = Direction.entries
36-
.map { currentPos + it.delta }
37-
.filter { coords -> coords.first in 0 until height && coords.second in 0 until width }
38-
.filter { coords -> grid[coords.first][coords.second] == currHeight + 1 }
36+
val neighbours = grid.neighbourPositions(currentPos)
37+
.filter { (grid[it] ?: -1L) == currHeight + 1 }
3938
if (neighbours.isEmpty()) return emptySet()
4039

4140
return neighbours.flatMap { pos -> aux(trailSoFar + pos) }.toSet()

src/main/kotlin/day12/day12.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Advent of Code 2024, Day 12.
2+
// By Sebastian Raaphorst, 2024.
3+
4+
package day12
5+
6+
import common.aocreader.fetchAdventOfCodeInput
7+
import common.gridalgorithms.*
8+
import common.parsing.parseCharGrid
9+
import common.runner.timedFunction
10+
11+
private fun regionCosts1(regions: Collection<Region>): Int =
12+
regions.sumOf { region -> region.area * region.perimeter }
13+
14+
private fun regionCosts2(regions: Collection<Region>): Int =
15+
regions.sumOf { region -> region.area * region.edges }
16+
17+
fun parse(input: String): Grid<Char> =
18+
parseCharGrid(input)
19+
20+
fun answer1(input: String): Int =
21+
parse(input).let(::findRegions).let(::regionCosts1)
22+
23+
fun answer2(input: String): Int =
24+
parse(input).let(::findRegions).let(::regionCosts2)
25+
26+
fun main() {
27+
val input = fetchAdventOfCodeInput(2024, 12)
28+
println("--- Day 12: Garden Groups ---")
29+
timedFunction("Part 1") { answer1(input) } // 1522850
30+
timedFunction("Part 2") { answer2(input) } // 953738
31+
}

src/test/kotlin/day12/day12.kt

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Advent of Code 2024, Day 12
2+
// By Sebastian Raaphorst, 2024.
3+
4+
package day12
5+
6+
import org.junit.jupiter.api.Test
7+
import kotlin.test.assertEquals
8+
9+
class Day12 {
10+
companion object {
11+
val input1 =
12+
"""
13+
AAAA
14+
BBCD
15+
BBCC
16+
EEEC
17+
""".trimIndent()
18+
19+
val input2 =
20+
"""
21+
RRRRIICCFF
22+
RRRRIICCCF
23+
VVRRRCCFFF
24+
VVRCCCJFFF
25+
VVVVCJJCFE
26+
VVIVCCJJEE
27+
VVIIICJJEE
28+
MIIIIIJJEE
29+
MIIISIJEEE
30+
MMMISSJEEE
31+
""".trimIndent().trim()
32+
}
33+
34+
@Test
35+
fun `Problem 1 example`() {
36+
assertEquals(140, answer1(input1))
37+
assertEquals(1930, answer1(input2))
38+
}
39+
40+
@Test
41+
fun `Problem2 example`() {
42+
assertEquals(80, answer2(input1))
43+
assertEquals(1206, answer2(input2))
44+
}
45+
}

0 commit comments

Comments
 (0)