diff --git a/2025/src/main/kotlin/dev/dc/aoc/y25/Day09.kt b/2025/src/main/kotlin/dev/dc/aoc/y25/Day09.kt new file mode 100644 index 0000000..b5da14c --- /dev/null +++ b/2025/src/main/kotlin/dev/dc/aoc/y25/Day09.kt @@ -0,0 +1,188 @@ +package dev.dc.aoc.y25 + +import dev.dc.aoc.data.getInput +import kotlin.math.absoluteValue +import kotlin.math.max +import kotlin.math.min + +data class TilePair( + private val p: Pair, + private val q: Pair +) { + val minX = min(p.first, q.first) + val minY = min(p.second, q.second) + val maxX = max(p.first, q.first) + val maxY = max(p.second, q.second) + + val topLeft = minX to minY + val topRight = maxX to minY + val bottomLeft = minX to maxY + val bottomRight = maxX to maxY + + val corners = setOf(topLeft, topRight, bottomLeft, bottomRight) + + override fun equals(other: Any?) = when { + this === other -> true + other !is TilePair -> false + p == other.p && q == other.q || p == other.q && q == other.p -> true + else -> false + } + + override fun hashCode(): Int { + return p.hashCode() + q.hashCode() + } +} + +object Day09 { + private fun parse(input: String) = input.lines() + .map { it.split(",") } + .map { (x, y) -> x.toLong() to y.toLong() } + + fun part1(input: String): Long { + val locations: List> = parse(input) + + val pairs = locations.flatMap { point -> + (locations - point).map { + it to point + } + } + + return pairs.maxOf { (p1, p2) -> + ((p1.first - p2.first + 1) * (p1.second - p2.second + 1)).absoluteValue + } + } + + data class Point(val x: Int, val y: Int) + + fun part2(input: String): Long { + val points = parse(input).map { Point(it.first.toInt(), it.second.toInt()) } + + val uniqueX = points.map(Point::x).toSortedSet() + val uniqueY = points.map(Point::y).toSortedSet() + + val xMap = uniqueX.indices.associateBy { i -> uniqueX.elementAt(i) } + val yMap = uniqueY.indices.associateBy { i -> uniqueY.elementAt(i) } + + val grid = Array(uniqueY.size) { + CharArray(uniqueX.size) { + '.' + } + } + + val zPoints = points.map { + val x = xMap.getValue(it.x) + val y = yMap.getValue(it.y) + + grid[y][x] = '#' + + Point(x, y) + } + + for (i in 0 until zPoints.size) { + val a = zPoints[i] + val b = zPoints[(i + 1) % zPoints.size] + + if (a.x == b.x) { + val y0 = min(a.y, b.y) + val y1 = max(a.y, b.y) + + for (y in y0..y1) { + grid[y][a.x] = '#' + } + } else if (a.y == b.y) { + val x0 = min(a.x, b.x) + val x1 = max(a.x, b.x) + + for (x in x0..x1) { + grid[a.y][x] = '#' + } + } + } + + fun insidePoint(): Point { + for (y in 0 until uniqueY.size) { + for (x in 0 until uniqueX.size) { + if (grid[y][x] != '.') continue + + var hitsLeft = 0 + var previous = '.' + + for (i in x downTo 0) { + val current = grid[y][i] + if (current != previous) { + hitsLeft += 1 + } + previous = current + } + + if (hitsLeft % 2 == 1) { + return Point(x, y) + } + } + } + error("no inside point") + } + + fun floodFill() { + val stack = ArrayDeque(setOf(insidePoint())) + val directions = setOf( + 0 to 1, 0 to -1, 1 to 0, -1 to 0 + ) + + while (stack.isNotEmpty()) { + val (x, y) = stack.removeLast() + if (grid[y][x] != '.') continue + grid[y][x] = '#' + + directions.forEach { (dx, dy) -> + val nx = x + dx + val ny = y + dy + + if (ny in 0 until uniqueY.size && nx in 0 until uniqueX.size) { + if (grid[ny][nx] == '.') { + stack.addLast(Point(nx, ny)) + } + } + } + } + } + + fun isEnclosed(a: Point, b: Point): Boolean { + val x1 = xMap.getValue(a.x) + val x2 = xMap.getValue(b.x) + + val y1 = yMap.getValue(a.y) + val y2 = yMap.getValue(b.y) + + for (x in min(x1, x2)..max(x1, x2)) { + if (grid[y1][x] == '.' || grid[y2][x] == '.') return false + } + + for (y in min(y1, y2)..max(y1, y2)) { + if (grid[y][x1] == '.' || grid[y][x2] == '.') return false + } + + return true + } + + floodFill() + + val combinations = points.flatMap { point -> + (points - point).map { + point to it + } + } + + val max = combinations + .filter { (a, b) -> isEnclosed(a, b) } + .maxOf { (a, b) -> ((a.x - b.x).absoluteValue + 1).toLong() * ((a.y - b.y).absoluteValue + 1).toLong() } + + return max + } +} + +suspend fun main() { + val input = getInput(2025, 9) + println(Day09.part1(input)) + println(Day09.part2(input)) +} diff --git a/2025/src/test/kotlin/dev/dc/aoc/y25/Day09Test.kt b/2025/src/test/kotlin/dev/dc/aoc/y25/Day09Test.kt new file mode 100644 index 0000000..dfac390 --- /dev/null +++ b/2025/src/test/kotlin/dev/dc/aoc/y25/Day09Test.kt @@ -0,0 +1,27 @@ +package dev.dc.aoc.y25 + +import kotlin.test.Test +import kotlin.test.assertEquals + +class Day09Test { + private val testData = """ + 7,1 + 11,1 + 11,7 + 9,7 + 9,5 + 2,5 + 2,3 + 7,3 + """.trimIndent() + + @Test + fun part1() { + assertEquals(50, Day09.part1(testData)) + } + + @Test + fun part2() { + assertEquals(24, Day09.part2(testData)) + } +} diff --git a/data/src/main/kotlin/dev/dc/aoc/data/getInput.kt b/data/src/main/kotlin/dev/dc/aoc/data/getInput.kt index 6068820..3490128 100644 --- a/data/src/main/kotlin/dev/dc/aoc/data/getInput.kt +++ b/data/src/main/kotlin/dev/dc/aoc/data/getInput.kt @@ -5,6 +5,7 @@ import io.ktor.client.engine.okhttp.* import io.ktor.client.plugins.* import io.ktor.client.request.* import io.ktor.client.statement.* +import okhttp3.Headers import kotlin.io.path.Path import kotlin.io.path.exists import kotlin.io.path.readText @@ -21,6 +22,7 @@ private val client: HttpClient name = "session", value = sessionCookie ) + header("User-Agent", "https://github.com/dcowley/advent-of-code-kotlin by dean.w.cowley@gmail.com") } } @@ -33,7 +35,8 @@ suspend fun getInput(year: Int, day: Int): String { else -> { client.use { - it.get("https://adventofcode.com/$year/day/$day/input") + val response = it.get("https://adventofcode.com/$year/day/$day/input") + response .bodyAsText() .trim() .also(path::writeText)