Skip to content

Commit f1e7f1a

Browse files
committed
Add solution for Y2023D22.
1 parent 3fa84c1 commit f1e7f1a

File tree

6 files changed

+161
-1
lines changed

6 files changed

+161
-1
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ _"Anything that's worth doing, is worth overdoing."_
1515
Table of contents to jump straight to the problem you're looking for.
1616

1717
<details open>
18-
<summary>2023 (42x⭐)</summary>
18+
<summary>2023 (44x⭐)</summary>
1919

2020
| Day | Title | Stars |
2121
|:---:|:---------------------------------------------------------------------|:-----:|
@@ -40,6 +40,7 @@ Table of contents to jump straight to the problem you're looking for.
4040
| 19 | [Aplenty](solutions/aockt/y2023/Y2023D19.kt) | ⭐⭐ |
4141
| 20 | [Pulse Propagation](solutions/aockt/y2023/Y2023D20.kt) | ⭐⭐ |
4242
| 21 | [Step Counter](solutions/aockt/y2023/Y2023D21.kt) | ⭐⭐ |
43+
| 22 | [Sand Slabs](solutions/aockt/y2023/Y2023D22.kt) | ⭐⭐ |
4344

4445
</details>
4546

inputs/aockt/y2023/d22/input.txt

19.1 KB
Binary file not shown.
26 Bytes
Binary file not shown.
28 Bytes
Binary file not shown.

solutions/aockt/y2023/Y2023D22.kt

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package aockt.y2023
2+
3+
import aockt.util.parse
4+
import aockt.util.spacial.Area
5+
import aockt.util.spacial.overlaps
6+
import aockt.util.spacial3d.Point3D
7+
import io.github.jadarma.aockt.core.Solution
8+
9+
object Y2023D22 : Solution {
10+
11+
/** A line of sand cubes, defined by the [start] and [end] coordinates. */
12+
private data class SandBrick(val start: Point3D, val end: Point3D) {
13+
14+
init {
15+
require(start.z <= end.z) { "Start and end given out of order." }
16+
require(start.z >= 1) { "Sand brick collides with ground." }
17+
val isHorizontal = start.z == end.z && (start.x == end.x || start.y == end.y)
18+
val isVertical = start.x == end.x && start.y == end.y
19+
require(isHorizontal || isVertical) { "The sand brick must be a straight line." }
20+
}
21+
22+
/** The area this line occupies on a flat plane, which can cause collisions when falling. */
23+
val fallingArea: Area = Area(
24+
xRange = minOf(start.x, end.x)..maxOf(start.x, end.x),
25+
yRange = minOf(start.y, end.y)..maxOf(start.y, end.y),
26+
)
27+
28+
/** Returns the state of the sand brick if it were to fall until the [start] rests at the given [restHeight]. */
29+
fun fallTo(restHeight: Long): SandBrick = SandBrick(
30+
start = start.copy(z = restHeight),
31+
end = end.copy(z = end.z - start.z + restHeight),
32+
)
33+
34+
/** Checks if this and the [other] brick will stack on each other after falling to the ground. */
35+
fun fallingAreaOverlaps(other: SandBrick): Boolean = fallingArea overlaps other.fallingArea
36+
}
37+
38+
/** A physics simulator for magical sand brick. */
39+
private class SandBrickSimulator(bricks: Iterable<SandBrick>) {
40+
41+
/** The resting position of all bricks. */
42+
val settledBricks: List<SandBrick> =
43+
bricks
44+
.toMutableList()
45+
.apply {
46+
sortBy { it.start.z }
47+
forEachIndexed { index, brick ->
48+
this[index] = slice(0..<index)
49+
.filter { brick.fallingAreaOverlaps(it) }
50+
.maxOfOrNull { it.end.z + 1 }
51+
.let { restHeight -> brick.fallTo(restHeight ?: 1L) }
52+
}
53+
}
54+
.sortedBy { it.start.z }
55+
56+
/** A mapping from a brick to all bricks that it rests directly on top of. */
57+
val supportedBy: Map<SandBrick, Set<SandBrick>>
58+
59+
/** Syntactical sugar for getting all bricks resting directly on top of this one. */
60+
private val SandBrick.supportedBricks: Set<SandBrick> get() = supportedBy.getValue(this)
61+
62+
/** A mapping from a brick to all bricks that rest directly on top of it. */
63+
val supporting: Map<SandBrick, Set<SandBrick>>
64+
65+
/** Syntactical sugar for getting all bricks that this brick rests directly on top of. */
66+
private val SandBrick.standingOn: Set<SandBrick> get() = supporting.getValue(this)
67+
68+
init {
69+
val supportedBy: Map<SandBrick, MutableSet<SandBrick>> = settledBricks.associateWith { mutableSetOf() }
70+
val supporting: Map<SandBrick, MutableSet<SandBrick>> = settledBricks.associateWith { mutableSetOf() }
71+
72+
settledBricks.forEachIndexed { index, above ->
73+
settledBricks.slice(0..<index).forEach { below ->
74+
if (below.fallingAreaOverlaps(above) && above.start.z == below.end.z + 1) {
75+
supportedBy.getValue(below).add(above)
76+
supporting.getValue(above).add(below)
77+
}
78+
}
79+
}
80+
81+
this.supportedBy = supportedBy
82+
this.supporting = supporting
83+
}
84+
85+
/**
86+
* The bricks that do not contribute to the structural integrity of the sand formation.
87+
* They can be disintegrated without causing other bricks to fall.
88+
*/
89+
val redundantBricks: Set<SandBrick> =
90+
settledBricks
91+
.filter { it.supportedBricks.all { supported -> supported.standingOn.count() >= 2 } }
92+
.toSet()
93+
94+
/** Simulate disintegrating the [brick] and return all bricks which would fall as a result. */
95+
fun fallingBricksIfDisintegrating(brick: SandBrick): Set<SandBrick> = buildSet {
96+
require(brick in supporting) { "The brick $brick is not part of the simulation." }
97+
val fallingBricks = this
98+
99+
brick.supportedBricks
100+
.filter { supported -> supported.standingOn.size == 1 }
101+
.let(fallingBricks::addAll)
102+
103+
val queue = ArrayDeque(elements = fallingBricks)
104+
105+
while (queue.isNotEmpty()) {
106+
queue.removeFirst()
107+
.supportedBricks
108+
.minus(fallingBricks)
109+
.filter { supportedByFalling -> fallingBricks.containsAll(supportedByFalling.standingOn) }
110+
.onEach(fallingBricks::add)
111+
.forEach(queue::add)
112+
}
113+
}
114+
}
115+
116+
/** Parses the [input] and returns the list of [SandBrick]s. */
117+
private fun parseInput(input: String): List<SandBrick> = parse {
118+
input
119+
.lineSequence()
120+
.map { line -> line.replace('~', ',') }
121+
.map { line -> line.split(',', limit = 6).map(String::toInt) }
122+
.onEach { require(it.size == 6) }
123+
.map {
124+
SandBrick(
125+
start = it.take(3).let { (x, y, z) -> Point3D(x, y, z) },
126+
end = it.takeLast(3).let { (x, y, z) -> Point3D(x, y, z) },
127+
)
128+
}
129+
.toList()
130+
}
131+
132+
override fun partOne(input: String) = parseInput(input).let(::SandBrickSimulator).redundantBricks.count()
133+
override fun partTwo(input: String) = parseInput(input).let(::SandBrickSimulator).run {
134+
settledBricks
135+
.map(::fallingBricksIfDisintegrating)
136+
.sumOf { it.count() }
137+
}
138+
}

tests/aockt/y2023/Y2023D22Test.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package aockt.y2023
2+
3+
import io.github.jadarma.aockt.test.AdventDay
4+
import io.github.jadarma.aockt.test.AdventSpec
5+
6+
@AdventDay(2023, 22, "Sand Slabs")
7+
class Y2023D22Test : AdventSpec<Y2023D22>({
8+
9+
val exampleInput = """
10+
1,0,1~1,2,1
11+
0,0,2~2,0,2
12+
0,2,3~2,2,3
13+
0,0,4~0,2,4
14+
2,0,5~2,2,5
15+
0,1,6~2,1,6
16+
1,1,8~1,1,9
17+
""".trimIndent()
18+
19+
partOne { exampleInput shouldOutput 5 }
20+
partTwo { exampleInput shouldOutput 7 }
21+
})

0 commit comments

Comments
 (0)