Skip to content

Day 12 complete. #23

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions src/main/kotlin/common/gridalgorithms/floodfill.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Advent of Code 2024
// By Sebastian Raaphorst, 2024.

package common.gridalgorithms

import common.intpos2d.*

data class Region(val area: Int, val perimeter: Int, val edges: Int)

fun findRegions(grid: Grid<Char>): List<Region> {
val visited = mutableSetOf<IntPos2D>()
val rows = grid.size
val cols = grid[0].size

fun neighbours(pos: IntPos2D): List<IntPos2D> =
Direction.entries.map { dir ->
pos + dir.delta
}.filter { pos -> pos.first in 0 until rows && pos.second in 0 until cols }

fun floodFill(start: IntPos2D): Region {
val stack = mutableListOf(start)
var area = 0
var perimeter = 0
val symbol = grid[start.first][start.second]
var corners = 0

while (stack.isNotEmpty()) {
val pos = stack.removeLast()
if (pos !in visited) {
visited.add(pos)
area++

// Calculate perimeter: count neighbors that are not part of the same region,
// as well as the walls that are out of bounds.
val neighbours = neighbours(pos)
val localPerimeter = neighbours.count { pos2 ->
grid[pos2.first][pos2.second] != symbol
}
val outOfBounds = Direction.entries.map { dir -> pos + dir.delta }
.count { pos2 -> pos2.first < 0 || pos2.first >= rows
|| pos2.second < 0 || pos2.second >= cols }
perimeter += localPerimeter + outOfBounds

// Calculate the corners, which will ultimately give us the number of
// edges. Every corner is a shift in direction, indicating an edge.
corners += Diagonals.count { (d1, d2) ->
val side1 = grid[pos + d1.delta]
val side2 = grid[pos + d2.delta]
val corner = grid[pos + d1.delta + d2.delta]

// Two cases:
// 1. The symbol here is different from the corners:
// ? B
// B A
// 2. The symbol is the same as the sides but different from the corner:
// B A
// A A
(symbol != side1 && symbol != side2) ||
(symbol == side1 && symbol == side2 && symbol != corner)
}

// Add valid neighbors to the stack
stack.addAll(
neighbours(pos).filter { pos2 ->
grid[pos2.first][pos2.second] == symbol && pos2 !in visited
}
)
}
}

// area is A
// perimeter is b
return Region(area, perimeter, corners)
}

// Iterate over the grid and find all regions
return (0 until rows).flatMap { x ->
(0 until cols).mapNotNull { y ->
if (IntPos2D(x, y) !in visited) floodFill(x to y) else null
}
}
}
24 changes: 24 additions & 0 deletions src/main/kotlin/common/gridalgorithms/grid.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Advent of Code 2024
// By Sebastian Raaphorst, 2024.

package common.gridalgorithms

import common.intpos2d.Direction
import common.intpos2d.*

typealias Grid<T> = List<List<T>>

operator fun <T> Grid<T>.contains(pos: IntPos2D): Boolean =
pos.first in this.indices && pos.second in this[pos.first].indices

operator fun <T> Grid<T>.get(pos: IntPos2D): T? =
if (pos in this) this[pos.first][pos.second] else null

fun <T> Grid<T>.neighbourPositions(pos: IntPos2D): Set<IntPos2D> =
Direction.entries.map { pos + it.delta }
.filter { it in this }
.toSet()

// Get the value neighbours, and not the position neighbours.
fun <T> Grid<T>.neighbourValues(pos: IntPos2D): Set<T> =
Direction.entries.mapNotNull { this[pos + it.delta] }.toSet()
11 changes: 11 additions & 0 deletions src/main/kotlin/common/intpos2d/intpos2d.kt
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,14 @@ enum class Direction(val delta: IntPos2D) {
WEST -> EAST
}
}

//fun IntPos2D.neighbours(rows: Int, cols: Int): List<IntPos2D> =
// Direction.entries.map { this + it.delta }
// .filter { it.first in 0 until rows && it.second in 0 until rows }

val Diagonals: Set<Pair<Direction, Direction>> = setOf(
Pair(Direction.NORTH, Direction.WEST),
Pair(Direction.WEST, Direction.SOUTH),
Pair(Direction.SOUTH, Direction.EAST),
Pair(Direction.EAST, Direction.NORTH)
)
12 changes: 11 additions & 1 deletion src/main/kotlin/common/parsing/parsing.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

package common.parsing

import common.gridalgorithms.Grid

val WhitespaceParser = Regex("""\s+""")

/**
Expand All @@ -24,10 +26,18 @@ fun <C1, C2> parseColumns(input: String,
* The lines must be separated by a newline, and the entries within the line with whitespace.
* The grid can be ragged.
*/
fun <T> parseGrid(input: String, toElem: (String) -> T): List<List<T>> =
fun <T> parseGrid(input: String, toElem: (String) -> T): Grid<T> =
input.lines()
.filter(String::isNotBlank)
.map { line ->
line.trim()
.split(WhitespaceParser)
.map { toElem(it) } }

/**
* Parse a grid that is just rows of chars.
*/
fun parseCharGrid(input: String): List<List<Char>> =
input.lines()
.filter(String::isNotBlank)
.map { line -> line.trim().map { it } }
11 changes: 5 additions & 6 deletions src/main/kotlin/day10/day10.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package day10

import common.aocreader.fetchAdventOfCodeInput
import common.gridalgorithms.*
import common.intpos2d.*
import common.runner.timedFunction

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

private fun findTrails(grid: List<List<Int>>): Map<IntPos2D, Trails> {
val height = grid.size
val width = grid[0].size
val rows = grid.size
val cols = grid[0].size

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

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

return neighbours.flatMap { pos -> aux(trailSoFar + pos) }.toSet()
Expand Down
31 changes: 31 additions & 0 deletions src/main/kotlin/day12/day12.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Advent of Code 2024, Day 12.
// By Sebastian Raaphorst, 2024.

package day12

import common.aocreader.fetchAdventOfCodeInput
import common.gridalgorithms.*
import common.parsing.parseCharGrid
import common.runner.timedFunction

private fun regionCosts1(regions: Collection<Region>): Int =
regions.sumOf { region -> region.area * region.perimeter }

private fun regionCosts2(regions: Collection<Region>): Int =
regions.sumOf { region -> region.area * region.edges }

fun parse(input: String): Grid<Char> =
parseCharGrid(input)

fun answer1(input: String): Int =
parse(input).let(::findRegions).let(::regionCosts1)

fun answer2(input: String): Int =
parse(input).let(::findRegions).let(::regionCosts2)

fun main() {
val input = fetchAdventOfCodeInput(2024, 12)
println("--- Day 12: Garden Groups ---")
timedFunction("Part 1") { answer1(input) } // 1522850
timedFunction("Part 2") { answer2(input) } // 953738
}
45 changes: 45 additions & 0 deletions src/test/kotlin/day12/day12.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Advent of Code 2024, Day 12
// By Sebastian Raaphorst, 2024.

package day12

import org.junit.jupiter.api.Test
import kotlin.test.assertEquals

class Day12 {
companion object {
val input1 =
"""
AAAA
BBCD
BBCC
EEEC
""".trimIndent()

val input2 =
"""
RRRRIICCFF
RRRRIICCCF
VVRRRCCFFF
VVRCCCJFFF
VVVVCJJCFE
VVIVCCJJEE
VVIIICJJEE
MIIIIIJJEE
MIIISIJEEE
MMMISSJEEE
""".trimIndent().trim()
}

@Test
fun `Problem 1 example`() {
assertEquals(140, answer1(input1))
assertEquals(1930, answer1(input2))
}

@Test
fun `Problem2 example`() {
assertEquals(80, answer2(input1))
assertEquals(1206, answer2(input2))
}
}
Loading