···11+import copy
22+import fileinput
33+44+55+def read_disk(disk_map, part_2=False):
66+ """
77+ Takes a disk map as a string and returns a list of the form
88+ (block_id, length) and the maximium block ID. For free space,
99+ `block_id` is set to None.
1010+ """
1111+ # Pad the disk with free space at the end for part 1.
1212+ disk_map += '0'
1313+1414+ disk = []
1515+ for block_id, (file_length, free_length) in enumerate(zip(disk_map[::2], disk_map[1::2])):
1616+ if part_2:
1717+ disk.append((block_id, int(file_length)))
1818+ disk.append((None, int(free_length)))
1919+ else:
2020+ for _ in range(int(file_length)):
2121+ disk.append((block_id, 1))
2222+ for _ in range(int(free_length)):
2323+ disk.append((None, 1))
2424+2525+ return disk, block_id
2626+2727+def disk_checksum(disk):
2828+ """Returns the checksum of a disk."""
2929+ checksum = 0
3030+ idx = 0
3131+ for block_id, length in disk:
3232+ if block_id == None:
3333+ idx += length
3434+ else:
3535+ for _ in range(length):
3636+ checksum += block_id * idx
3737+ idx += 1
3838+3939+ return checksum
4040+4141+4242+def compact_disk(disk, max_block_id, part_2=False):
4343+ """Performs the compacting operation on the disk."""
4444+ disk = copy.deepcopy(disk)
4545+4646+ # The current block we should try to shift (if in Part 2).
4747+ block = max_block_id
4848+4949+ while True:
5050+ # Search for the next block to compact.
5151+ idx = len(disk) - 1
5252+5353+ # If part 2, it's when we find a block matching the expected ID.
5454+ if part_2:
5555+ while disk[idx][0] != block:
5656+ idx -= 1
5757+ if idx < 0:
5858+ break
5959+6060+ # Otherwise, it's whenever we find a non-empty block.
6161+ else:
6262+ while disk[idx][0] == None:
6363+ idx -= 1
6464+ if idx < 0:
6565+ break
6666+6767+ # Try to perform a shift with the left-most free space we find.
6868+ block_id, block_length = disk[idx]
6969+ for free in range(len(disk)):
7070+ free_id, free_length = disk[free]
7171+7272+ # If the `free` pointer is to the right of the `idx` pointer,
7373+ # we've moved too far to attempt to find a possible swap.
7474+ if idx < free:
7575+ break
7676+7777+ # We've found a valid block to move!
7878+ if free_id == None and block_length <= free_length:
7979+ # Delete the existing free and file blocks (order matters).
8080+ del disk[idx]
8181+ del disk[free]
8282+8383+ # Create new free and file blocks (order matters).
8484+ disk.insert(free, (block_id, block_length))
8585+ disk.insert(idx, (None, block_length))
8686+8787+ # If we're in part 2 and we moved the file into a free
8888+ # spot with more room available, we need to create a
8989+ # new free block with the remainder space.
9090+ if block_length < free_length:
9191+ disk.insert(free + 1, (None, free_length - block_length))
9292+9393+ break
9494+9595+ # Termination conditions.
9696+ if part_2:
9797+ block -= 1
9898+ if block == 0:
9999+ break
100100+ else:
101101+ if idx < free:
102102+ break
103103+104104+ return disk
105105+106106+107107+# Read problem input.
108108+DISK_MAP = fileinput.input()[0].strip()
109109+110110+disk = compact_disk(*read_disk(DISK_MAP))
111111+print("Part 1:", disk_checksum(disk))
112112+113113+disk = compact_disk(*read_disk(DISK_MAP, part_2=True), part_2=True)
114114+print("Part 2:", disk_checksum(disk))
115115+116116+117117+