Advent of Code - Day 9: Disk Fragmenter


2024-12-09-generated.png

Another push of the button leaves you in the familiar hallways of some friendly amphipods! Good thing you each somehow got your own personal mini submarine. The Historians jet away in search of the Chief, mostly by driving directly into walls.

-- [Day 9 - Advent of Code 2024[(https://adventofcode.com/2024/day/9)

Solution in Java

Full source can be found: in GitHub

Since todays challenge talks about disk usage with two potential states, one containing files (the FileSegment) the other being empty (the Spacing), I decided to model that out first.

 1sealed interface DiskUsage permits Spacing, FileSegment {  
 2    int size();  
 3}  
 4
 5final class Spacing implements DiskUsage {  
 6    private int size;  
 7    public Spacing(int size) {  
 8        this.size = size;  
 9    }  
10  
11    public int size() {  
12        return size;  
13    }  
14  
15    public void shrink(int size) {  
16        this.size -= size;  
17    }
18}  
19
20record FileSegment(int size, int fileNumber) implements DiskUsage {  
21    public FileSegment reduceSize(int size) {  
22        return new FileSegment(this.size - size, this.fileNumber);  
23    } 
24}

Part 1

The first step I took was to create a small method that would compute the checksum as indicated in the assignment. This code could be re-used for both parts today.

 1private long computeChecksum(List<DiskUsage> diskUsage) {  
 2    var checkSum = 0L;  
 3    var blockPointer = 0;  
 4    for (var file : diskUsage) {  
 5        if (file instanceof FileSegment fileSegment) {  
 6            for (var i = 0; i < fileSegment.size; i++) {  
 7                checkSum += (blockPointer + i) * fileSegment.fileNumber;  
 8            }  
 9        }  
10  
11        blockPointer += file.size();  
12    }  
13  
14    return checkSum;  
15}

After tackling this I needed a way to parse the input into the data structures created earlier.

 1private List<DiskUsage> parseUsage(String input) {  
 2    var diskUsage = new ArrayList<DiskUsage>();  
 3    for (var idx = 0; idx < input.length(); idx++) {  
 4        if (idx % 2 == 0) {  
 5            diskUsage.add(new FileSegment(input.charAt(idx) - '0', idx / 2));  
 6        } else {  
 7            diskUsage.add(new Spacing(input.charAt(idx) - '0'));  
 8        }  
 9    }  
10  
11    return diskUsage;  
12}

This method essentially loops over all the numbers in the input using the each even position as a FileSegment and each odd position as a Spacing.

Now that all that boiler plating is out of the way it was time to write the actual defrag algorithm. For part 1 this was relatively simple as you could move individual segments into any Spacing location.

 1public void part1() {  
 2    var diskUsage = parseUsage(inputLoader.string());  
 3    var updatedList = new ArrayList<DiskUsage>();  
 4  
 5    // defrag algorithm  
 6    for (var idx = 0; idx < diskUsage.size(); idx++) {  
 7        var usage = diskUsage.get(idx);  
 8        switch (usage) {  
 9            case Spacing spacing -> {  
10                // fill from right of list  
11                var toFill = spacing.size();  
12                while (toFill > 0) {  
13                    var fillWith = diskUsage.removeLast();  
14                    switch (fillWith) {  
15                        case FileSegment fileSegment -> {  
16                            var movable = Math.min(toFill, fileSegment.size);  
17                            updatedList.add(new FileSegment(movable, fileSegment.fileNumber));  
18                            if (movable < fileSegment.size) {  
19                                diskUsage.add(fileSegment.reduceSize(movable));  
20                            }  
21                            toFill -= movable;  
22                        }  
23                        case Spacing space -> {}  
24                    }  
25                }  
26            }  
27            case FileSegment fileSegment -> {  
28                log.debug("Encountered file {} of size {}, no need to move.", fileSegment.fileNumber, fileSegment.size);  
29                updatedList.add(fileSegment);  
30            }  
31        }  
32    }
33  
34    validator.part1(computeChecksum(updatedList));  
35}

Part 2

In part 2 all that was needed was a small little change. I loop over the FileSegment from the last element in the list to the current Spacing to find one that fits. If any fits that gets copied over and a Spacing gets put into its place. If we still have space left then we look for the next FileSegment that would fit.

 1public void part2() {  
 2    var diskUsage = parseUsage(inputLoader.string());  
 3  
 4    // defrag algorithm  
 5    for (var idx = 0; idx < diskUsage.size(); idx++) {  
 6        var usage = diskUsage.get(idx);  
 7        switch (usage) {  
 8            case Spacing spacing -> {  
 9                // fill from right of list  
10                for (var rtl = diskUsage.size() - 1; rtl >= idx; rtl--) {  
11                    var fillAttempt = diskUsage.get(rtl);  
12                    if (fillAttempt instanceof FileSegment fileSegment && fileSegment.size <= spacing.size) {  
13                        // insert at idx  
14                        diskUsage.set(idx, fileSegment);  
15  
16                        // replace file with space  
17                        diskUsage.set(rtl, new Spacing(fileSegment.size()));  
18  
19                        spacing.shrink(fileSegment.size);  
20                        if (spacing.size() > 0) {  
21                            diskUsage.add(idx + 1, spacing);  
22                        }  
23  
24                        break;  
25                    }  
26                }  
27            }  
28            case FileSegment s -> {}  
29        }  
30    }
31  
32    validator.part2(computeChecksum(diskUsage));  
33}

See also