No Space Left On Device
Question
This was a fun day! we finally got something challenging.
We got a device from the elves when trying to update that device we got an error that notifies us that there isn't enough memory on our device
$ system-update --please --pretty-please-with-sugar-on-top
Error: No space left on the device
To determine what's going on we start exploring the device file system and record the output (our puzzle input)
For example
$ cd /
$ ls
dir a
14848514 b.txt
8504156 c.dat
dir d
$ cd a
$ ls
dir e
29116 f
2557 g
62596 h.lst
$ cd e
$ ls
584 i
$ cd ..
$ cd ..
$ cd d
$ ls
4060174 j
8033020 d.log
5626152 d.ext
7214296 k
represent the following structure
- / (dir)
- a (dir)
- e (dir)
- i (file, size=584)
- f (file, size=29116)
- g (file, size=2557)
- h.lst (file, size=62596)
- b.txt (file, size=14848514)
- c.dat (file, size=8504156)
- d (dir)
- j (file, size=4060174)
- d.log (file, size=8033020)
- d.ext (file, size=5626152)
- k (file, size=7214296)
When I first solved it I iterated over all commands and didn't create any kind of structure and just kept track of the current dir and update its parent size once we are done with it.
I thought about trying something a bit more interesting for this post, let's create a file system representation from our input and use it later to answer today's puzzle
First, we need to make sense of each line of our input, for that we are going to create a tokenizer!
our tokens
// token/token.go
package token
type TokenType string
const (
Cd TokenType = "Cd"
Ls TokenType = "Ls"
File TokenType = "File"
Dir TokenType = "Dir"
)
type Token struct {
Type TokenType
Literal string
}
We have 4 types of tokens, one for each type of output line.
In addition, we also save the raw data in literal
so we can make use of it later on.
Now let's write our tokenizer, its responsibility is to get our input and transform it into a list of meaningful tokens
// token/tokenizer.go
package token
import "strings"
func newToken(tokenType TokenType, literal string) Token {
return Token{
Type: tokenType,
Literal: literal,
}
}
func Tokenize(raw string) []Token {
lines := strings.Split(string(raw), "\n")
tokens := []Token{}
for _, l := range lines {
parts := strings.Split(l, " ")
// process commands
if parts[0] == "$" {
if parts[1] == "ls" {
tokens = append(tokens, newToken(Ls, l))
} else {
tokens = append(tokens, newToken(Cd, l))
}
// process ls output
} else {
if parts[0] == "dir" {
tokens = append(tokens, newToken(Dir, l))
} else {
tokens = append(tokens, newToken(File, l))
}
}
}
return tokens
}
Create a struct to represent our file system
type FileSystem struct {
root *FileSystemNode
}
type FileSystemNode struct {
Size int
Parent *FileSystemNode
Token token.Token
Name string
Dirs map[string]*FileSystemNode
}
Now to the fun part, going over our tokens and constructing a tree like structure based on that
func newFileSystemNode(name string, token token.Token, parent *FileSystemNode) *FileSystemNode {
return &FileSystemNode{Name: name, Parent: parent, Token: token, Dirs: map[string]*FileSystemNode{}}
}
func NewFileSystem(tokens []token.Token) *FileSystem {
root := newFileSystemNode("/", token.Token{Type: token.Dir, Literal: "/"}, nil)
fileSystem := &FileSystem{root}
current := root
for _, t := range tokens {
switch t.Type {
case token.File:
current.Size += CreateFileNode(t.Literal).Size
case token.Dir:
node := CreateDirNode(t.Literal)
current.Dirs[node.Name] = newFileSystemNode(node.Name, t, current)
case token.Ls:
continue
case token.Cd:
cdNode := CreateCdNode(t.Literal)
if cdNode.To == ".." {
current.Parent.Size += current.Size
current = current.Parent
} else {
_, ok := current.Dirs[cdNode.To]
if ok {
current = current.Dirs[cdNode.To]
}
}
default:
fmt.Println("Unexpected token!", t)
}
}
// In case we are not ending at the root node
for current != root {
current.Parent.Size += current.Size
current = current.Parent
}
return fileSystem
}
We iterate over our tokens and perform some operations depending on the type of token we got
- file token: add the file size to the current dir
- dir token: create a new directory in the current dir and name it based on the token literal (the dir name)
- ls token: we don't care about it and just continue our loop
- cd token:
- ".." literal: we change
current
to becurrent.parent
and add the size of the current dir to the parent - else, its some dir that we have seen before using
ls
and we change the current dir to becurrent.Dirs[dirName]
- ".." literal: we change
type CdNode struct {
To string
}
func CreateCdNode(str string) CdNode {
parts := strings.Split(str, " ")
return CdNode{
To: parts[2],
}
}
At the end of the function we backtrack to our root directory and add each directory size in that path to its parent, this is because our output does not contain ..
commands back to the top.
Now that we have got our file system creation process all dialed in we can start implementing the first part solution
Part 1
We are tasked to find all directories with size <= 100,000
To do that we need to have a way to walk over each directory in our file system structure. Let's add methods to support that capability
func (tree *FileSystem) Walk(visitor func(t *FileSystemNode)) {
tree.root.Walk(visitor)
}
func (node *FileSystemNode) Walk(visitor func(t *FileSystemNode)) {
visitor(node)
for _, t := range node.Dirs {
t.Walk(visitor)
}
}
Both FileSystem and FileSystemNode get a
Walk
method
We pass in a function that will be called on each node in our file system.
Using the above method our solution is now as simple as
func Part1(raw string) int {
fs := parse(raw)
sum := 0
fs.Walk(func(node *fileSystem.FileSystemNode) {
if node.Size <= 100000 {
sum += t.Size
}
})
return sum
}
Part 2
In part 2 we are tasked with increasing the amount of free space to at least 3,000,000 we also know that the total memory on our device is 7,000,000
We need to find the smallest directory that we can delete that will increase the amount of free memory >= 3,000,000
In our example, we have the following options:
- Delete directory e, which would increase unused space by 584.
- Delete directory a, which would increase unused space by 94853.
- Delete directory d, which would increase unused space by 24933642.
- Delete directory /, which would increase unused space by 48381165.
Directories e
and a
are both too small; deleting them would not free up enough space. However, directories d
and /
are both big enough! Between these, choose the smallest: d
, increasing unused space by 24933642.
The logic to solve part 2 is also fairly straightforward but we do need to expose the size of our fileSystem
first
func (tree *FileSystem) Size() int {
return tree.root.Size
}
Part 2 solution
func Part2(raw string) int {
fs := parse(raw)
const OS_MEM = 70000000
const THRESHOLD = 30000000
unusedSpace := OS_MEM - fs.Size()
min := OS_MEM
fs.Walk(func(node *fileSystem.FileSystemNode) {
if unusedSpace+node.Size > THRESHOLD {
if min > node.Size {
min = node.Size
}
}
})
return min
}
For each directory, we check if by deleting it we have enough free memory unusedSpace+t.Size > THRESHOLD
if it does we check to see if it's less than our current smallest directory.
Pheww...That's it for day 7!
I know this approach is a bit too structured for an AoC problem and initially, I solved it without building the entire structure, tokens etc...
For the sake of this blog post, I thought I'll make things a bit more structured and interesting
You can find the complete code here
Thanks for reading!
Top comments (0)