From 85f08d9621f3de4329015fd9a5cb6d1b73abdace Mon Sep 17 00:00:00 2001 From: Peter Norvig Date: Fri, 19 Dec 2025 09:11:36 -0800 Subject: [PATCH] Add files via upload --- ipynb/Advent-2025-AI.ipynb | 272 +++++++++++++++++++++++++++++++++++-- 1 file changed, 260 insertions(+), 12 deletions(-) diff --git a/ipynb/Advent-2025-AI.ipynb b/ipynb/Advent-2025-AI.ipynb index 15bd0f0..b7a9856 100644 --- a/ipynb/Advent-2025-AI.ipynb +++ b/ipynb/Advent-2025-AI.ipynb @@ -3601,14 +3601,6 @@ " return ok" ] }, - { - "cell_type": "markdown", - "id": "e762a942-8c4f-4387-9268-1842fd5a8a5a", - "metadata": {}, - "source": [ - "*Kudos to ChatGPT for writing code that works, and for quickly rejecting regions where `total_area > W * H`. But by failing to immediately detect the cases where all the presents trivially fit into 3x3 squares, this program takes over 3 minutes to run, when it could have been done in a millisecond.*" - ] - }, { "cell_type": "code", "execution_count": 57, @@ -3633,6 +3625,262 @@ " solve(text))" ] }, + { + "cell_type": "markdown", + "id": "26b54768-6a65-4ae3-9318-d24b40a30911", + "metadata": {}, + "source": [ + "*Kudos to ChatGPT for writing code that works, and for quickly rejecting regions where `total_area > W * H`. But by failing to immediately detect the cases where all the presents trivially fit into 3x3 squares, this program takes over 3 minutes to run, when it could have been done in a millisecond.*" + ] + }, + { + "cell_type": "markdown", + "id": "9e431b20-0bdd-4d79-86d8-3fe4ede29044", + "metadata": {}, + "source": [ + "# AoC Utilities\n", + "\n", + "*I showed this notebook at a [Hacker Dojo](https://hackerdojo.org/) meetup and one comment was that the lines-of-code comparison was unfair, since I used my utilities module, which saved a lot of lines, particularly with parsing the input. I completely agree with this comment, so I asked each of the LLMs **If you were going to do the Advent of Code programming contest, what set of utility functions would you define ahead of time?***\n", + "\n", + "*They all were quite familiar with AoC and had ideas for utilities. All three came up with very similar functions for these four areas:*\n", + "1) *Input parsing*\n", + "2) *Manipulating a 2D grid and (x, y) coordinate points*\n", + "3) *Graph search (Dijkstra and breadth-first search)*\n", + "4) *Various math functions*\n", + "\n", + "*All three LLMs chose to implement grids as dicts rather than nested lists, perhaps because some AoC problems have grids of unbounded size. Since all three came up with similar code, I asked them **How did you come up with that?** and they each explained:*\n", + "\n", + "***Gemini*** *gave credit to several people for inspirations: [betaveros](https://github.com/betaveros), [ecnerwala](https://github.com/ecnerwala/aoc-2019), [hyperneutrino](https://github.com/hyperneutrino/aoc-header), [Jonathan Paulson](https://github.com/jonathanpaulson/AdventOfCode), and some guy named [Peter Norvig](https://github.com/norvig/pytudes/tree/main) (for the `ints` function, about which Gemini says \"It is a brilliant \"lazy\" hack that saves 10 minutes of typing per day.\") Gemini also mentioned that some programmers at the top of the leaderboard use complex numbers for 2D coordinates, but that an `(x, y)` tuple is easier to read. Gemini also included a complicated function, `solve_cycle`, designed to simulate several generations of a process (like the Game of Life), find a repeated state, and then look up the state for a generation far in the future. Gemini said*: this is a lifesaver for problems that ask \"What will the grid look like after 1,000,000,000 steps?\"\n", + "\n", + "***Claude*** *said its inspiration came from* \"The AoC problem archetypes themselves\" *and* \"common frustrations: The `ints()` regex approach exists because so many inputs look like \"`Sensor at x=2, y=18: closest beacon is at x=-2, y=15`\" and manually parsing that is tedious.\"\n", + "\n", + "**ChatGPT** *had the most to say on how it developed its utilities:*\n", + "\n", + "Over the years, reading:\n", + "- Reddit solution megathreads\n", + "- Writeups by top solvers\n", + "- GitHub repos after the contest\n", + "\n", + "…you see the same shapes again and again, even when code differs. Examples:\n", + "- Everyone’s Dijkstra looks the same.\n", + "- Everyone eventually has a DIRS4.\n", + "- Everyone regrets not having ints() on Day 1.\n", + "\n", + "A more “actionable” mapping (day number → utilities I’d expect)\n", + "\n", + "- Day 1–3: lines, ints, Counter, simple loops\n", + "- Day 4–7: tokenization (words, split_strip), sets/maps, maybe regex\n", + "- Day 8–10: coordinate sets, manhattan, DIRS4, simple simulation\n", + "- Day 11–14: grid regions ⇒ grid_*, neighbors, flood fill (BFS/DFS)\n", + "- Day 15–17: shortest path ⇒ dijkstra (+ sometimes “state includes direction”)\n", + "- Day 18–20: graph building, BFS/DFS, sometimes union-find (add when needed)\n", + "- Day 21–24: DP/memoization ⇒ lru_cache, careful state encoding, sometimes modular math / cycle finding\n", + "- Day 25: usually a “build the thing / find the structure” day; helpers depend on puzzle\n", + "\n", + "\n", + "The template reflects those convergent solutions—but simplified to the core essence.\n", + "\n", + "This is not a library. It’s a memory prosthetic.\n", + "\n", + "*Below I merge the three responses into one for the four areas where they all wrote very similar code, and then I give the functions that were unique to one LLM:*" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "3b3dbe00-a191-412b-b062-edefc9ab4013", + "metadata": {}, + "outputs": [], + "source": [ + "# =============================================================================\n", + "# 1. INPUT PARSING\n", + "# =============================================================================\n", + "\n", + "def read_input(day: int) -> str:\n", + " \"\"\"Read raw input file.\"\"\"\n", + " return open(f\"day{day:02d}.txt\").read().strip()\n", + "\n", + "def lines(s: str) -> list[str]:\n", + " return s.strip().split('\\n')\n", + "\n", + "def paragraphs(s: str) -> list[str]:\n", + " \"\"\"Split on blank lines (common for grouped input).\"\"\"\n", + " return s.strip().split('\\n\\n')\n", + "\n", + "def ints(s: str) -> list[int]:\n", + " \"\"\"Extract all integers from a string, including negatives.\"\"\"\n", + " import re\n", + " return list(map(int, re.findall(r'-?\\d+', s)))\n", + "\n", + "# =============================================================================\n", + "# 2. GRID & COORDINATES\n", + "# =============================================================================\n", + "\n", + "DIRS4 = [(0, 1), (1, 0), (0, -1), (-1, 0)] # right, down, left, up\n", + "DIRS8 = DIRS4 + [(1, 1), (1, -1), (-1, 1), (-1, -1)]\n", + "\n", + "def grid(s: str) -> dict[tuple[int, int], str]:\n", + " \"\"\"Parse a 2D grid into {(row, col): char} dict.\"\"\"\n", + " return {(r, c): ch \n", + " for r, line in enumerate(s.strip().split('\\n')) \n", + " for c, ch in enumerate(line)}\n", + "\n", + "def neighbors4(r, c):\n", + " return [(r + dr, c + dc) for dr, dc in DIRS4]\n", + "\n", + "def neighbors8(r, c):\n", + " return [(r + dr, c + dc) for dr, dc in DIRS8]\n", + "\n", + "def in_bounds(r, c, rows, cols):\n", + " return 0 <= r < rows and 0 <= c < cols\n", + "\n", + "def manhattan(p1, p2):\n", + " \"\"\"\n", + " Manhattan distance between two points (tuples).\n", + " \"\"\"\n", + " return sum(abs(a - b) for a, b in zip(p1, p2))\n", + "\n", + "def print_grid(grid_dict, default='.'):\n", + " \"\"\"\n", + " Visualizes a dictionary grid.\n", + " \"\"\"\n", + " if not grid_dict:\n", + " return\n", + " rs = [r for r, c in grid_dict.keys()]\n", + " cs = [c for r, c in grid_dict.keys()]\n", + " min_r, max_r = min(rs), max(rs)\n", + " min_c, max_c = min(cs), max(cs)\n", + "\n", + " for r in range(min_r, max_r + 1):\n", + " line = \"\"\n", + " for c in range(min_c, max_c + 1):\n", + " line += str(grid_dict.get((r, c), default))\n", + " print(line)\n", + "\n", + "# =============================================================================\n", + "# 3. GRAPH ALGORITHMS\n", + "# =============================================================================\n", + "\n", + "def bfs(start, neighbors_fn, goal_fn=None):\n", + " \"\"\"Generic BFS. Returns distances dict (and path to goal if goal_fn provided).\"\"\"\n", + " dist = {start: 0}\n", + " queue = deque([start])\n", + " while queue:\n", + " node = queue.popleft()\n", + " if goal_fn and goal_fn(node):\n", + " return dist[node], dist\n", + " for neighbor in neighbors_fn(node):\n", + " if neighbor not in dist:\n", + " dist[neighbor] = dist[node] + 1\n", + " queue.append(neighbor)\n", + " return dist\n", + "\n", + "def dijkstra(start, neighbors_fn, goal_fn=None):\n", + " \"\"\"neighbors_fn returns [(neighbor, cost), ...]\"\"\"\n", + " dist = {start: 0}\n", + " pq = [(0, start)]\n", + " while pq:\n", + " d, node = heappop(pq)\n", + " if d > dist.get(node, float('inf')):\n", + " continue\n", + " if goal_fn and goal_fn(node):\n", + " return d\n", + " for neighbor, cost in neighbors_fn(node):\n", + " nd = d + cost\n", + " if nd < dist.get(neighbor, float('inf')):\n", + " dist[neighbor] = nd\n", + " heappush(pq, (nd, neighbor))\n", + " return dist\n", + "\n", + "# =============================================================================\n", + "# 4. MATH & HELPERS\n", + "# =============================================================================\n", + "\n", + "def lcm(a, b):\n", + " return a * b // gcd(a, b)\n", + "\n", + "def lcm_many(nums):\n", + " return reduce(lcm, nums)\n", + "\n", + "def sign(x):\n", + " return (x > 0) - (x < 0)\n", + "\n", + "# =============================================================================\n", + "# Functions unique to Gemini\n", + "# =============================================================================\n", + "\n", + "def digits(text):\n", + " \"\"\"\n", + " Extracts all single digits from a string.\n", + " Example: \"a1b2\" -> [1, 2]\n", + " \"\"\"\n", + " return [int(x) for x in re.findall(r'\\d', text)]\n", + "\n", + "def solve_cycle(start_state, step_fn, steps_needed):\n", + " \"\"\"\n", + " Simulates a process until a cycle is found, then fast-forwards to the target step.\n", + " \n", + " :param start_state: The initial state (must be hashable, e.g., tuple or frozenset).\n", + " :param step_fn: Function(state) -> next_state.\n", + " :param steps_needed: The huge number (e.g., 1_000_000_000).\n", + " :return: The state at `steps_needed`.\n", + " \"\"\"\n", + " seen = {}\n", + " history = []\n", + " curr = start_state\n", + " \n", + " for i in range(steps_needed):\n", + " # If we have seen this state before, we found a cycle!\n", + " if curr in seen:\n", + " first_seen_index = seen[curr]\n", + " cycle_len = i - first_seen_index\n", + " remaining_steps = steps_needed - i\n", + " \n", + " # Calculate where we land in the cycle\n", + " final_index = first_seen_index + (remaining_steps % cycle_len)\n", + " return history[final_index]\n", + " \n", + " # Record state\n", + " seen[curr] = i\n", + " history.append(curr)\n", + " \n", + " # Advance\n", + " curr = step_fn(curr)\n", + " \n", + " return curr\n", + " \n", + "# =============================================================================\n", + "# Functions unique to Claude\n", + "# =============================================================================\n", + "\n", + "def transpose(grid):\n", + " return list(map(list, zip(*grid)))\n", + "\n", + "def flatten(nested):\n", + " return [item for sublist in nested for item in sublist]\n", + "\n", + "# =============================================================================\n", + "# Functions unique to ChatGPT\n", + "# =============================================================================\n", + "\n", + "def words(s: str) -> list[str]:\n", + " return re.findall(r\"[A-Za-z]+\", s)\n", + "\n", + "def split_strip(s: str, sep: str = \",\") -> list[str]:\n", + " return [x.strip() for x in s.split(sep)]\n", + "\n", + "def csv(s, cast=str):\n", + " return [cast(x) for x in s.split(\",\")]\n", + "\n", + "def windows(xs, n):\n", + " for i in range(len(xs)-n+1):\n", + " yield xs[i:i+n]\n", + "\n", + "def pairwise(xs):\n", + " for a,b in zip(xs, xs[1:]):\n", + " yield a,b" + ] + }, { "cell_type": "markdown", "id": "8aa26008-a652-4860-9c84-5ba4344d32f3", @@ -3668,7 +3916,7 @@ " 7.1\tGemini\t.001\t.001\t63\t13\tEasy\n", " 7.2\tGemini\t.002\t.002\t70\t11\tEasy\n", " 8.1\tClaude\t.828\t.583\t91\t27\tEasy\n", - " 8.2\tClaude\t.835\t.618\t82\t11\tEasy; but LLMs Union-Find data type runs slower than simple approach.\n", + " 8.2\tClaude\t.835\t.618\t82\t11\tEasy; but LLMs Union-Find data type runs slower than mine.\n", " 9.1\tChatGPT\t.027\t.037\t33\t7\tEasy\n", " 9.2\tChatGPT\t.771\t.016\t157\t36\tLLM code a bit complicated; human uses “2 point” trick for speedup\n", " 10.1\tGemini\t.005\t.001\t101\t18\tEasy\n", @@ -3676,7 +3924,7 @@ " 11.1\tClaude\t.023\t.001\t83\t11\tEasy; LLM has a bit of vestigial code\n", " 11.2\tClaude\t.001\t.001\t77\t11\tEasy\n", " 12.1\tChatGPT\t3min\t.002\t238\t20\tHuman saw shortcut to avoid search; LLM wrote search functions\n", - " TOTAL 3.29\t1.60 1672\t324\tHuman-written code is 5x briefer 2x faster, even ignoring 12.1" + " TOTAL 3.29\t1.60 1672\t324\tOverall, Human code is 5x briefer, 2x faster (even ignoring 12.1)" ] }, { @@ -3718,7 +3966,7 @@ } ], "source": [ - "summary(answers) # This is the most recent run of this notebook; the chart above is from a previous run." + "summary(answers) # Below is the most recent run of this notebook; the chart above is from a previous run." ] } ], @@ -3738,7 +3986,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.15" + "version": "3.13.5" } }, "nbformat": 4,