Docking Data
2020-12-15
Original Prompt Part 1
I found this prompt very hard to parse, but the puzzle itself is straightforward. The prompt mentions a computer, but it’s not the one we wrote on day 8. In fact for our purposes, it’s a humble dict
. We’re going to be writing numeric values at string keys.
Lines of input come in two forms. Either:
mask = <LONG_STRING>
mem[<NUMBER>] = <NUMBER_2>
If we get a mask
line, we store that value. If we get a mem
line, we’re writing values into the aforementioned storage. The basic setup is simple:
mem = {}mask = ""
for line in self.input: if line.startswith("mask"): mask = line.split(" = ")[1] else: ...
Let’s zoom in on that else
block. The first thing to do is parse out the values from that line. While we could do some surgical split
ting, but a regex will be fewer lines. I won’t go into all of regex here, but will callout a great Python-specific feature: named capture groups. While all regex engines can denote capture groups with ()
, Python lets you name them!
import res = 'mem[42] = 100'
# without named groupsnoname = re.match(r'mem\[(\d+)\] = (\d+)', s)address, value = noname.groups() # => ('42', '100')
# with namesnamed = re.match(r"mem\[(?P<address>\d+)\] = (?P<value>\d+)", s)named.group('address') # '42'named.group('value') # '100'
It’s not much on a regex this small, but it confers a lot of readability. Either way, we can extract out the address
and value
(as strings). Next, we’ve got to do some conversion.
First up is turning a number into a padded binary string. Sounds a little imposing, but Python’s bin
(ary) and rjust
functions make short work of it:
int_to_padded_binary = lambda i: bin(i)[2:].rjust(36, "0")
A couple of points of interest:
- it’s a lambda function since it’s a little one-liner. It could be a regular function too, but it being so simple lets us slim it down
- there are a lot of ways to do do this and they’re all equally correct
- the
bin
function returns the number with a leading0b
. Normally this is fine, but if we further pad the result, we get things like000000b1010
, which isn’t valid. So we drop the prefix with[2:]
With that in hand, we’ve got both our padded binary and our mask, so we’re ready to tackle the example!
value: 000000000000000000000000000000001011 (decimal 11) mask: XXXXXXXXXXXXXXXXXXXXXXXXXXXXX1XXXX0X result: 000000000000000000000000000001001001 (decimal 73)
It’s exceedingly convenient that both strings are the same length; this allows us to take full advantage of Python’s zip
function. It takes two iterables and gives us pairs:
list(zip('ABCD', '1234'))
# [('A', '1'), ('B', '2'), ('C', '3'), ('D', '4')]
We need to go through each matching character from the mask and value and sometimes replace the value character if the mask character isn’t an X
:
def apply_mask_to_int(mask: str, i: int) -> str: res = [] for mask_val, target_val in zip(mask, int_to_padded_binary(i)): if mask_val != 'X': res.append(mask_val) else: res.append(target_val) return ''.join(res)
Not so bad! We can make it a list comprehension without losing much clarity:
def apply_mask_to_int(mask: str, i: int) -> str: return "".join( [ mask_val if mask_val != "X" else target_val for mask_val, target_val in zip(mask, int_to_padded_binary(i)) ] )
Now we call that from our else
block above:
data = re.match(r"mem\[(?P<address>\d+)\] = (?P<value>\d+)", line)
mem[data.group("address")] = int( apply_mask_to_int(mask, int(data.group("value"))), 2)
We call int
twice:
- once to turn our regex-extracted
value
from a string to a number - once to turn our padded binary back into a regular number. Note the call is a little different:
int(padded, 2)
, which tells theint
function to convert from base-2 (aka binary)
To wrap up, we add up all the values stored in memory:
return sum(mem.values())
Part 2
Part 2 is mostly the same as part 1 - the big difference is that the bitmask is applied to the key instead of the value:
mem = {}mask = ""
for line in self.input: if line.startswith("mask"): mask = line.split(" = ")[1] else: data = re.match(r"mem\[(?P<raw_address>\d+)\] = (?P<value>\d+)", line)
...
return sum(mem.values())
Let’s fill in that ...
! First, our mask application is slightly different. Now, we replace the value if anything but 0
. Easy enough:
def apply_mask_to_int_v2(mask: str, i: int) -> str: return "".join( [ target_val if mask_val == "0" else mask_val for mask_val, target_val in zip(mask, int_to_padded_binary(i)) ] )
And we’ll call it from that else
:
floating_mask = apply_mask_to_int_v2( mask, int(data.group("raw_address")))
Now the tricky part. We have to use this same mask repeatedly and replace the X
s differently each time. If we look at their examples, we see a pattern:
…X1101X becomes:
…011010 => 00 …011011 => 01 …111010 => 10 …111011 => 11
Keen readers will clock that replacement as counting up in binary with leading zeroes! We replace X
with padded binary representations of 0, then 1, 2, etc.
Because each X
can have 2 values, a mask will write to 2 ^ num_x
addresses (2 X
s is 4 addresses, 3 is 8, etc). The really sneaky thing is replacing the X
s one at a time. Luckily, Python’s string.replace
function takes an argument to control the number of replacements. So if we iterate through the padded binary, we can replace one X
at a time. This is doable with a small tweak to our padding function:
int_to_padded_binary = lambda i, l=36: bin(i)[2:].rjust(l, "0")
Very similar to before, but the length of the padding is now configurable (and defaults to 36, so no code changes in part 1 are necessary). Now, the good stuff:
def resolve_floaters(mask: str) -> int: num_x = mask.count("X") num_combos = 2 ** num_x
for i in range(num_combos): res = mask # only want to pad for the Xs we have replacements = int_to_padded_binary(i, num_x) for digit in replacements: # each time we loop, another X disappears res = res.replace("X", digit, 1) # send back a regular number yield int(res, 2)
The yield
keys you into the fact that we’ll iterate over the results of this function, which we do in the else
block:
for address in resolve_floaters(floating_mask): mem[address] = int(data.group("value"))