Skip to content

Commit

Permalink
Add puzzle solution for 2023, day 22
Browse files Browse the repository at this point in the history
  • Loading branch information
curtislb committed Jun 5, 2024
1 parent 3289d16 commit 06ddf04
Show file tree
Hide file tree
Showing 16 changed files with 1,850 additions and 1 deletion.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ includes:
* [Advent of Code 2019][aoc-2019-link] (Days 1-25)
* [Advent of Code 2020][aoc-2020-link] (Days 1-25)
* [Advent of Code 2021][aoc-2021-link] (Days 1-25)
* [Advent of Code 2023][aoc-2023-link] (Days 1-21)
* [Advent of Code 2023][aoc-2023-link] (Days 1-22)

## Getting Started

Expand Down
3 changes: 3 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -318,4 +318,7 @@ dependencies {
kover(project(":year2023:day21:garden"))
kover(project(":year2023:day21:part1"))
kover(project(":year2023:day21:part2"))
kover(project(":year2023:day22:bricks"))
kover(project(":year2023:day22:part1"))
kover(project(":year2023:day22:part2"))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.curtislb.adventofcode.common.collection

/**
* Resizable-array implementation of the queue data structure.
*
* @param E The type of elements contained in the queue.
*
* @constructor Creates a new empty instance of [ArrayQueue].
*/
class ArrayQueue<E> : Collection<E> {
/**
* A deque of elements currently in the queue.
*/
private val deque: ArrayDeque<E> = ArrayDeque()

override val size: Int
get() = deque.size

override fun isEmpty(): Boolean = deque.isEmpty()

override fun contains(element: E): Boolean = element in deque

override fun containsAll(elements: Collection<E>): Boolean = deque.containsAll(elements)

override fun iterator(): Iterator<E> = deque.iterator()

/**
* Adds the specified [element] to the queue.
*/
fun offer(element: E) {
deque.addLast(element)
}

/**
* Removes and returns the least-recently-added element in the queue.
*
* @throws NoSuchElementException If the queue is empty.
*/
fun poll(): E = deque.removeFirst()
}

/**
* Returns a new instance of [ArrayQueue] with the specified [elements] in FIFO order.
*/
fun <T> arrayQueueOf(vararg elements: T): ArrayQueue<T> = ArrayQueue<T>().apply {
for (element in elements) {
offer(element)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.curtislb.adventofcode.common.collection

/**
* Returns the indices of the list, sorted by the result of the [selector] function applied to the
* corresponding list element.
*/
inline fun <T, R : Comparable<R>> List<T>.sortIndicesBy(crossinline selector: (T) -> R): List<Int> {
val indexList = indices.toMutableList()
indexList.sortBy { selector(this[it]) }
return indexList
}
5 changes: 5 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -497,3 +497,8 @@ include("year2023:day20:pulse")
include("year2023:day21:garden")
include("year2023:day21:part1")
include("year2023:day21:part2")

// Day 22: Sand Slabs
include("year2023:day22:bricks")
include("year2023:day22:part1")
include("year2023:day22:part2")
21 changes: 21 additions & 0 deletions year2023/day22/bricks/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
plugins {
val kotlinVersion: String by System.getProperties()
val koverVersion: String by System.getProperties()

kotlin("jvm") version kotlinVersion
id("org.jetbrains.kotlinx.kover") version koverVersion
}

dependencies {
val assertjVersion: String by properties
val junitJupiterVersion: String by properties
val junitPlatformVersion: String by properties

api(project(":common:geometry"))
implementation(project(":common:collection"))
implementation(project(":common:range"))
testImplementation("org.assertj:assertj-core:$assertjVersion")
testImplementation("org.junit.jupiter:junit-jupiter-api:$junitJupiterVersion")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:$junitJupiterVersion")
testRuntimeOnly("org.junit.platform:junit-platform-launcher:$junitPlatformVersion")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package com.curtislb.adventofcode.year2023.day22.bricks

import com.curtislb.adventofcode.common.collection.sortIndicesBy
import com.curtislb.adventofcode.common.geometry.Cuboid
import com.curtislb.adventofcode.common.range.size
import java.io.File

/**
* A collection of falling bricks that will settle into one or more rigid stacks, where each brick
* is supported by the ground (at `z=0`) or all bricks directly below it.
*
* @property bricks A list of cuboid bricks in 3D space, where the z-axis of each unit cube
* represents its initial distance above the ground.
*
* @constructor Creates a new instance of [FallingBricks] with the given list of [bricks].
*
* @throws IllegalArgumentException If any brick in [bricks] is empty or not fully above ground.
*/
class FallingBricks(private val bricks: List<Cuboid>) {
init {
for (brick in bricks) {
require(!brick.isEmpty()) { "Brick must be non-empty: $brick" }
require(brick.zRange.first > 0) { "Brick must be fully above ground (z=0): $brick" }
}
}

/**
* The list of settled bricks after they've fallen into their final positions.
*/
private val settledBricks: List<SettledBrick> by lazy {
// Assign each brick an ID equal to its index
val settledBricks = List(bricks.size) { SettledBrick(it) }

// Determine the final settled position of each brick and the bricks above/below it
val arrayHeight = bricks.maxOf { it.yRange.last } + 1
val arrayWidth = bricks.maxOf { it.xRange.last } + 1
val floorHeights = Array(arrayHeight) { IntArray(arrayWidth) { 0 } }
val topBrickIds = Array(arrayHeight) { IntArray(arrayWidth) { -1 } }
for (brickId in bricks.sortIndicesBy { it.zRange.first }) {
val brick = bricks[brickId]

// Find the maximum "floor" height under the current brick
var prevFloorHeight = 0
for (rowIndex in brick.yRange) {
for (colIndex in brick.xRange) {
prevFloorHeight = maxOf(prevFloorHeight, floorHeights[rowIndex][colIndex])
}
}

// Add the brick's height to the "floor" and update the bricks above/below it
val newFloorHeight = prevFloorHeight + brick.zRange.size()
for (rowIndex in brick.yRange) {
for (colIndex in brick.xRange) {
val topBrickId = topBrickIds[rowIndex][colIndex]
if (topBrickId != -1 && floorHeights[rowIndex][colIndex] == prevFloorHeight) {
val prevTopBrick = settledBricks[topBrickId]
val newTopBrick = settledBricks[brickId]
prevTopBrick.bricksAbove.add(newTopBrick)
newTopBrick.bricksBelow.add(prevTopBrick)
}
floorHeights[rowIndex][colIndex] = newFloorHeight
topBrickIds[rowIndex][colIndex] = brickId
}
}
}

settledBricks
}

/**
* Returns the number of settled bricks that can be safely disintegrated, without causing any
* bricks above them to fall.
*/
fun countSafeBricks(): Int = settledBricks.count { it.isSafeToRemove() }

/**
* Returns the sum, over all settled bricks, of the number of other bricks that would fall if
* each brick were disintegrated.
*/
fun sumSupportedBricks(): Int = settledBricks.sumOf { it.countSupportedBricks() }

companion object {
/**
* Regex used to parse the x-, y-, and z-coordinates of a falling brick.
*/
private val BRICK_REGEX = Regex("""(\d+),(\d+),(\d+)~(\d+),(\d+),(\d+)""")

/**
* Returns a new collection of [FallingBricks], with initial brick positions read from the
* specified [file].
*
* Each line of the file, must have the following format, where `(x1,y1,z1)` and
* `(x2,y2,z2)` are coordinates of opposite-corner unit cubes contained in the brick:
*
* ```
* $x1,$y1,$z1~$x2,$y2,$z2
* ```
*/
fun fromFile(file: File): FallingBricks {
val bricks = mutableListOf<Cuboid>()

file.forEachLine { line ->
// Parse coordinates from the current line
val matchResult = BRICK_REGEX.matchEntire(line)
require(matchResult != null) { "Malformed line: $line" }

// Convert strings to unit coordinates
val x1 = matchResult.groupValues[1].toInt()
val y1 = matchResult.groupValues[2].toInt()
val z1 = matchResult.groupValues[3].toInt()
val x2 = matchResult.groupValues[4].toInt()
val y2 = matchResult.groupValues[5].toInt()
val z2 = matchResult.groupValues[6].toInt()

// Construct brick with the given coordinates
val brick = Cuboid(
xRange = minOf(x1, x2)..maxOf(x1, x2),
yRange = minOf(y1, y2)..maxOf(y1, y2),
zRange = minOf(z1, z2)..maxOf(z1, z2)
)
bricks.add(brick)
}

return FallingBricks(bricks)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.curtislb.adventofcode.year2023.day22.bricks

import com.curtislb.adventofcode.common.collection.arrayQueueOf

/**
* A brick that has settled into a vertical stack, with other bricks directly above/below it.
*
* @property id An integer ID that uniquely identifies the brick. Used to check equality.
*
* @constructor Creates a new instance of [SettledBrick] with the given [id].
*/
class SettledBrick(val id: Int) {
/**
* The set of bricks directly above this one in the stack.
*/
val bricksAbove: MutableSet<SettledBrick> = mutableSetOf()

/**
* The set of bricks directly below this one in the stack.
*/
val bricksBelow: MutableSet<SettledBrick> = mutableSetOf()

/**
* Returns `true` is this brick can be safely disintegrated without causing any of the bricks
* above it to fall.
*/
fun isSafeToRemove(): Boolean = bricksAbove.all { it.bricksBelow.size > 1 }

/**
* Returns the total number of bricks that would fall if this brick were disintegrated.
*/
fun countSupportedBricks(): Int {
// Mark this brick as one that would "fall" if it were disintegrated
val supportedBricks = mutableSetOf(this)

// Use BFS to find bricks above this one, marking any that would fall
val brickQueue = arrayQueueOf(this)
while (brickQueue.isNotEmpty()) {
val brick = brickQueue.poll()
for (brickAbove in brick.bricksAbove) {
if (brickAbove.bricksBelow.all { it in supportedBricks }) {
supportedBricks.add(brickAbove)
brickQueue.offer(brickAbove)
}
}
}

// Exclude this brick from the final count
return supportedBricks.size - 1
}

override fun toString(): String = "SettledBrick(id=$id)"

override fun equals(other: Any?): Boolean = other is SettledBrick && id == other.id

override fun hashCode(): Int = id.hashCode()
}
Loading

0 comments on commit 06ddf04

Please sign in to comment.