|
| 1 | +# AOC 2022 Day 07 |
| 2 | + |
| 3 | +import pathlib |
| 4 | + |
| 5 | +root_path = pathlib.Path.home() / "git" / "AOC2022" / "day07" / "day07" |
| 6 | + |
| 7 | + |
| 8 | +def get_path_name(folder_name): |
| 9 | + if len(folder_name) > 1: |
| 10 | + return "/".join(folder_name)[1:] |
| 11 | + else: |
| 12 | + return "/".join(folder_name) |
| 13 | + |
| 14 | + |
| 15 | +def parse(lines): |
| 16 | + |
| 17 | + # We read each line and create some structures |
| 18 | + # - We maintain a path list stack, which tells us how deep the structure is |
| 19 | + # - It starts with ["/"] |
| 20 | + # - We can do a "/".join(path)[1:] to get the full path, and drop the "/" |
| 21 | + # - We maintain a dictionary of paths to files |
| 22 | + # - The key is the full path |
| 23 | + # - The value is a list of tuples |
| 24 | + # - Each tuple contains the name and file size |
| 25 | + # |
| 26 | + # Here's the plan when we get to each line |
| 27 | + # - if it's a cd command |
| 28 | + # - If it's a directory name, we push that onto the end of the path list |
| 29 | + # - If it's a .., we pop the last item off the path list |
| 30 | + # - if it's an ls command, or a dir <name> |
| 31 | + # - skip to the next line |
| 32 | + # - if it's a number and file |
| 33 | + # - construct the tuple (file, size) |
| 34 | + # - Get the full path with the code above |
| 35 | + # - Is this path in the dictionary? |
| 36 | + # - If not, create it with a blank list |
| 37 | + # - Append the tuple to the path list |
| 38 | + |
| 39 | + # Where do we keep the folder names |
| 40 | + folder_name = ["/"] |
| 41 | + |
| 42 | + # Where do we keep the files and sizes |
| 43 | + folder_files = {} |
| 44 | + |
| 45 | + # Start reading at the second line - we know the first two line |
| 46 | + for line in lines[2:]: |
| 47 | + # Split the line so we can parse it |
| 48 | + part = line.split() |
| 49 | + |
| 50 | + # What kind of line is it? |
| 51 | + # Are we changing into a folder |
| 52 | + if part[1] == "ls": |
| 53 | + # We can skip these lines |
| 54 | + continue |
| 55 | + |
| 56 | + # Is it a folder? We need to ready for it |
| 57 | + if part[0] == "dir": |
| 58 | + # Get the current path, and append this path to it |
| 59 | + path = get_path_name(folder_name) |
| 60 | + if path.endswith("/"): |
| 61 | + path += part[1] |
| 62 | + else: |
| 63 | + path += "/" + part[1] |
| 64 | + |
| 65 | + # Add a blank to the dictionary |
| 66 | + # If we don't do this, we miss empty folders in our output |
| 67 | + # They may contribute to the answer later. |
| 68 | + folder_files[path] = [] |
| 69 | + |
| 70 | + # Keep going |
| 71 | + continue |
| 72 | + |
| 73 | + # Are we changing folders |
| 74 | + if part[1] == "cd": |
| 75 | + # Backing up? Remove this folder |
| 76 | + if part[2] == "..": |
| 77 | + folder_name.pop() |
| 78 | + # Heading down? Add this folder |
| 79 | + else: |
| 80 | + folder_name.append(part[2]) |
| 81 | + |
| 82 | + else: |
| 83 | + # It's a file, so we need the full path |
| 84 | + path = get_path_name(folder_name) |
| 85 | + |
| 86 | + if path not in folder_files.keys(): |
| 87 | + folder_files[path] = [] |
| 88 | + |
| 89 | + # Append this tuple to that list |
| 90 | + folder_files[path].append((part[1], int(part[0]))) |
| 91 | + |
| 92 | + # We're done, return the dictionary |
| 93 | + return folder_files |
| 94 | + |
| 95 | + |
| 96 | +def get_file_sizes(tree): |
| 97 | + |
| 98 | + # We've got the tree, so we need all the keys |
| 99 | + paths = sorted([k for k in tree.keys()], reverse=True) |
| 100 | + |
| 101 | + # We need a dictionary to store the file sizes for each path |
| 102 | + files_sizes = {} |
| 103 | + |
| 104 | + # And a place for the total |
| 105 | + for current_path in paths: |
| 106 | + for containing_path in tree.keys(): |
| 107 | + if containing_path.startswith(current_path): |
| 108 | + # Do we have a current path |
| 109 | + if current_path not in files_sizes.keys(): |
| 110 | + files_sizes[current_path] = 0 |
| 111 | + |
| 112 | + # Add the files in this folder |
| 113 | + for _, size in tree[containing_path]: |
| 114 | + files_sizes[current_path] += size |
| 115 | + |
| 116 | + # print(f"Checking {check_path}... {current_path} is in it") |
| 117 | + |
| 118 | + # DEBUG: Print the files sizes |
| 119 | + # for k, v in files_sizes.items(): |
| 120 | + # print(f"Folder {k} contains {v} bytes of files") |
| 121 | + return files_sizes |
| 122 | + |
| 123 | + |
| 124 | +def part1(file_sizes): |
| 125 | + |
| 126 | + total = 0 |
| 127 | + |
| 128 | + # Now we can go through them all in this order |
| 129 | + for _, total_size in file_sizes.items(): |
| 130 | + if total_size <= 100_000: |
| 131 | + total += total_size |
| 132 | + |
| 133 | + return total |
| 134 | + |
| 135 | + |
| 136 | +def part2(file_sizes): |
| 137 | + # How much free space do we have? |
| 138 | + free_space = 70_000_000 - file_sizes["/"] |
| 139 | + |
| 140 | + # How much more do we need? |
| 141 | + needed_space = 30_000_000 - free_space |
| 142 | + |
| 143 | + # Let's find the one folder which gets us to enough free space |
| 144 | + space = 70_000_000 |
| 145 | + for _, proposed in file_sizes.items(): |
| 146 | + # Not enough space? |
| 147 | + if proposed < needed_space: |
| 148 | + continue |
| 149 | + |
| 150 | + # Found one, let's make sure it's the smallest |
| 151 | + space = min(space, proposed) |
| 152 | + |
| 153 | + return space |
| 154 | + |
| 155 | + |
| 156 | +if __name__ == "__main__": |
| 157 | + |
| 158 | + with open(root_path / "input", "r") as f: |
| 159 | + # with open(root_path / "sample", "r") as f: |
| 160 | + lines = [line.strip() for line in f.readlines()] |
| 161 | + |
| 162 | + tree = parse(lines) |
| 163 | + file_sizes = get_file_sizes(tree) |
| 164 | + |
| 165 | + print(f"Part 1: Answer: {part1(file_sizes)}") |
| 166 | + print(f"Part 2: Answer: {part2(file_sizes)}") |
0 commit comments