Binary Diagnostic

Part 1

… is just a simple idiom/expression exercise, count the number of '1' per column in a series of rows (lines) of text.

This task inspired the rant Python Is Slow.

aoc = __import__('aoc-common').aoc()

inp = aoc.input_text(3)

from collections import Counter

lines = inp.split()
ncolumns = len(lines[0])
one_counts = [r.count('1') for r in zip(*lines)]

gam = int(''.join(['01'[x >= len(lines)//2] for x in one_counts]), 2)
eps = int(''.join(['01'[x <= len(lines)//2] for x in one_counts]), 2)

print(f'part 1 answer: {gam*eps}')

Part 2

… is verging on being algorithmic. Winnow down the lines iteratively based on some criteria of this column count until one remains.

Standard operating procedure, the tricky part is figuring out the best way to generalize the two operations (selecting min/max by column) into a single action. This kind of stuff is often a pain point in imperative-like languages: abstracting over some kind of “direction.” Often you end with ugly hacks like step = -1 or +1 or even explicitly with enums like FORWARD, NORTH, and so on. Or, in the case below, this ugly which argument.

Note also there’s no guarantee in the input that this process will end before we run out of columns, but I’ll pass that off as a problem error.


from enum import Enum

class Dir(Enum):
  BIGGER = 0
  SMALLER = 1

nums = [int(x, 2) for x in inp.split()]

def recursive_divide(lst, which, idx=ncolumns):
  assert lst and idx >= 0
  if len(lst) <= 1:
    return lst[0]

  sep = [[], []]
  for x in lst:
    sep[1 & x >> idx - 1].append(x)

  return recursive_divide(
    sep[which.value] if len(sep[0]) <= len(sep[1]) else sep[1^which.value],
    which,
    idx - 1)

oxy = recursive_divide(nums, Dir.BIGGER)
co2 = recursive_divide(nums, Dir.SMALLER)

print(f'part 2 answer: {oxy*co2}')