diff --git a/2024/src/main/kotlin/aoc/year2024/Day12.kt b/2024/src/main/kotlin/aoc/year2024/Day12.kt new file mode 100644 index 0000000..b2cc3e9 --- /dev/null +++ b/2024/src/main/kotlin/aoc/year2024/Day12.kt @@ -0,0 +1,70 @@ +package aoc.year2024 + +import aoc.library.Direction +import aoc.library.Point +import aoc.library.Puzzle +import aoc.library.move + +object Day12 : Puzzle(12) { + + override fun solvePart2(input: String): Int = solve(input, ::perimeterV2) + + override fun solvePart1(input: String): Int = solve(input, ::perimeterV1) + + private fun solve( + input: String, + perimeter: (Set) -> Int, + ): Int { + val garden = parse(input) + return clusterByRegions(garden).sumOf { + val pointsInRegion = it.points + perimeter(pointsInRegion) * pointsInRegion.size + } + } + + private fun clusterByRegions(garden: Map): List { + val regions = mutableListOf() + val visited = mutableSetOf() + garden.keys.forEach { start -> + if (start !in visited) { + val groupChar = garden.getValue(start) + val group = mutableSetOf() + fun visit(point: Point) { + if (!group.add(point)) return + point.adjacentOrthogonal() + .forEach { adjacent -> + val char = garden[adjacent] + if (char == groupChar) { + visit(adjacent) + } + } + } + visit(start) + regions += Region(groupChar, group) + visited.addAll(group) + } + } + return regions + } + + private fun parse(input: String): Map = input.lines().flatMapIndexed { y, line -> + line.mapIndexed { x, char -> + Point(x, y) to char + } + }.toMap() + + private fun perimeterV1(points: Set): Int = points.sumOf { point -> + point.adjacentOrthogonal().count { it !in points } + } + + private fun perimeterV2(points: Set): Int { + fun Point.hasFence(direction: Direction): Boolean = this in points && move(direction) !in points + return points.sumOf { point -> + Direction.entries.count { direction -> + point.hasFence(direction) && !point.move(direction.clockwise()).hasFence(direction) + } + } + } + + private data class Region(val char: Char, val points: Set) +} diff --git a/2024/src/test/kotlin/aoc/year2024/Day12Test.kt b/2024/src/test/kotlin/aoc/year2024/Day12Test.kt new file mode 100644 index 0000000..3ead17e --- /dev/null +++ b/2024/src/test/kotlin/aoc/year2024/Day12Test.kt @@ -0,0 +1,79 @@ +package aoc.year2024 + +import aoc.library.solvePart1 +import aoc.library.solvePart2 +import io.kotest.matchers.ints.shouldBeExactly +import org.junit.jupiter.api.Test + +class Day12Test { + + @Test + fun part1TestInput() { + Day12.solvePart1( + """ + AAAA + BBCD + BBCC + EEEC + """.trimIndent(), + ) shouldBeExactly 140 + } + + @Test + fun part1TestInput2() { + Day12.solvePart1( + """ + RRRRIICCFF + RRRRIICCCF + VVRRRCCFFF + VVRCCCJFFF + VVVVCJJCFE + VVIVCCJJEE + VVIIICJJEE + MIIIIIJJEE + MIIISIJEEE + MMMISSJEEE + """.trimIndent(), + ) shouldBeExactly 1930 + } + + @Test + fun part1() { + Day12.solvePart1() shouldBeExactly 1434856 + } + + @Test + fun part2TestInput() { + Day12.solvePart2( + """ + AAAA + BBCD + BBCC + EEEC + """.trimIndent(), + ) shouldBeExactly 80 + } + + @Test + fun part2TestInput2() { + Day12.solvePart2( + """ + RRRRIICCFF + RRRRIICCCF + VVRRRCCFFF + VVRCCCJFFF + VVVVCJJCFE + VVIVCCJJEE + VVIIICJJEE + MIIIIIJJEE + MIIISIJEEE + MMMISSJEEE + """.trimIndent(), + ) shouldBeExactly 1206 + } + + @Test + fun part2() { + Day12.solvePart2() shouldBeExactly 891106 + } +}