DEV Community

Yuan Gao
Yuan Gao

Posted on

Advent of Code 2021: Day 03 with Python, and more numpy

Link to challenge on Advent of Code 2021 website

Loading the data

The data is a big list of binary values, but we actually need to load the data as a big matrix of 1 or 0 to be able to efficiently slice the matrix while doing calculations.

To do this, we can have numpy fix the data for us. A regular import of lines using "U" format code for strings, imports the data as an array of strings. We also want to know how many bits are in each value, which we get from the first entry.

lines = np.loadtxt("day_3.txt", "U")
bits = int(len(lines[0]))
Enter fullscreen mode Exit fullscreen mode

Resulting in:

array(['011010010110', '101110100110', '011011011110', '100011010001',
       '011011100111', '011000100101', '101101011001', '010001001001',
       '100010110111', '001110110101', '100111011001', 
Enter fullscreen mode Exit fullscreen mode

Then, we can make a view of the data, which will give us a single large array containing one character per entry, and we also cast it to int type:

lines.view('U1').astype(int)
Enter fullscreen mode Exit fullscreen mode

Resulting in:

array([0, 1, 1, ..., 1, 1, 0])
Enter fullscreen mode Exit fullscreen mode

Finally, we need to put this back into the right shape that it was in before, so we reshape it. The final import code is:

lines = np.loadtxt("day_3.txt", "U")
bits = int(len(lines[0]))
data = lines.view('U1').astype(int).reshape(lines.shape[0], bits)
Enter fullscreen mode Exit fullscreen mode

Binary array to int conversion

For both parts of the question, we need to convert an array of bits, like [0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 1, 0] into a number like 1602. There are several ways of doing it, the way I've chosen is to multiply the array by another array that is made up of powers of 2: [..., 32, 16, 8, 4, 2, 1]. So I will pre-calculate pow2 early on in the code:

pow2 = 1<<np.arange(bits)[::-1]
Enter fullscreen mode Exit fullscreen mode

Other methods involve converting the array into a string, and using python's built-in int() function to convert it. int has base-conversion abilities:

bin2dec = lambda x: int("".join(map(str,x.astype(int))),2)
Enter fullscreen mode Exit fullscreen mode

Finally, numpy actually has a function to pack bits back into bytes, however it's a little awkward to use as you need to pad it out to the correct number of values for your eventual type.

bin2dec = lambda x: np.packbits(np.pad(x,[32-bits, 0])).view('>u4')
Enter fullscreen mode Exit fullscreen mode

I'll be using the pow2 method, precalculating it, and doing pow2.dot(x) any time I want to convert x to a number.

Part 1

Part 1 requires counting the number of 0 or 1 in each column and comparing them to see which one is more. Since numpy can directly tell us all the elements that have a given value, and it can also do this per axis.

So the number of 1 in every bit position is:

ones = (data == 1).sum(axis=0)
Enter fullscreen mode Exit fullscreen mode

The result is a count for each column:

array([483, 487, 485, 467, 513, 509, 492, 532, 508, 497, 490, 506])
Enter fullscreen mode Exit fullscreen mode

Doing the same for zeros, and then comparing the two produces an array of true/false, and this can be turned back into a number my doing a dot product against pow2 which was precalculated earlier.

ones = (data == 1).sum(axis=0)
zeros = (data == 0).sum(axis=0)
result = pow2.dot(ones > zeros) * pow2.dot(ones < zeros)
Enter fullscreen mode Exit fullscreen mode

Part 2

Part 2 is...long. I don't even know if I can paraphrase the problem exactly. The idea is to take each column of bits successively, and test to see if each row has a bit value that is the same as the majority of other bits, and discard the value if not. Repeating for the minority case.

To achieve this, we loop through the columns using a for loop:

for idx in range(bits):
  column = data[:, idx]
Enter fullscreen mode Exit fullscreen mode

And then for this columm of pixels, we figure out whether there are more 1 than 0 by taking the sum of the column, and seeing if that number is greater than half the size of the column:

column.sum()*2 >= column.size
Enter fullscreen mode Exit fullscreen mode

This will return True if the 1s are more common than the 0s. Since we need to then collect all the entries that match this value, we can do:

column == (column.sum()*2 >= column.size)
Enter fullscreen mode Exit fullscreen mode

This will return an array of True or False depending on whether the value in column is the same as the majority value or not.

Then, we can select just these entries out of our data matrix, and discard the rest, by using this array as the subscript when accessing the data matrix:

data[column == (column.sum()*2 >= column.size)]
Enter fullscreen mode Exit fullscreen mode

We have to apply this algorithm iteratively over the columns of the array, and stop when data gets to 1 row

for idx in range(bits):
  column = data[:, idx]
  data = data[column == (column.sum()*2 >= column.size)]
  if len(data) == 1:
    break
Enter fullscreen mode Exit fullscreen mode

However, we have to repeat this for the minority case as well. So the final code looks like:

a = b = data
for idx in range(bits):
    acol = a[:, idx]
    bcol = b[:, idx]
    a = a[acol == (acol.sum()*2 >= acol.size)] if len(a) > 1 else a
    b = b[bcol == (bcol.sum()*2 < bcol.size)] if len(b) > 1 else b
result = pow2.dot(a[0] == 1) * pow2.dot(b[0] == 1)
Enter fullscreen mode Exit fullscreen mode

Full Code

import numpy as np

lines = np.loadtxt("day_3.txt", "U")
bits = int(len(lines[0]))
data = lines.view('U1').astype(int).reshape(lines.shape[0], bits)
pow2 = 1 << np.arange(bits)[::-1]

ones = (data == 1).sum(axis=0)
zeros = (data == 0).sum(axis=0)
result = pow2.dot(ones > zeros) * pow2.dot(ones < zeros)
print("Part 1 result:", result)

a = b = data
for idx in range(bits):
    acol = a[:, idx]
    bcol = b[:, idx]
    a = a[acol == (acol.sum()*2 >= acol.size)] if len(a) > 1 else a
    b = b[bcol == (bcol.sum()*2 < bcol.size)] if len(b) > 1 else b
result = pow2.dot(a[0]) * pow2.dot(b[0])
print("Part 2 result:", result)
Enter fullscreen mode Exit fullscreen mode

Discussion (1)

Collapse
hasobi profile image
Hasobi

great post, i have been folowing your method for the past these three days!