···11+import os, sys, re, math, copy, fileinput
22+from string import ascii_uppercase, ascii_lowercase
33+from collections import Counter, defaultdict, deque, namedtuple
44+from itertools import count, product, permutations, combinations, combinations_with_replacement
55+66+import advent
77+from utils import parse_line, parse_nums, mul, all_unique, factors, memoize, primes, resolve_mapping
88+from utils import chunks, parts, gcd, lcm, print_grid, min_max_xy
99+from utils import new_table, transposed, rotated, firsts, lasts
1010+from utils import md5, sha256, VOWELS, CONSONANTS
1111+from utils import Point, DIRS, DIRS_4, DIRS_8, N, NE, E, SE, S, SW, W, NW
1212+# Itertools Functions:
1313+# product('ABCD', repeat=2) AA AB AC AD BA BB BC BD CA CB CC CD DA DB DC DD
1414+# permutations('ABCD', 2) AB AC AD BA BC BD CA CB CD DA DB DC
1515+# combinations_with_replacement('ABCD', 2) AA AB AC AD BB BC BD CC CD DD
1616+# combinations('ABCD', 2) AB AC AD BC BD CD
1717+1818+# day .lines .nlines(negs=True) .pars .npars(negs=True) .board .pboard .tboard
1919+2020+tot = 0
2121+res = []
2222+2323+day = advent.Day(year=2023, day=0)
2424+
+603
2023/utils.py
···11+import re
22+import math
33+import hashlib
44+import operator
55+import copy
66+from collections import Counter
77+from functools import total_ordering, reduce
88+99+1010+LETTERS = [x for x in 'abcdefghijklmnopqrstuvwxyz']
1111+VOWELS = {'a', 'e', 'i', 'o', 'u'}
1212+CONSONANTS = set(x for x in LETTERS if x not in VOWELS)
1313+1414+1515+def parse_line(regex, line):
1616+ """Returns capture groups in regex for line. Int-ifies numbers."""
1717+ ret = []
1818+ for match in re.match(regex, line).groups():
1919+ try:
2020+ ret.append(int(match))
2121+ except ValueError:
2222+ ret.append(match)
2323+2424+ return ret
2525+2626+2727+def parse_nums(line, negatives=True):
2828+ """
2929+ Returns a list of numbers in `line`.
3030+3131+ Pass negatives=False to parse 1-2 as [1, 2].
3232+ """
3333+ num_re = r'-?\d+' if negatives else r'\d+'
3434+ return [int(n) for n in re.findall(num_re, line)]
3535+3636+3737+def new_table(width, height, val=None):
3838+ """Returns a `width` by `height` table populated with `val`."""
3939+ return [[val for _ in range(width)] for _ in range(height)]
4040+4141+4242+def transposed(matrix):
4343+ """Returns the transpose of the given matrix."""
4444+ return [list(r) for r in zip(*matrix)]
4545+4646+4747+def rotated(matrix):
4848+ """Returns the given matrix rotated 90 degrees clockwise."""
4949+ return [list(r) for r in zip(*matrix[::-1])]
5050+5151+def firsts(matrix):
5252+ """Like matrix[0], but for the first column."""
5353+ return rotated(matrix)[0]
5454+5555+def lasts(matrix):
5656+ """Like matrix[-1], but for the last column."""
5757+ return rotated(matrix)[-1]
5858+5959+6060+def mul(lst):
6161+ """Like sum(), but for multiplication."""
6262+ return reduce(operator.mul, lst, 1) # NOQA
6363+6464+6565+def chunks(l, n):
6666+ """Yield successive n-sized chunks from l."""
6767+ for i in range(0, len(l), n):
6868+ yield l[i:i + n]
6969+7070+def parts(l, n):
7171+ """Splits l into n equal parts. Excess (if it exists) returned as the n+1-th."""
7272+ m = len(l) // n
7373+ for i in range(0, n):
7474+ yield l[i*m:(i+1)*m]
7575+7676+ if len(l) % n != 0:
7777+ yield l[m*n:]
7878+7979+8080+def all_unique(lst):
8181+ """Returns True if all items in `lst` are unique."""
8282+ return len(lst) == len(set(lst))
8383+8484+8585+def topsort(graph, tiebreak=None):
8686+ """
8787+ Given a graph where graph[x] is an iterable of edges of directed
8888+ edges originating from x, returns a topologically sorted list of
8989+ nodes in the graph.
9090+9191+ If `tiebreak` is given, this lambda is passed to sorted() when
9292+ choosing what node to visit next.
9393+ """
9494+ if tiebreak is None:
9595+ tiebreak = lambda x: x
9696+9797+ visited = set()
9898+ stack = []
9999+100100+ def _topsort(node):
101101+ visited.add(node)
102102+103103+ # Reversed because the DFS causes equal level nodes to be popped backwards.
104104+ for n in sorted(graph[node], key=tiebreak, reverse=True):
105105+ if n not in visited:
106106+ _topsort(n)
107107+108108+ stack.append(node)
109109+110110+ for n in sorted(graph, key=tiebreak, reverse=True):
111111+ if not n in visited:
112112+ _topsort(n)
113113+114114+ return stack[::-1]
115115+116116+117117+def gcd(a,b):
118118+ """Compute the greatest common divisor of a and b"""
119119+ while b > 0:
120120+ a, b = b, a % b
121121+ return a
122122+123123+124124+def lcm(a, b):
125125+ """Compute the lowest common multiple of a and b"""
126126+ return a * b / gcd(a, b)
127127+128128+129129+def egcd(a, b):
130130+ x0, x1, y0, y1 = 1, 0, 0, 1
131131+ while b:
132132+ q, a, b = a // b, b, a % b
133133+ x0, x1 = x1, x0 - q * x1
134134+ y0, y1 = y1, y0 - q * y1
135135+ return a, x0, y0
136136+137137+def modinv(a, n):
138138+ g, x, _ = egcd(a, n)
139139+ if g == 1:
140140+ return x % n
141141+ else:
142142+ raise ValueError("%d is not invertible mod %d" % (a, n))
143143+144144+def crt(rems, mods):
145145+ """
146146+ Solve a system of modular equivalences via the Chinese Remainder Theorem.
147147+ Does not require pairwise coprime moduli.
148148+149149+ Returns (n, m), where n is the solution and m is the modulo.
150150+151151+ Arguments
152152+ rems: the remainders of the problem
153153+ mods: the modulos of the problem
154154+155155+ """
156156+157157+ # copy inputs
158158+ orems, omods = rems, mods
159159+ rems = list(rems)
160160+ mods = list(mods)
161161+162162+ newrems = []
163163+ newmods = []
164164+165165+ for i in range(len(mods)):
166166+ for j in range(i+1, len(mods)):
167167+ g = gcd(mods[i], mods[j])
168168+ if g == 1:
169169+ continue
170170+ if rems[i] % g != rems[j] % g:
171171+ raise ValueError("inconsistent remainders at positions %d and %d (mod %d)" % (i, j, g))
172172+ mods[j] //= g
173173+174174+ while 1:
175175+ # transfer any remaining gcds to mods[j]
176176+ g = gcd(mods[i], mods[j])
177177+ if g == 1:
178178+ break
179179+ mods[i] //= g
180180+ mods[j] *= g
181181+182182+ if mods[i] == 1:
183183+ continue
184184+185185+ newrems.append(rems[i] % mods[i])
186186+ newmods.append(mods[i])
187187+188188+ rems, mods = newrems, newmods
189189+190190+ # standard CRT
191191+ s = 0
192192+ n = 1
193193+ for k in mods:
194194+ n *= k
195195+196196+ for i in range(len(mods)):
197197+ ni = n // mods[i]
198198+ s += rems[i] * modinv(ni, mods[i]) * ni
199199+ return s % n, n
200200+201201+202202+def min_max_xy(points):
203203+ """
204204+ For a list of points, returns min_x, max_x, min_y, max_y.
205205+ This works on tuples (x, y) and Point(x, y).
206206+ """
207207+ if len(points) == 0:
208208+ return None, None, None, None
209209+ if type(points[0]) == tuple:
210210+ min_x = min(p[0] for p in points)
211211+ max_x = max(p[0] for p in points)
212212+ min_y = min(p[1] for p in points)
213213+ max_y = max(p[1] for p in points)
214214+ else:
215215+ min_x = min(p.x for p in points)
216216+ max_x = max(p.x for p in points)
217217+ min_y = min(p.y for p in points)
218218+ max_y = max(p.y for p in points)
219219+220220+ return min_x, max_x, min_y, max_y
221221+222222+223223+def print_grid(grid, f=None, quiet=False):
224224+ """
225225+ Outputs `grid` to stdout. This works whether `grid` is a 2D array,
226226+ or a sparse matrix (dictionary) with keys either (x, y) or Point(x, y).
227227+228228+ This function also returns a tuple (a, b), where a is the serialized
229229+ representation of the grid, in case what gets printed out to stdout
230230+ needs to be consumed afterwards, and b is a Counter over the values
231231+ in `grid`.
232232+233233+ Arguments:
234234+ f: a function to transform the values of grid to something printable.
235235+ quiet: don't output to stdout.
236236+237237+ Returns:
238238+ List[String]: Serialized, printable version of the grid.
239239+ Counter: The values contained in the grid.
240240+ """
241241+ if f is None:
242242+ f = lambda x: str(x) # NOQA
243243+244244+ counts = Counter()
245245+ serialized = []
246246+247247+ if type(grid) is dict:
248248+ positions = list(grid.keys())
249249+ min_x, max_x, min_y, max_y = min_max_xy(positions)
250250+ if type(positions[0]) is tuple:
251251+ for y in range(min_y, max_y + 1):
252252+ row = ''.join(f(grid.get((x, y), ' ')) for x in range(min_x, max_x + 1))
253253+ if not quiet:
254254+ print(row)
255255+ serialized.append(row)
256256+ for c in row:
257257+ counts[c] += 1
258258+259259+ else:
260260+ # (x, y) => point
261261+ for y in range(min_y, max_y + 1):
262262+ row = ''.join(f(grid.get(Point(x, y), ' ')) for x in range(min_x, max_x + 1))
263263+ if not quiet:
264264+ print(row)
265265+ serialized.append(row)
266266+ for c in row:
267267+ counts[c] += 1
268268+ else:
269269+ min_x = 0
270270+ min_y = 0
271271+ for y in range(len(grid)):
272272+ row = ''.join(f(grid[y][x]) for x in range(len(grid[0])))
273273+ if not quiet:
274274+ print(row)
275275+ serialized.append(row)
276276+ for x, c in enumerate(row):
277277+ counts[c] += 1
278278+ max_x = x
279279+ max_y = y
280280+281281+ if not quiet:
282282+ print("height={} ({} -> {})".format(max_y - min_y + 1, min_y, max_y))
283283+ print("width={} ({} -> {})".format(max_x - min_x + 1, min_x, max_x))
284284+ print("Statistics:")
285285+ for item, num in counts.most_common():
286286+ print("{}: {}".format(item, num))
287287+288288+ return serialized, counts
289289+290290+def resolve_mapping(candidates):
291291+ """
292292+ Given a dictionary `candidates` mapping keys to candidate values, returns
293293+ a dictionary where each `key` maps to a unique `value`. Hangs if intractable.
294294+295295+ Example:
296296+297297+ candidates = {
298298+ 'a': [0, 1, 2],
299299+ 'b': [0, 1],
300300+ 'c': [0],
301301+ }
302302+303303+ resolve_mapping(candidates) -> {'c': 0, 'b': 1, 'a': 2}
304304+ """
305305+ resolved = {}
306306+307307+ # Ensure the mapping is key -> set(values).
308308+ candidates_map = {}
309309+ for k, v in candidates.items():
310310+ candidates_map[k] = set(v)
311311+312312+ while len(resolved) < len(candidates_map):
313313+ for candidate in candidates_map:
314314+ if len(candidates_map[candidate]) == 1 and candidate not in resolved:
315315+ r = candidates_map[candidate].pop()
316316+ for c in candidates_map:
317317+ candidates_map[c].discard(r)
318318+319319+ resolved[candidate] = r
320320+ break
321321+322322+ return resolved
323323+324324+325325+def memoize(f):
326326+ """Simple dictionary-based memoization decorator"""
327327+ cache = {}
328328+329329+ def _mem_fn(*args):
330330+ hargs = (','.join(str(x) for x in args))
331331+ if hargs not in cache:
332332+ cache[hargs] = f(*args)
333333+ return cache[hargs]
334334+335335+ _mem_fn.cache = cache
336336+ return _mem_fn
337337+338338+339339+@memoize
340340+def _eratosthenes(n):
341341+ """http://stackoverflow.com/a/3941967/239076"""
342342+ # Initialize list of primes
343343+ _primes = [True] * n
344344+345345+ # Set 0 and 1 to non-prime
346346+ _primes[0] = _primes[1] = False
347347+348348+ for i, is_prime in enumerate(_primes):
349349+ if is_prime:
350350+ yield i
351351+352352+ # Mark factors as non-prime
353353+ for j in range(i * i, n, i): # NOQA
354354+ _primes[j] = False
355355+356356+357357+@memoize
358358+def primes(n):
359359+ """Return a list of primes from [2, n)"""
360360+ return list(_eratosthenes(n))
361361+362362+363363+@memoize
364364+def factors(n):
365365+ """Returns the factors of n."""
366366+ return sorted(
367367+ x for tup in (
368368+ [i, n // i] for i in range(1, int(n ** 0.5) + 1)
369369+ if n % i == 0)
370370+ for x in tup)
371371+372372+373373+def md5(msg):
374374+ m = hashlib.md5()
375375+ m.update(msg)
376376+ return m.hexdigest()
377377+378378+379379+def sha256(msg):
380380+ s = hashlib.sha256()
381381+ s.update(msg)
382382+ return s.hexdigest()
383383+384384+385385+def knot_hash(msg):
386386+ lengths = [ord(x) for x in msg] + [17, 31, 73, 47, 23]
387387+ sparse = range(0, 256)
388388+ pos = 0
389389+ skip = 0
390390+391391+ for _ in range(64):
392392+ for l in lengths:
393393+ for i in range(l // 2):
394394+ x = (pos + i) % len(sparse)
395395+ y = (pos + l - i - 1) % len(sparse)
396396+ sparse[x], sparse[y] = sparse[y], sparse[x]
397397+398398+ pos = pos + l + skip % len(sparse)
399399+ skip += 1
400400+401401+ hash_val = 0
402402+403403+ for i in range(16):
404404+ res = 0
405405+ for j in range(0, 16):
406406+ res ^= sparse[(i * 16) + j]
407407+408408+ hash_val += res << ((16 - i - 1) * 8)
409409+410410+ return '%032x' % hash_val
411411+412412+413413+HEX_DIRS = {
414414+ 'N': (1, -1, 0),
415415+ 'NE': (1, 0, -1),
416416+ 'SE': (0, 1, -1),
417417+ 'S': (-1, 1, 0),
418418+ 'SW': (-1, 0, 1),
419419+ 'NW': (0, -1, 1),
420420+}
421421+422422+423423+def hex_distance(x, y, z):
424424+ """Returns a given hex point's distance from the origin."""
425425+ return (abs(x) + abs(y) + abs(z)) // 2
426426+427427+428428+@total_ordering
429429+class Point:
430430+ """Simple 2-dimensional point."""
431431+ def __init__(self, x, y):
432432+ self.x = x
433433+ self.y = y
434434+435435+ def __add__(self, other):
436436+ return Point(self.x + other.x, self.y + other.y)
437437+438438+ def __sub__(self, other):
439439+ return Point(self.x - other.x, self.y - other.y)
440440+441441+ def __mul__(self, n):
442442+ return Point(self.x * n, self.y * n)
443443+444444+ def __div__(self, n):
445445+ return Point(self.x / n, self.y / n)
446446+447447+ def __neg__(self):
448448+ return Point(-self.x, -self.y)
449449+450450+ def __eq__(self, other):
451451+ return self.x == other.x and self.y == other.y
452452+453453+ def __ne__(self, other):
454454+ return not self == other
455455+456456+ def __lt__(self, other):
457457+ return self.length < other.length
458458+459459+ def __str__(self):
460460+ return "({}, {})".format(self.x, self.y)
461461+462462+ def __repr__(self):
463463+ return "Point({}, {})".format(self.x, self.y)
464464+465465+ def __hash__(self):
466466+ return hash(tuple((self.x, self.y)))
467467+468468+ def dist(self, other):
469469+ return math.sqrt((self.x - other.x) ** 2 + (self.y - other.y) ** 2)
470470+471471+ def dist_manhattan(self, other):
472472+ return abs(self.x - other.x) + abs(self.y - other.y)
473473+474474+ def dist_chess(self, other):
475475+ return max(abs(self.x - other.x), abs(self.y - other.y))
476476+477477+ def dist_chebyshev(self, other):
478478+ return self.dist_chess(other)
479479+480480+ def angle(self, to=None):
481481+ if to is None:
482482+ return math.atan2(self.y, self.x)
483483+ return math.atan2(self.y - to.y, self.x - to.x)
484484+485485+ def rotate(self, turns):
486486+ """Returns the rotation of the Point around (0, 0) `turn` times clockwise."""
487487+ turns = turns % 4
488488+489489+ if turns == 1:
490490+ return Point(self.y, -self.x)
491491+ elif turns == 2:
492492+ return Point(-self.x, -self.y)
493493+ elif turns == 3:
494494+ return Point(-self.y, self.x)
495495+ else:
496496+ return self
497497+498498+ @property
499499+ def manhattan(self):
500500+ return abs(self.x) + abs(self.y)
501501+502502+ @property
503503+ def chess(self):
504504+ return max(abs(self.x), abs(self.y))
505505+506506+ @property
507507+ def chebyshev(self):
508508+ return self.chess
509509+510510+ @property
511511+ def length(self):
512512+ return math.sqrt(self.x ** 2 + self.y ** 2)
513513+514514+ def neighbours_4(self):
515515+ return [self + p for p in DIRS_4]
516516+517517+ def neighbors_4(self):
518518+ return self.neighbours_4()
519519+520520+ def neighbours(self):
521521+ return self.neighbours_4()
522522+523523+ def neighbors(self):
524524+ return self.neighbours()
525525+526526+ def neighbours_8(self):
527527+ return [self + p for p in DIRS_8]
528528+529529+ def neighbors_8(self):
530530+ return self.neighbours_8()
531531+532532+N = Point(0, 1)
533533+NE = Point(1, 1)
534534+E = Point(1, 0)
535535+SE = Point(1, -1)
536536+S = Point(0, -1)
537537+SW = Point(-1, -1)
538538+W = Point(-1, 0)
539539+NW = Point(-1, 1)
540540+541541+DIRS_4 = DIRS = [
542542+ Point(0, 1), # north
543543+ Point(1, 0), # east
544544+ Point(0, -1), # south
545545+ Point(-1, 0), # west
546546+]
547547+548548+DIRS_8 = [
549549+ Point(0, 1), # N
550550+ Point(1, 1), # NE
551551+ Point(1, 0), # E
552552+ Point(1, -1), # SE
553553+ Point(0, -1), # S
554554+ Point(-1, -1), # SW
555555+ Point(-1, 0), # W
556556+ Point(-1, 1), # NW
557557+]
558558+559559+class UnionFind:
560560+ """
561561+ If this comes in handy, thank you mcpower!
562562+ https://www.reddit.com/r/adventofcode/comments/a9c61w/2018_day_25_solutions/eci5kaf/
563563+ """
564564+ # n: int
565565+ # parents: List[Optional[int]]
566566+ # ranks: List[int]
567567+ # num_sets: int
568568+569569+ def __init__(self, n: int) -> None:
570570+ self.n = n
571571+ self.parents = [None] * n
572572+ self.ranks = [1] * n
573573+ self.num_sets = n
574574+575575+ def find(self, i: int) -> int:
576576+ p = self.parents[i]
577577+ if p is None:
578578+ return i
579579+ p = self.find(p)
580580+ self.parents[i] = p
581581+ return p
582582+583583+ def in_same_set(self, i: int, j: int) -> bool:
584584+ return self.find(i) == self.find(j)
585585+586586+ def merge(self, i: int, j: int) -> None:
587587+ i = self.find(i)
588588+ j = self.find(j)
589589+590590+ if i == j:
591591+ return
592592+593593+ i_rank = self.ranks[i]
594594+ j_rank = self.ranks[j]
595595+596596+ if i_rank < j_rank:
597597+ self.parents[i] = j
598598+ elif i_rank > j_rank:
599599+ self.parents[j] = i
600600+ else:
601601+ self.parents[j] = i
602602+ self.ranks[i] += 1
603603+ self.num_sets -= 1