-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add puzzle solution for 2023, day 22
- Loading branch information
Showing
16 changed files
with
1,850 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
49 changes: 49 additions & 0 deletions
49
common/collection/src/main/kotlin/com/curtislb/adventofcode/common/collection/ArrayQueue.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
11 changes: 11 additions & 0 deletions
11
common/collection/src/main/kotlin/com/curtislb/adventofcode/common/collection/SortIndices.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} |
127 changes: 127 additions & 0 deletions
127
...2/bricks/src/main/kotlin/com/curtislb/adventofcode/year2023/day22/bricks/FallingBricks.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} | ||
} |
57 changes: 57 additions & 0 deletions
57
...22/bricks/src/main/kotlin/com/curtislb/adventofcode/year2023/day22/bricks/SettledBrick.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
Oops, something went wrong.