Skip to content

Commit 4cb98f7

Browse files
committed
Day 09 more functional at a slight performance hit.
1 parent abe3a3c commit 4cb98f7

File tree

1 file changed

+122
-148
lines changed

1 file changed

+122
-148
lines changed

src/main/kotlin/day09/day09.kt

Lines changed: 122 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -1,218 +1,192 @@
11
// Advent of Code 2024, Day 09.
22
// By Sebastian Raaphorst, 2024.
33

4-
// This code was a misery to write and could stand for a lot of improvement.
5-
64
package day09
75

86
import common.aocreader.fetchAdventOfCodeInput
97
import java.math.BigInteger
108

119
private typealias Range = LongRange
12-
private typealias RangeList = MutableList<Range>
10+
private typealias RangeList = List<Range>
1311

14-
private fun Range.size(): Long =
15-
last - first + 1
12+
private fun Range.size(): Long = last - first + 1
1613

17-
private data class File(val id: Int, var blocks: RangeList)
14+
private data class File(val id: Int, val blocks: RangeList)
1815

19-
private class Filesystem(var files: MutableList<File>, var gaps: RangeList) {
20-
init {
21-
sortGaps()
22-
sortFiles()
23-
}
16+
private data class Filesystem(val files: List<File>, val gaps: RangeList) {
17+
fun sortAndMergeRanges(ranges: RangeList): RangeList {
18+
if (ranges.isEmpty()) return emptyList()
2419

25-
// The gaps need to be sorted by the first block they occupy.
26-
private fun sortGaps() {
27-
gaps.sortBy { g -> g.first }
20+
val sorted = ranges.sortedBy { it.first }
21+
return sorted.fold(mutableListOf<Range>()) { acc, range ->
22+
if (acc.isEmpty()) {
23+
acc += range
24+
} else {
25+
val lastRange = acc.last()
26+
if (range.first <= lastRange.last + 1) {
27+
acc[acc.lastIndex] = lastRange.first..maxOf(lastRange.last, range.last)
28+
} else {
29+
acc += range
30+
}
31+
}
32+
acc
33+
}
2834
}
2935

30-
// The files need to be sorted by the last block they occupy.
31-
private fun sortFiles() {
32-
files.sortBy { file -> file.blocks.maxOfOrNull(Range::last) }
33-
}
36+
private fun sortFiles(files: List<File>): List<File> =
37+
files.sortedBy { it.blocks.maxOfOrNull { block -> block.last } }
3438

35-
// Sorts the file so that the highest range is in the last block.
36-
private fun sortFile(file: File) {
37-
file.blocks.sortBy(Range::last)
38-
}
39+
private fun sortFile(file: File): File =
40+
file.copy(blocks = file.blocks.sortedBy { it.last })
41+
42+
fun rearrange1(): Filesystem {
43+
tailrec fun loop(fs: Filesystem): Filesystem {
44+
val files = fs.files
45+
val gaps = fs.gaps
46+
47+
val lastFile = files.lastOrNull() ?: return fs
48+
val lastBlock = lastFile.blocks.lastOrNull() ?: return fs
49+
val firstGap = gaps.firstOrNull() ?: return fs
50+
51+
// Done if the first gap is after the last block
52+
if (firstGap.first >= lastBlock.last) return fs
3953

40-
/**
41-
* Reparse before using this: solves part 1.
42-
*/
43-
fun rearrange1() {
44-
// We want to get the highest block and move it to the earliest position if
45-
// it makes sense to do so.
46-
while (true) {
47-
// Get the last file, which we want to move earlier if possible, and the
48-
// first gap, which is where we want to put it at.
49-
var lastFile = files.lastOrNull() ?: break
50-
val lastBlock = lastFile.blocks.lastOrNull() ?: break
51-
var firstGap = gaps.firstOrNull() ?: break
52-
53-
// We are done if the first gap is before the end of the last file.
54-
if (firstGap.first >= lastBlock.last)
55-
break
56-
57-
// Otherwise we have three cases.
5854
val blockSize = lastBlock.size()
5955
val gapSize = firstGap.size()
6056

61-
when {
57+
val (newFiles, newGaps) = when {
6258
gapSize == blockSize -> {
63-
// The sizes are the same, in which case we flip them.
64-
gaps -= firstGap
65-
lastFile.blocks -= lastBlock
66-
lastFile.blocks += firstGap
59+
// Same size: Swap block and gap
60+
val updatedFile = lastFile.copy(blocks = (lastFile.blocks - listOf(lastBlock)) + listOf(firstGap))
61+
val updatedFiles = files.dropLast(1) + sortFile(updatedFile)
62+
val updatedGaps = (gaps - listOf(firstGap)) + listOf(lastBlock)
63+
updatedFiles to updatedGaps
6764
}
68-
6965
gapSize > blockSize -> {
70-
// If the gap is bigger than the block, we divide the gap in two
71-
// and move the block to the first part of the gap.
66+
// Gap larger than block: split gap into two
7267
val dividingPoint = firstGap.first + blockSize
7368
val subGap1 = firstGap.first until dividingPoint
7469
val subGap2 = dividingPoint..firstGap.last
7570

76-
lastFile.blocks -= lastBlock
77-
lastFile.blocks += subGap1
78-
79-
gaps -= firstGap
80-
gaps += subGap2
81-
gaps += lastBlock
71+
val updatedFile = lastFile.copy(blocks = (lastFile.blocks - listOf(lastBlock)) + listOf(subGap1))
72+
val updatedFiles = files.dropLast(1) + sortFile(updatedFile)
73+
val updatedGaps = (gaps - listOf(firstGap)) + listOf(subGap2, lastBlock)
74+
updatedFiles to updatedGaps
8275
}
83-
8476
else -> {
85-
// If the block is bigger than the gap, we divide the block in two
86-
// and move the last part to the gap.
77+
// Block larger than gap: split block into two
8778
val dividingPoint = lastBlock.last - gapSize + 1
8879
val subBlock1 = lastBlock.first until dividingPoint
8980
val subBlock2 = dividingPoint..lastBlock.last
9081

91-
lastFile.blocks -= lastBlock
92-
lastFile.blocks += firstGap
93-
lastFile.blocks += subBlock1
94-
95-
gaps -= firstGap
96-
97-
// We shouldn't need this.
98-
gaps += subBlock2
82+
val updatedFile = lastFile.copy(blocks = (lastFile.blocks - listOf(lastBlock)) + listOf(firstGap, subBlock1))
83+
val updatedFiles = files.dropLast(1) + sortFile(updatedFile)
84+
val updatedGaps = (gaps - listOf(firstGap)) + listOf(subBlock2)
85+
updatedFiles to updatedGaps
9986
}
10087
}
10188

102-
// Re-sort.
103-
sortFile(lastFile)
104-
sortAndMergeRanges(lastFile.blocks).let { lastFile.blocks = it }
105-
sortFiles()
89+
val mergedFiles = sortFiles(newFiles).map { f -> f.copy(blocks = sortAndMergeRanges(f.blocks)) }
90+
val mergedGaps = sortAndMergeRanges(newGaps)
10691

107-
// Sort and merge the gaps.
108-
gaps = sortAndMergeRanges(gaps)
92+
return loop(Filesystem(mergedFiles, mergedGaps))
10993
}
94+
95+
return loop(this)
11096
}
11197

112-
/**
113-
* Reparse before using this: solves part 2.
114-
*/
115-
fun rearrange2() {
116-
for (file in files.reversed()) {
117-
// At this point, each file only has one range.
118-
val fileId = file.id
119-
val fileRange = file.blocks.lastOrNull() ?: error("No data for $fileId")
98+
fun rearrange2(): Filesystem {
99+
// For each file from the end, try to move it into a suitable gap
100+
val reversed = files.asReversed()
101+
val updated = reversed.fold(this) { acc, file ->
102+
val fileRange = file.blocks.lastOrNull() ?: return@fold acc
120103
val fileRangeSize = fileRange.size()
121104

122-
// Find the first gap it will fit.
123-
val gap = gaps.firstOrNull { gap -> gap.size() >= fileRangeSize && gap.start < fileRange.start} ?: continue
124-
val gapSize = gap.size()
125-
126-
when {
127-
fileRangeSize == gapSize -> {
128-
// The files are the same size. Just move the file to the gap.
129-
file.blocks = mutableListOf(gap)
130-
gaps.remove(gap)
131-
}
132-
fileRangeSize < gapSize -> {
133-
val dividingPoint = gap.start + fileRangeSize
134-
val gap1 = gap.start until dividingPoint
135-
val gap2 = dividingPoint..gap.last
136-
137-
file.blocks = mutableListOf(gap1)
138-
gaps.remove(gap)
139-
gaps.add(gap2)
140-
}
105+
// Find the first suitable gap
106+
val gap = acc.gaps.firstOrNull { g ->
107+
g.size() >= fileRangeSize && g.first < fileRange.first
141108
}
142109

143-
// Sort and merge the gaps.
144-
gaps = sortAndMergeRanges(gaps)
145-
}
146-
}
147-
148-
fun sortAndMergeRanges(ranges: MutableList<Range>): MutableList<Range> {
149-
if (ranges.isEmpty()) return mutableListOf()
150-
151-
// Sort the ranges by their start values
152-
val sortedRanges = ranges.sortedBy { it.first }
153-
val merged = mutableListOf<Range>()
154-
155-
var currentRange = sortedRanges.first()
156-
157-
for (range in sortedRanges.drop(1)) {
158-
if (range.first <= currentRange.last + 1) {
159-
// Merge ranges if they overlap or are contiguous.
160-
currentRange = currentRange.first..maxOf(currentRange.last, range.last)
110+
if (gap == null) {
111+
acc
161112
} else {
162-
// Add the non-overlapping range to the result.
163-
merged.add(currentRange)
164-
currentRange = range
113+
val updatedFile: File
114+
val updatedGaps: RangeList
115+
val gapSize = gap.size()
116+
117+
when {
118+
fileRangeSize == gapSize -> {
119+
// Same size: just replace
120+
updatedFile = file.copy(blocks = listOf(gap))
121+
updatedGaps = acc.gaps - listOf(gap)
122+
}
123+
fileRangeSize < gapSize -> {
124+
val dividingPoint = gap.first + fileRangeSize
125+
val gap1 = gap.first until dividingPoint
126+
val gap2 = dividingPoint..gap.last
127+
updatedFile = file.copy(blocks = listOf(gap1))
128+
updatedGaps = (acc.gaps - listOf(gap)) + listOf(gap2)
129+
}
130+
else -> {
131+
// Should not happen as per the logic, but just in case
132+
updatedFile = file
133+
updatedGaps = acc.gaps
134+
}
135+
}
136+
137+
val newFiles = acc.files.map {
138+
if (it.id == file.id) updatedFile else it
139+
}
140+
Filesystem(sortFiles(newFiles), sortAndMergeRanges(updatedGaps))
165141
}
166142
}
167143

168-
// Add the final range
169-
merged.add(currentRange)
170-
return merged
144+
return updated
171145
}
172146

173147
fun checksum(): BigInteger =
174-
files.sumOf { (idx, block) -> block.sumOf { pos -> pos.sumOf { idx * it }.toBigInteger() } }
148+
files.fold(BigInteger.ZERO) { acc, (idx, blocks) ->
149+
acc + blocks.fold(BigInteger.ZERO) { acc2, range ->
150+
acc2 + (range.fold(BigInteger.ZERO) { acc3, blockPos ->
151+
acc3 + (idx * blockPos).toBigInteger()
152+
})
153+
}
154+
}
175155

176156
companion object {
177157
private fun isFile(idx: Int) = idx % 2 == 0
178158
private fun isSpace(idx: Int) = idx % 2 == 1
179159

180160
fun parse(input: String): Filesystem {
181-
val files: MutableList<File> = mutableListOf()
182-
val space: RangeList = mutableListOf()
183-
var currBlock = 0L
184-
185-
input.withIndex().forEach { (idx, ch) ->
186-
val length: Int = ch.digitToInt()
187-
188-
// Skip any entries with length 0.
161+
val parsed = input.withIndex().fold(
162+
Triple(mutableListOf<File>(), mutableListOf<Range>(), 0L)
163+
) { (files, spaces, currBlock), (idx, ch) ->
164+
val length = ch.digitToInt()
189165
if (length > 0) {
190166
val range: Range = currBlock until (currBlock + length)
191-
if (isFile(idx))
192-
files.add(File(idx / 2, mutableListOf(range)))
193-
else if (isSpace(idx))
194-
space.add(range)
195-
currBlock += length
167+
when {
168+
isFile(idx) -> files.add(File(idx / 2, listOf(range)))
169+
isSpace(idx) -> spaces.add(range)
170+
}
171+
Triple(files, spaces, currBlock + length)
172+
} else {
173+
Triple(files, spaces, currBlock)
196174
}
197175
}
198176

199-
return Filesystem(files, space)
177+
val (files, gaps, _) = parsed
178+
val fs = Filesystem(files, gaps)
179+
// Sort everything before returning
180+
return fs.copy(files = fs.sortFiles(fs.files), gaps = fs.sortAndMergeRanges(fs.gaps))
200181
}
201182
}
202183
}
203184

204-
fun answer1(input: String): BigInteger {
205-
val f = Filesystem.parse(input)
206-
f.rearrange1()
207-
return f.checksum()
208-
}
209-
210-
fun answer2(input: String): BigInteger {
211-
val f = Filesystem.parse(input)
212-
f.rearrange2()
213-
return f.checksum()
214-
}
185+
fun answer1(input: String): BigInteger =
186+
Filesystem.parse(input).rearrange1().checksum()
215187

188+
fun answer2(input: String): BigInteger =
189+
Filesystem.parse(input).rearrange2().checksum()
216190

217191
fun main() {
218192
val input = fetchAdventOfCodeInput(2024, 9)

0 commit comments

Comments
 (0)