From 1b3e58c30713f37e9cd5555d217754a3e5e4f2cb Mon Sep 17 00:00:00 2001 From: felixm Date: Sun, 17 Dec 2023 22:07:11 -0500 Subject: [PATCH] Extend lib and solve day 17 with A*. Fun. --- README.md | 4 ++ d17.py | 78 +++++++++++++++++++++++ dx.py | 39 ++++-------- lib.py | 178 +++++++++++++++++++++++++++++++++++++++++++++++++++-- monitor.py | 39 ++++++++++++ 5 files changed, 307 insertions(+), 31 deletions(-) create mode 100644 d17.py create mode 100755 monitor.py diff --git a/README.md b/README.md index 3e42525..d9df0ce 100644 --- a/README.md +++ b/README.md @@ -12,4 +12,8 @@ - Day 10: 180:00; this one was hard for me. - Day 11: 68:00; okay but not elegant and way too slow ofc; x-ray solution would have been neat - Day 12: 52:00 and 22:00 for leaderboard; had the right idea and I am good at this type of problem +- ... - Day 16: 00:27:30 745; best placement so far, of course still horribly slow +- Day 17: a couple of hours... realized that I need A* after a while; reused + implementation from Project Euler but improved with heapq which was super fun + diff --git a/d17.py b/d17.py new file mode 100644 index 0000000..5b7c623 --- /dev/null +++ b/d17.py @@ -0,0 +1,78 @@ +from lib import * + +EXAMPLE = """ +2413432311323 +3215453535623 +3255245654254 +3446585845452 +4546657867536 +1438598798454 +4457876987766 +3637877979653 +4654967986887 +4564679986453 +1224686865563 +2546548887735 +4322674655533 +""" + +def solve(i: Input, second=False): + g = i.grid2() + starts = [((0, 0), (0, None))] + + def is_goal(node): + pos, _ = node + return pos == (g.n_cols - 1, g.n_rows - 1) + + def neighbors(node): + pos, dirs = node + repeats, prev_dir = dirs + nbs = [] + for dir in [NORTH, WEST, SOUTH, EAST]: + if second: + if repeats < 4 and prev_dir is not None and prev_dir != dir: + continue + if repeats == 10 and prev_dir == dir: + continue + else: + if repeats == 3 and prev_dir == dir: + continue + + if prev_dir == NORTH and dir == SOUTH: + continue + elif prev_dir == SOUTH and dir == NORTH: + continue + elif prev_dir == EAST and dir == WEST: + continue + elif prev_dir == WEST and dir == EAST: + continue + + nb = add2(pos, dir) + if not g.contains(nb): + continue + nbs.append((nb, (repeats + 1 if dir == prev_dir else 1, dir))) + return nbs + + def h(node): + pos, _ = node + return abs(g.n_rows - 1 - pos[0]) + abs(g.n_cols - 1 - pos[1]) + + def d(_, b): + pos, _ = b + if pos == (0, 0): + return 0 + return int(g[pos]) + + a = A_Star(starts, is_goal, h, d, neighbors) + return a.cost + +def main(): + DAY_INPUT = "i17.txt" + print("Example 1:", solve(Input(EXAMPLE))) + print("Solution 1:", solve(Input(DAY_INPUT))) + print("Example 2:", solve(Input(EXAMPLE), True)) + print("Solution 2:", solve(Input(DAY_INPUT), True)) + return + +if __name__ == "__main__": + main() diff --git a/dx.py b/dx.py index cee3769..2dc32c8 100644 --- a/dx.py +++ b/dx.py @@ -1,43 +1,30 @@ -import lib +from lib import * EXAMPLE = """ """ -def solve(lines: list[str]): +def solve(i: Input, second=False): res = 0 - - # g = list(map(list, lines)) - # for (ri, r) in enumerate(g): - # for (ci, c) in enumerate(r): - # pass - - for (i, line) in enumerate(lines): - print(i, line) - # digits = lib.str_to_int_list(line) - # digit = lib.str_to_single_int(line) - return res - -def solve2(lines: list[str]): - res = 0 - for (i, line) in enumerate(lines): - print(i, line) + i.stats() + # g = i.grid2() + # ls = i.lines() + # ps = i.paras() return res def main(): - lines = lib.str_to_lines_no_empty(EXAMPLE) - print("Example 1:", solve(lines)) + DAY_INPUT = "ix.txt" + + print("Example 1:", solve(Input(EXAMPLE))) return - lines = lib.str_to_lines_no_empty(open("ix.txt").read()) - print("Solution 1:", solve(lines)) + print("Solution 1:", solve(Input(DAY_INPUT))) return - lines = lib.str_to_lines_no_empty(EXAMPLE) - print("Example 2:", solve2(lines)) + print("Example 2:", solve(Input(EXAMPLE), True)) return - lines = lib.str_to_lines_no_empty(open("ix.txt").read()) - print("Solution 2:", solve2(lines)) + print("Solution 2:", solve(Input(DAY_INPUT), True)) + return if __name__ == "__main__": main() diff --git a/lib.py b/lib.py index 6373b11..d19913c 100644 --- a/lib.py +++ b/lib.py @@ -1,4 +1,134 @@ import re +import os +import string +import heapq + +NUMBERS = string.digits +LETTERS_LOWER = string.ascii_lowercase +LETTERS_UPPER = string.ascii_uppercase + +UP = (-1, 0) +DOWN = (1, 0) +RIGHT = (0, 1) +LEFT = (0, -1) + +NORTH = UP +SOUTH = DOWN +EAST = RIGHT +WEST = LEFT + +INF = float("inf") +fst = lambda l: l[0] +snd = lambda l: l[1] + +def maps(f, xs): + if isinstance(xs, list): + return [maps(f, x) for x in xs] + return f(xs) + +def mape(f, xs): + return list(map(f, xs)) + +def add2(a: tuple[int, int], b: tuple[int, int]) -> tuple[int, int]: + return (a[0] + b[0], a[1] + b[1]) + +class Grid2D: + def __init__(self, text: str): + lines = [line for line in text.splitlines() if line.strip() != ""] + self.grid = list(map(list, lines)) + self.n_rows = len(self.grid) + self.n_cols = len(self.grid[0]) + + def __getitem__(self, pos: tuple[int, int]): + row, col = pos + return self.grid[row][col] + + def __setitem__(self, pos: tuple[int, int], val): + row, col = pos + self.grid[row][col] = val + + def clone_with_val(self, val): + c = Grid2D("d\nd") + c.n_rows = self.n_rows + c.n_cols = self.n_cols + c.grid = [[val for _ in range(c.n_cols)] + for _ in range(self.n_rows)] + return c + + def rows(self) -> list[list[str]]: + return [row for row in self.grid] + + def cols(self) -> list[list[str]]: + rows = self.rows() + return [[row[col_i] for row in rows] + for col_i in range(self.n_cols)] + + def find(self, chars: str) -> list[tuple[int, int]]: + r = [] + for row_i in range(self.n_rows): + for col_i in range(self.n_cols): + c = (row_i, col_i) + if self[c] in chars: + r.append(c) + return r + + def all_coords(self) -> list[tuple[int, int]]: + return [(row_i, col_i) + for row_i in range(self.n_rows) + for col_i in range(self.n_cols)] + + def row_coords(self, row_i) -> list[tuple[int, int]]: + assert row_i < self.n_rows, f"{row_i=} must be smaller than {self.n_rows=}" + return [(col_i, row_i) for col_i in range(self.n_cols)] + + def col_coords(self, col_i) -> list[tuple[int, int]]: + assert col_i < self.n_cols, f"{col_i=} must be smaller than {self.n_cols=}" + return [(col_i, row_i) for row_i in range(self.n_rows)] + + def contains(self, pos: tuple[int, int]) -> bool: + row, col = pos + return row >= 0 and row < self.n_rows and col >= 0 and col < self.n_cols + + def neighbors_ort(self, pos: tuple[int, int]) -> list[tuple[int, int]]: + ort_rel = [(-1, 0), (0, 1), (1, 0), (0, -1)] + return [add2(pos, off) for off in ort_rel if self.contains(add2(pos, off))] + + def neighbors_vert(self, pos: tuple[int, int]) -> list[tuple[int, int]]: + ort_vert = [(-1, -1), (-1, 1), (1, 1), (1, -1)] + return [add2(pos, off) for off in ort_vert if self.contains(add2(pos, off))] + + def neighbors_adj(self, pos: tuple[int, int]) -> list[tuple[int, int]]: + return self.neighbors_ort(pos) + self.neighbors_vert(pos) + + def print(self): + for r in self.rows(): + print("".join(r)) + + def print_with_gaps(self): + for r in self.rows(): + print(" ".join(map(str, r))) + +class Input: + def __init__(self, text: str): + if os.path.isfile(text): + self.text = open(text).read() + else: + self.text = text + + def stats(self): + print(f" size: {len(self.text)}") + print(f"lines: {len(self.text.splitlines())}") + ps = len(self.paras()) + print(f"paras: {ps}") + + def lines(self) -> list[str]: + return self.text.splitlines() + + def paras(self) -> list[list[str]]: + return [p.splitlines() for p in self.text.split("\n\n")] + + def grid2(self) -> Grid2D: + return Grid2D(self.text) def prime_factors(n): """ @@ -32,14 +162,15 @@ def lcm(numbers: list[int]) -> int: s *= f return s -def str_to_single_int(line: str) -> int: +def str_to_int(line: str) -> int: line = line.replace(" ", "") r = re.compile(r"-?\d+") - for m in r.findall(line): - return int(m) - raise Exception("No single digit sequence in '{line}'") + m = r.findall(line) + assert len(m) == 0, "str_to_int no int" + assert len(m) > 1, "str_to_int multiple ints" + return int(m[0]) -def str_to_int_list(line: str) -> list[int]: +def str_to_ints(line: str) -> list[int]: r = re.compile(r"-?\d+") return list(map(int, r.findall(line))) @@ -48,3 +179,40 @@ def str_to_lines_no_empty(text: str) -> list[str]: def str_to_lines(text: str) -> list[str]: return list(text.splitlines()) + +def count_trailing_repeats(lst): + count = 0 + for elem in reversed(lst): + if elem != lst[-1]: + break + else: + count += 1 + return count + +class A_Star(object): + def __init__(self, starts, is_goal, h, d, neighbors): + """ + :param h: heuristic function + :param d: cost from node to node function + :param neighbors: neighbors function + """ + open_set = [] + g_score = {} + + for start in starts: + heapq.heappush(open_set, (h(start), start)) + g_score[start] = d(0, start) + + while open_set: + current_f_score, current = heapq.heappop(open_set) + if is_goal(current): + self.cost = current_f_score + break + + for neighbor in neighbors(current): + tentative_g_score = g_score[current] + d(current, neighbor) + if neighbor not in g_score or \ + tentative_g_score < g_score[neighbor]: + g_score[neighbor] = tentative_g_score + f_score = g_score[neighbor] + h(neighbor) + heapq.heappush(open_set, (f_score, neighbor)) diff --git a/monitor.py b/monitor.py new file mode 100755 index 0000000..1309ea7 --- /dev/null +++ b/monitor.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python + +import hashlib +import time +import subprocess +import sys + + +def get_file_hash(filename): + hasher = hashlib.sha256() + with open(filename, 'rb') as f: + hasher.update(f.read()) + return hasher.hexdigest() + + +def main(script_name, interval=1): + last_hash = None + process = None + + while True: + try: + current_hash = get_file_hash(script_name) + if current_hash != last_hash: + last_hash = current_hash + if process and process.poll() is None: + process.terminate() + print(f"Detected change in {script_name}, running script...") + process = subprocess.Popen(['pypy3', script_name], shell=False) + time.sleep(interval) + except KeyboardInterrupt: + if process: + process.terminate() + break + except FileNotFoundError: + print("The file was not found. Make sure the script name is correct.") + break + +if __name__ == "__main__": + main(sys.argv[1])