Advent of Code Day9: Disk Fragmenter
I had a bit of difficulty today (1)https://adventofcode.come/2024/day/9 for two reasons. Firstly, Swift doesn’t seem to be that good with deep recursions. I wanted to use a recursive solution, but my stack size grow too large. Secondly, I didn’t read the requirements for part 2 properly, and it took me a while to figure out how to bubble files up into the empty slots.
I eventually got it done with an imperative loop (2)https://github.com/Abizern/aoc-swift-2024/blob/main/Sources/Day09.swift
Part 1 #
Given a representation for a file system with file blocks and empty spaces, we are supposed to move files from the back into the empty spaces in the front and calculate a checksum.
I created a type to represent either a file block or a space, and this turned out to be helpful for part 2:
enum Descriptor: Equatable, CustomStringConvertible {
case file(id: Int, length: Int)
case empty(length: Int)
var expanded: [Int] {
switch self {
case .file(let id, let length):
Array(repeating: id, count: length)
case .empty(let length):
Array(repeating: Int.min, count: length)
}
}
var fileId: Int {
switch self {
case .file(id: let id, length: _):
id
case .empty(length: _):
Int.min
}
}
var length: Int {
switch self {
case .file(_, let length):
length
case .empty(let length):
length
}
}
}
This meant that the input was an array of these Descriptors
I expanded my list into a list of numbers that matches the examples by using the expanded
var on my type. Then I read from both ends of this list, if there was a space in the front, I appended the last value that was not a space in it’s place. I didn’t keep track of the spaces at the end, because they did not contribute to the checksum.
func rearrange(_ input: Deque<Int>) -> [Int] {
var input = input
var accumulator: [Int] = []
while let f = input.popFirst() {
if f > Int.min {
accumulator.append(f)
} else if !input.isEmpty {
accumulator.append(input.popLast()!)
// Clear out spaces from the back
while !input.isEmpty, input.last! == Int.min {
input.removeLast()
}
} else {
continue
}
}
return accumulator
}
I then had a simple function to calculate the checksum
func checksum(_ input: [Int]) -> Int {
input.enumerated().map(*).reduce(0, +)
}
and the entire solution was just putting these together:
func part1() async throws -> Int {
let files = Deque(diskMap.flatMap(\.expanded))
let rearranged = rearrange(files)
return checksum(rearranged)
}
Part 2 #
This is where I got stuck for a while. Rather than trying to move each fileID once, after every movement of a file block I tried to move the files at the back into any possible new spaces that were made available by the files being moved.
After I went through the example again, I kept track of the current fileID I was trying to move, but all my recursive code seemed to overrun the stack. I’m not sure if I was writing badly recurring code, or whether Swift not being optimised for recursion is an issue. I eventually managed to get my solution to work and my choice of data structure helped.
I run through the fileIDs in reverse, I find the length of the block to move, and then look for free space at the front. If it exists, I replace the old position with empty space and insert the the fileIDs in the space. If there is more space left over, I fill that with an empty block. Then I try the next lowest FileID.
When the fileID becomes 1
I return the list since the 0
files are at the front by definition.
unc defrag(_ input: [Descriptor]) -> [Descriptor] {
var input = input[...]
var highestIndex = input.last!.fileId
while highestIndex > 0 {
guard let candidateIndex = input.firstIndex(where: { $0.fileId == highestIndex }) else { fatalError("We should have fileID \(highestIndex)") }
let candidateLength = input[candidateIndex].length
guard let targetIndex = input.firstIndex(
where: { descriptor in
if case .empty(let length) = descriptor, length >= candidateLength {
true
} else {
false
}
}
),
targetIndex < candidateIndex
else {
highestIndex -= 1
continue
}
input.replaceSubrange(candidateIndex ... candidateIndex, with: [.empty(length: candidateLength)])
let targetLength = input[targetIndex].length
let newTarget = Descriptor.file(id: highestIndex, length: candidateLength)
if targetLength == candidateLength {
input.replaceSubrange(targetIndex ... targetIndex, with: [newTarget])
} else {
input.replaceSubrange(targetIndex ... targetIndex, with: [newTarget, .empty(length: targetLength - candidateLength)])
}
highestIndex -= 1
}
return Array(input)
}
Once that is working, it’s just a procedure to get the final result:
func part2() async throws -> Int {
defrag(diskMap)
.flatMap(\.expanded)
.map { $0 > Int.min ? $0 : 0 }
.enumerated()
.map { $0 * $1 }
.reduce(0, +)
}
And this still ran fairly quickly: in about 0.2s which is good enough.
Final thoughts #
Recursion didn’t work and it bothers me. When I get some time I’ll try it in a different language to see if it works better there.
Reading the question is important. I’m usually diligent about it, but for some reason I was so concerned about my recursive code not working that I didn’t think that maybe I was solving the wrong problem.