Advertisement
hhoppe

Advent of code 2023 day 8 general

Dec 8th, 2023 (edited)
845
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 2.79 KB | None | 0 0
  1. def _extended_gcd(a: int, b: int) -> tuple[int, int, int]:
  2.   """Finds the greatest common divisor using the extended Euclidean algorithm.
  3.  
  4.  Returns:
  5.    (gcd(a, b), x, y) with the property that a * x + b * y = gcd(a, b).
  6.  """
  7.   prev_x, x = 1, 0
  8.   prev_y, y = 0, 1
  9.   while b:
  10.     q = a // b
  11.     x, prev_x = prev_x - q * x, x
  12.     y, prev_y = prev_y - q * y, y
  13.     a, b = b, a % b
  14.   x, y = prev_x, prev_y
  15.   return a, x, y
  16.  
  17.  
  18. def _solve_modulo_congruences(moduli, remainders):
  19.   """Returns `x` satisfying `x % moduli[i] == remainders[i]`; handles non-coprime moduli."""
  20.   def merge(mr1, mr2):
  21.     (m, a), (n, b) = mr1, mr2
  22.     gcd, u, v = _extended_gcd(m, n)
  23.     if 0:  # Simpler algorithm that assumes the moduli are coprime.
  24.       return m * n, (a * v * n + b * u * m) % (m * n)
  25.     else:  # General algorithm; see https://math.stackexchange.com/a/1644698.
  26.       lamb = (a - b) // gcd
  27.       sigma = a - m * u * lamb
  28.       assert sigma == b + n * lamb * v
  29.       lcm = math.lcm(m, n)
  30.       return lcm, sigma % lcm
  31.  
  32.   return functools.reduce(merge, zip(moduli, remainders))[1]
  33.  
  34.  
  35. def day8(s, *, part2=False):
  36.   lines = s.splitlines()
  37.   moves = lines[0]
  38.   routes = {}
  39.   for line in lines[2:]:
  40.     src, dsts = line.split(' = ')
  41.     routes[src] = dsts[1:-1].split(', ')
  42.  
  43.   def advance(node, move):
  44.     return routes[node][{'L': 0, 'R': 1}[move]]
  45.  
  46.   if not part2:
  47.     node = 'AAA'
  48.     for index, move in enumerate(itertools.cycle(moves)):
  49.       if node == 'ZZZ':
  50.         return index
  51.       node = advance(node, move)
  52.  
  53.   # Run an initial simulation long enough that all nodes should be cycling.
  54.   nodes = {node for node in routes if node.endswith('A')}
  55.   num = 1_000
  56.   for index, move in enumerate(itertools.islice(itertools.cycle(moves), num)):
  57.     if all(node.endswith('Z') for node in nodes):
  58.       return index
  59.     nodes = {advance(node, move) for node in nodes}
  60.  
  61.   # Identify the periods and phases of the cycles.
  62.   moduli, remainders = [], []
  63.   for node in nodes:
  64.     last_seen = {}
  65.     for index, move in enumerate(itertools.islice(itertools.cycle(moves), num, None)):
  66.       if node.endswith('Z'):
  67.         if node in last_seen:
  68.           moduli.append(index - last_seen[node])
  69.           break
  70.         last_seen[node] = index
  71.       node = advance(node, move)
  72.     # Fortunately, the problem is special in that each cycle contains a single solution.
  73.     assert len(last_seen) == 1
  74.     remainders.append(last_seen[node])
  75.  
  76.   index = num + _solve_modulo_congruences(moduli, remainders)
  77.   if 1:  # A particular property of this particular input:
  78.     # The remainders happen to align such that the solution lies exactly at math.lcm(*moduli)!
  79.     assert all(remainder + num == modulo for modulo, remainder in zip(moduli, remainders))
  80.     assert index == math.lcm(*moduli)
  81.   return index
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement