Skip to content

Commit

Permalink
Day 12 Part B
Browse files Browse the repository at this point in the history
  • Loading branch information
luanpotter committed Dec 17, 2024
1 parent 0bcd171 commit 4838440
Show file tree
Hide file tree
Showing 3 changed files with 289 additions and 11 deletions.
111 changes: 109 additions & 2 deletions src/main/kotlin/xyz/luan/advent/day12/Main.kt
Original file line number Diff line number Diff line change
@@ -1,16 +1,80 @@
package xyz.luan.advent.day12

import xyz.luan.advent.util.*
import kotlin.math.max
import kotlin.math.min

fun main() {
val garden = Garden.parse(readFile("day12/input"))
println("A: ${garden.compute()}")
.buildGraph()
println("A: ${garden.computeA()}")
println("B: ${garden.computeB()}")
}

enum class Side { TOP, RIGHT, BOTTOM, LEFT }

data class FenceBit(
val from: Coord,
val to: Coord,
val inside: Side,
) {
val y: Int? get() = setOf(from.y, to.y).singleOrNull()
val x: Int? get() = setOf(from.x, to.x).singleOrNull()

fun tryCombine(bit: FenceBit): FenceBit? {
val side = if (inside == bit.inside) inside else return null

val bitX = bit.x
val bitY = bit.y

if (bitX != null && bitX == x) {
val minY = min(from.y, to.y)
val maxY = max(from.y, to.y)
if ((bit.from.y > maxY && bit.to.y > maxY) || (bit.from.y < minY && bit.to.y < minY)) {
return null
}
val otherMinY = min(bit.from.y, bit.to.y)
val otherMaxY = max(bit.from.y, bit.to.y)
return FenceBit(bitX to min(minY, otherMinY), bitX to max(maxY, otherMaxY), side)
} else if (bitY != null && bitY == y) {
val minX = min(from.x, to.x)
val maxX = max(from.x, to.x)
if ((bit.from.x > maxX && bit.to.x > maxX) || (bit.from.x < minX && bit.to.x < minX)) {
return null
}
val otherMinX = min(bit.from.x, bit.to.x)
val otherMaxX = max(bit.from.x, bit.to.x)
return FenceBit(min(minX, otherMinX) to bitY, max(maxX, otherMaxX) to bitY, side)
}
return null
}

companion object {
fun between(
c1: Coord,
c2: Coord,
): FenceBit {
return if (c1.x == c2.x) {
val x = c1.x
val y = max(c1.y, c2.y)
val side = if (c1.y > c2.y) Side.TOP else Side.BOTTOM
FenceBit(x to y, x + 1 to y, side)
} else if (c1.y == c2.y) {
val x = max(c1.x, c2.x)
val y = c1.y
val side = if (c1.x > c2.x) Side.LEFT else Side.RIGHT
FenceBit(x to y, x to y + 1, side)
} else {
error("Invalid fence bit: $c1 -> $c2")
}
}
}
}

class Garden(
private val plots: Matrix<Char>,
) {
fun compute(): Long {
fun buildGraph(): GardenGraph {
val graph = mutableListOf<Set<Coord>>()

val toProcess = mutableSetOf(Coord(0, 0))
Expand Down Expand Up @@ -44,7 +108,14 @@ class Garden(

graph.add(group)
}
return GardenGraph(plots, graph)
}

class GardenGraph(
private val plots: Matrix<Char>,
private val graph: List<Set<Coord>>,
) {
fun computeA(): Long {
return graph.sumOf { region ->
val area = region.size.toLong()
val perimeter = region.sumOf { plot ->
Expand All @@ -57,6 +128,42 @@ class Garden(
}
}

fun computeB(): Long {
fun simplify(bits: MutableSet<FenceBit>) {
for (b1 in bits.toList()) {
for (b2 in bits.toList()) {
if (b1 == b2) {
continue
}
val combined = b1.tryCombine(b2)
if (combined != null) {
bits.remove(b1)
bits.remove(b2)
bits.add(combined)
simplify(bits)
return
}
}
}
}

return graph.sumOf { region ->
val area = region.size.toLong()
val fenceBits = region
.flatMap { plot ->
plots.allNeighborsOf(plot)
.filter { it !in region }
.map { FenceBit.between(plot, it) }
}
.toMutableSet()
simplify(fenceBits)
val perimeter = fenceBits.size.toLong()
area * perimeter
}
}

}

companion object {
fun parse(input: List<String>): Garden {
val plots = input
Expand Down
15 changes: 11 additions & 4 deletions src/main/kotlin/xyz/luan/advent/util/Advent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,30 @@ private fun loadResource(path: String): URL {
fun <T> MutableSet<T>.pop(): T = first().also { remove(it) }

typealias Coord = Pair<Int, Int>
val Coord.x: Int get() = first
val Coord.y: Int get() = second

typealias Matrix<T> = Array<Array<T>>

val <T> Matrix<T>.coords: List<Coord>
get() = indices.flatMap { x -> this[x].indices.map { y -> x to y } }

fun <T> Matrix<T>.get(coord: Coord): T = this[coord.first][coord.second]
fun <T> Matrix<T>.get(coord: Coord): T = this[coord.x][coord.y]

fun <T> Matrix<T>.set(coord: Coord, value: T) {
this[coord.first][coord.second] = value
this[coord.x][coord.y] = value
}

fun <T> Matrix<T>.neighborsOf(coord: Coord): List<Coord> {
val (x, y) = coord
val coords = this.coords
return allNeighborsOf(coord)
.filter { it in coords }
}

fun <T> Matrix<T>.allNeighborsOf(coord: Coord): List<Coord> {
val (x, y) = coord
return directions
.map { (dx, dy) -> x + dx to y + dy }
.filter { it in coords }
}

private val directions = setOf(
Expand Down
174 changes: 169 additions & 5 deletions src/test/kotlin/xyz/luan/advent/day12/Day12Test.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import kotlin.test.assertEquals

class Day12Test {
@Test
fun `simplest case`() {
testCase(
fun `part A 1`() {
testCaseA(
listOf(
"AAAA",
"BBCD",
Expand All @@ -15,8 +15,11 @@ class Day12Test {
),
140L,
)
}

testCase(
@Test
fun `part A 2`() {
testCaseA(
listOf(
"OOOOO",
"OXOXO",
Expand All @@ -28,8 +31,169 @@ class Day12Test {
)
}

private fun testCase(input: List<String>, result: Long) {
@Test
fun `part B 1`() {
testCaseB(
listOf(
"EEEEE",
"EXXXX",
"EEEEE",
"EXXXX",
"EEEEE",
),
236L,
)
}

@Test
fun `part B 2`() {
testCaseB(
listOf(
"AAAAAA",
"AAABBA",
"AAABBA",
"ABBAAA",
"ABBAAA",
"AAAAAA",
),
368L,
)
}

@Test
fun `fence bits between`() {
// left of (0, 0)
val bit1 = FenceBit.between(-1 to 0, 0 to 0)
assertEquals(0 to 0, bit1.from)
assertEquals(0 to 1, bit1.to)

// top of (0, 0)
val bit2 = FenceBit.between(0 to -1, 0 to 0)
assertEquals(0 to 0, bit2.from)
assertEquals(1 to 0, bit2.to)

// right of (0, 0)
val bit3 = FenceBit.between(1 to 0, 0 to 0)
assertEquals(1 to 0, bit3.from)
assertEquals(1 to 1, bit3.to)

// bottom of (0, 0)
val bit4 = FenceBit.between(0 to 1, 0 to 0)
assertEquals(0 to 1, bit4.from)
assertEquals(1 to 1, bit4.to)
}

@Test
fun `fence bits adjacent y combine`() {
val bit1 = FenceBit(0 to 0, 1 to 0, Side.TOP)
assertEquals(0, bit1.y)

val bit2 = FenceBit(1 to 0, 2 to 0, Side.TOP)
assertEquals(0, bit2.y)

val combined = bit1.tryCombine(bit2)!!
assertEquals(0 to 0, combined.from)
assertEquals(2 to 0, combined.to)
}

@Test
fun `fence bits long y combine`() {
val bit1 = FenceBit(0 to 0, 3 to 0, Side.TOP)
assertEquals(0, bit1.y)

val bit2 = FenceBit(3 to 0, 5 to 0, Side.TOP)
assertEquals(0, bit2.y)

val combined = bit1.tryCombine(bit2)!!
assertEquals(0 to 0, combined.from)
assertEquals(5 to 0, combined.to)
}

@Test
fun `fence bits intersecting y combine`() {
val bit1 = FenceBit(0 to 0, 3 to 0, Side.TOP)
assertEquals(0, bit1.y)

val bit2 = FenceBit(1 to 0, 4 to 0, Side.TOP)
assertEquals(0, bit2.y)

val combined = bit1.tryCombine(bit2)!!
assertEquals(0 to 0, combined.from)
assertEquals(4 to 0, combined.to)
}

@Test
fun `fence bits inside y combine`() {
val bit1 = FenceBit(0 to 0, 3 to 0, Side.TOP)
assertEquals(0, bit1.y)

val bit2 = FenceBit(1 to 0, 2 to 0, Side.TOP)
assertEquals(0, bit2.y)

val combined = bit1.tryCombine(bit2)!!
assertEquals(0 to 0, combined.from)
assertEquals(3 to 0, combined.to)
}

@Test
fun `fence bits adjacent x combine`() {
val bit1 = FenceBit(0 to 0, 0 to 1, Side.RIGHT)
assertEquals(0, bit1.x)

val bit2 = FenceBit(0 to 1, 0 to 2, Side.RIGHT)
assertEquals(0, bit2.x)

val combined = bit1.tryCombine(bit2)!!
assertEquals(0 to 0, combined.from)
assertEquals(0 to 2, combined.to)
}

@Test
fun `fence bits long x combine`() {
val bit1 = FenceBit(0 to 0, 0 to 3, Side.RIGHT)
assertEquals(0, bit1.x)

val bit2 = FenceBit(0 to 3, 0 to 5, Side.RIGHT)
assertEquals(0, bit2.x)

val combined = bit1.tryCombine(bit2)!!
assertEquals(0 to 0, combined.from)
assertEquals(0 to 5, combined.to)
}

@Test
fun `fence bits intersecting x combine`() {
val bit1 = FenceBit(0 to 0, 0 to 3, Side.RIGHT)
assertEquals(0, bit1.x)

val bit2 = FenceBit(0 to 1, 0 to 4, Side.RIGHT)
assertEquals(0, bit2.x)

val combined = bit1.tryCombine(bit2)!!
assertEquals(0 to 0, combined.from)
assertEquals(0 to 4, combined.to)
}

@Test
fun `fence bits inside x combine`() {
val bit1 = FenceBit(0 to 0, 0 to 3, Side.RIGHT)
assertEquals(0, bit1.x)

val bit2 = FenceBit(0 to 1, 0 to 2, Side.RIGHT)
assertEquals(0, bit2.x)

val combined = bit1.tryCombine(bit2)!!
assertEquals(0 to 0, combined.from)
assertEquals(0 to 3, combined.to)
}

private fun testCaseA(input: List<String>, result: Long) {
val gardens = Garden.parse(input)
assertEquals(result, gardens.buildGraph().computeA())
}

private fun testCaseB(input: List<String>, result: Long) {
val gardens = Garden.parse(input)
assertEquals(result, gardens.compute())
assertEquals(result, gardens.buildGraph().computeB())
}
}

0 comments on commit 4838440

Please sign in to comment.