|
1 | 1 | // Advent of Code 2024, Day 09. |
2 | 2 | // By Sebastian Raaphorst, 2024. |
3 | 3 |
|
4 | | -// This code was a misery to write and could stand for a lot of improvement. |
5 | | - |
6 | 4 | package day09 |
7 | 5 |
|
8 | 6 | import common.aocreader.fetchAdventOfCodeInput |
9 | 7 | import java.math.BigInteger |
10 | 8 |
|
11 | 9 | private typealias Range = LongRange |
12 | | -private typealias RangeList = MutableList<Range> |
| 10 | +private typealias RangeList = List<Range> |
13 | 11 |
|
14 | | -private fun Range.size(): Long = |
15 | | - last - first + 1 |
| 12 | +private fun Range.size(): Long = last - first + 1 |
16 | 13 |
|
17 | | -private data class File(val id: Int, var blocks: RangeList) |
| 14 | +private data class File(val id: Int, val blocks: RangeList) |
18 | 15 |
|
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() |
24 | 19 |
|
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 | + } |
28 | 34 | } |
29 | 35 |
|
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 } } |
34 | 38 |
|
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 |
39 | 53 |
|
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. |
58 | 54 | val blockSize = lastBlock.size() |
59 | 55 | val gapSize = firstGap.size() |
60 | 56 |
|
61 | | - when { |
| 57 | + val (newFiles, newGaps) = when { |
62 | 58 | 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 |
67 | 64 | } |
68 | | - |
69 | 65 | 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 |
72 | 67 | val dividingPoint = firstGap.first + blockSize |
73 | 68 | val subGap1 = firstGap.first until dividingPoint |
74 | 69 | val subGap2 = dividingPoint..firstGap.last |
75 | 70 |
|
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 |
82 | 75 | } |
83 | | - |
84 | 76 | 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 |
87 | 78 | val dividingPoint = lastBlock.last - gapSize + 1 |
88 | 79 | val subBlock1 = lastBlock.first until dividingPoint |
89 | 80 | val subBlock2 = dividingPoint..lastBlock.last |
90 | 81 |
|
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 |
99 | 86 | } |
100 | 87 | } |
101 | 88 |
|
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) |
106 | 91 |
|
107 | | - // Sort and merge the gaps. |
108 | | - gaps = sortAndMergeRanges(gaps) |
| 92 | + return loop(Filesystem(mergedFiles, mergedGaps)) |
109 | 93 | } |
| 94 | + |
| 95 | + return loop(this) |
110 | 96 | } |
111 | 97 |
|
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 |
120 | 103 | val fileRangeSize = fileRange.size() |
121 | 104 |
|
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 |
141 | 108 | } |
142 | 109 |
|
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 |
161 | 112 | } 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)) |
165 | 141 | } |
166 | 142 | } |
167 | 143 |
|
168 | | - // Add the final range |
169 | | - merged.add(currentRange) |
170 | | - return merged |
| 144 | + return updated |
171 | 145 | } |
172 | 146 |
|
173 | 147 | 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 | + } |
175 | 155 |
|
176 | 156 | companion object { |
177 | 157 | private fun isFile(idx: Int) = idx % 2 == 0 |
178 | 158 | private fun isSpace(idx: Int) = idx % 2 == 1 |
179 | 159 |
|
180 | 160 | 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() |
189 | 165 | if (length > 0) { |
190 | 166 | 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) |
196 | 174 | } |
197 | 175 | } |
198 | 176 |
|
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)) |
200 | 181 | } |
201 | 182 | } |
202 | 183 | } |
203 | 184 |
|
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() |
215 | 187 |
|
| 188 | +fun answer2(input: String): BigInteger = |
| 189 | + Filesystem.parse(input).rearrange2().checksum() |
216 | 190 |
|
217 | 191 | fun main() { |
218 | 192 | val input = fetchAdventOfCodeInput(2024, 9) |
|
0 commit comments