Compare commits
12
Commits
85e666b23f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d3be567d8 | ||
|
|
f489ad80ed | ||
|
|
bfcdac7e2c | ||
|
|
3c57787661 | ||
|
|
276bb7246f | ||
|
|
4578110de5 | ||
|
|
e9e1c4c43b | ||
|
|
42960b72dd | ||
|
|
c19ae6e8d5 | ||
|
|
5bed66e853 | ||
|
|
40b87eae78 | ||
|
|
f814edcd3a |
No files matched your search
@@ -26,6 +26,7 @@ For each notebook you can hover on the title to see a description, or click the
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/main/ipynb/lispy.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/main/ipynb/lispy.ipynb) | 2026 | <a href="ipynb/lispy.ipynb" title="Tutorial on interpreters.">(How to Write a (Lisp) Interpreter (in Python))</a> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/main/ipynb/RationalPi.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/main/ipynb/RationalPi.ipynb) | 2026 | <a href="ipynb/RationalPi.ipynb" title="Using continued fractions to find fractions (like 22/7) that are close to pi.">Approximating Pi with a Fraction</a> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/main/ipynb/clvr.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/main/ipynb/clvr.ipynb) | 2026 | <a href="ipynb/clvr.ipynb" title="Guess famous phrases from letter shapes. Beginner level.">Did you solve it? R y clvr ngh t rd ths sntnc?</a> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/main/ipynb/palsum.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/main/ipynb/palsum.ipynb) | 2026 | <a href="ipynb/palsum.ipynb" title="Finding all palindromic integers with no zero digit (human vs LLM solutions)">Integer Palindromes</a> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/main/ipynb/Euler3.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/main/ipynb/Euler3.ipynb) | 2026 | <a href="ipynb/Euler3.ipynb" title="For beginner programmers: How to find the largest prime factor of a number. Project Euler #3.">Largest prime factor</a> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/main/ipynb/Euler-Fable.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/main/ipynb/Euler-Fable.ipynb) | 2026 | <a href="ipynb/Euler-Fable.ipynb" title="Solutions to Project Euler math/programming problems by Claude Fable LLM">Project Euler #1–100 by Fable LLM</a> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/main/ipynb/Euler-Kimi.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/main/ipynb/Euler-Kimi.ipynb) | 2026 | <a href="ipynb/Euler-Kimi.ipynb" title="Solutions to Project Euler math/programming problems by Kimi LLM">Project Euler #1–100 by Kimi LLM</a> |
|
||||
@@ -42,6 +43,7 @@ For each notebook you can hover on the title to see a description, or click the
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/main/ipynb/Advent-2025-AI.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/main/ipynb/Advent-2025-AI.ipynb) | 2025 | <a href="ipynb/Advent-2025-AI.ipynb" title="Using large language coding models to solve AoC puzzles">Advent of Code 2025: AI LLM Edition</a> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/main/ipynb/Goldberg.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/main/ipynb/Goldberg.ipynb) | 2019 | <a href="ipynb/Goldberg.ipynb" title="A re-implementation in Python 3 of Yoav Goldberg's unreasonably effective character-level n-gram language model.">Effectiveness of Language Models</a> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/main/ipynb/How%20to%20Do%20Things%20with%20Words.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/main/ipynb/How%20to%20Do%20Things%20with%20Words.ipynb) | 2018 | <a href="ipynb/How%20to%20Do%20Things%20with%20Words.ipynb" title="Spelling Correction, Secret Codes, Word Segmentation, and more">How to Do Things with Words: NLP in Python</a> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/main/ipynb/palsum.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/main/ipynb/palsum.ipynb) | 2026 | <a href="ipynb/palsum.ipynb" title="Finding all palindromic integers with no zero digit (human vs LLM solutions)">Integer Palindromes</a> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/main/ipynb/Triplets.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/main/ipynb/Triplets.ipynb) | 2024 | <a href="ipynb/Triplets.ipynb" title="LLMs do better at producing a program to solve this puzzle than they do at solving it directly">The Languages of English, Math, and Programming</a> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/main/ipynb/CherylMind.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/main/ipynb/CherylMind.ipynb) | 2024 | <a href="ipynb/CherylMind.ipynb" title="Do LLMs have enough theory of mind to solve the Cheryl's Birthday puzzle?">LLMs, Theory of Mind, and Cheryl's Birthday</a> |
|
||||
| [co](https://colab.research.google.com/github/norvig/pytudes/blob/main/ipynb/OneLetterOff.ipynb) [nb](https://nbviewer.jupyter.org/github/norvig/pytudes/blob/main/ipynb/OneLetterOff.ipynb) | 2023 | <a href="ipynb/OneLetterOff.ipynb" title="Word game; use of large language model word embeddings to generate clues.">One Letter Off</a> |
|
||||
|
||||
@@ -328,7 +328,7 @@
|
||||
"def good_dates(puzzle, n=10) -> BeliefState:\n",
|
||||
" \"\"\"Pick a set of `n` dates for which the `puzzle` has a unique solution.\"\"\"\n",
|
||||
" while True:\n",
|
||||
" random_dates = set(random.sample(many_dates, n))\n",
|
||||
" random_dates = set(random.sample(tuple(many_dates), n))\n",
|
||||
" if know(puzzle(random_dates)):\n",
|
||||
" return random_dates"
|
||||
]
|
||||
|
||||
@@ -537,7 +537,7 @@
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Just a few seconds run time; great! "
|
||||
" "
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
+187
-633
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,744 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "e57b9914-8169-4acb-ab11-03891732ecf1",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"<div align=\"right\" style=\"text-align: right\"><i>Peter Norvig<br>Sept 2026</i></div>\n",
|
||||
"\n",
|
||||
"# Integer Palindromes\n",
|
||||
"\n",
|
||||
"Consider this problem:\n",
|
||||
"\n",
|
||||
"***Write a function to return the integer [palindromic numbers](https://en.wikipedia.org/wiki/Palindromic_number) written in base 10, with no zero digits, and whose digits add up to N.***\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"(This is a generalization of a problem from the [2026 AIME](https://artofproblemsolving.com/wiki/index.php/2026_AIME_I_Problems/Problem_2?srsltid=AfmBOor4WAOSBdOAsLKKfku51DG21_h8NUzWEenLIieAMaUFWilw8QeB) (American Invitational Mathematics Examination) which asked only for the count of palindromes with digit sum *N* = 13.) \n",
|
||||
"\n",
|
||||
"Here are my thoughts about the problem:\n",
|
||||
"- A palindrome is a sequence of characters. It is the base 10 string of characters, not the integer itself, that is a palindrome.\n",
|
||||
" - Thus, my function's signature will be `palindromes_with_digit_sum(N: int) -> list[str]`\n",
|
||||
" - For example, `palindromes_with_digit_sum(4) == ['1111', '121', '22', '4']`.\n",
|
||||
" - The empty string is [generally considered](https://en.wikipedia.org/wiki/Empty_string) a palindrome.\n",
|
||||
"- Valid palindromes can be classified into two kinds:\n",
|
||||
" - Base cases: a valid palindrome can be the empty string (when *N* = 0) or can be a single digit (when 1 ≤ *N* ≤ 9).\n",
|
||||
" - Multi-digit cases: a valid palindrome can have first and last digit *d*, and a middle part that is a valid palindrome.\n",
|
||||
" - Middle parts can be found by recursively asking for valid palindromes with digit sum equal to *N* - 2*d*.\n",
|
||||
"\n",
|
||||
"# Solution\n",
|
||||
"\n",
|
||||
"Those thoughts easily translate into a function that is concise and reasonably efficient:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"id": "c096f544-36dd-4d54-9b25-04add9e32bc8",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from functools import cache\n",
|
||||
"\n",
|
||||
"@cache\n",
|
||||
"def palindromes_with_digit_sum(N: int) -> list[str]:\n",
|
||||
" \"\"\"Palindromic digit-strings, with no zero digits, whose digits sum to `N`.\"\"\"\n",
|
||||
" base_cases = [''] if N == 0 else [str(N)] if 1 <= N <= 9 else [] # 0 or 1 digits\n",
|
||||
" multi_cases = [f'{d}{middle}{d}' # 2 or more digits\n",
|
||||
" for d in range(1, 10) if d * 2 <= N\n",
|
||||
" for middle in palindromes_with_digit_sum(N - 2 * d)]\n",
|
||||
" return multi_cases + base_cases"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "0114e46a-a615-4687-adf9-15d6eb177f82",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Here's the program in action:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"id": "8adb65e4-cb37-465c-95bd-8307a1bfe62c",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"['1111111', '11311', '12121', '151', '21112', '232', '313', '7']"
|
||||
]
|
||||
},
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"palindromes_with_digit_sum(7)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "9a2e7597-81c8-4461-8dcb-dc56bbba42d0",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"That looks right."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "925553ad-9dab-4290-84c5-6974fc9a79e2",
|
||||
"metadata": {
|
||||
"jp-MarkdownHeadingCollapsed": true
|
||||
},
|
||||
"source": [
|
||||
"# TL;DR: Comparing Human and LLM Solutions\n",
|
||||
"\n",
|
||||
"I asked four Large Language Model coding agents (Gemini 3.1 Pro, GPT 5.6 Sol, Claude Sonnet 5, and Kimi K3) to solve this problem.\n",
|
||||
"\n",
|
||||
"On the **positive** side, they all wrote correct code that passed all my tests, and they all got the idea that the first part of the digit string has to mirror the last part, and that you shouldn't generate half-parts with a sum greater than *N*/2.\n",
|
||||
"\n",
|
||||
"On the **negative** side, they all ran 10 to 20 times slower compared to my solution (on the particular test suite), largely because none of them used `@cache`. Only Gemini produced what I considered a clear, simple solution (basically the same as mine, but using statements instead of expressions). All the others had overly complicated code (with a line count 6 times mine). Only GPT provided type hints.\n",
|
||||
"\n",
|
||||
"Looking at the code they wrote, it seems like they all failed to realize that the size of the result increases exponentially as roughly 2<sup>*N*/2</sup>, and that caching could reduce an exponential number of calls. All but Gemini failed to realize that there is a simple recursive rule that determines the results for *N* in terms of the results for *N-2* through *N*-18. So I decided to give each LLM a second chance, this time using a prompt that contains two hints:\n",
|
||||
"\n",
|
||||
">*Write a function to return the integer palindromic numbers written in base 10, with no zero digits, and whose digits add up to N.*\n",
|
||||
">\n",
|
||||
">*Keep in mind these two very general CS ideas:*\n",
|
||||
">\n",
|
||||
">1. *Functions that are called repeatedly with the same argument can often benefit from caching.*\n",
|
||||
">\n",
|
||||
">2. *A problem with parameter N can often be broken down to a call or calls to the same function with smaller parameter values, followed by some adaptation to combine the smaller result(s) into the final result.*\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"For each coding agent, the table below lists the lines of code (including blank lines and comments), the run time in seconds on the test suite, and my subjective code style judgment. For the non-human agents, there are also columns for their second prompt (incorporating the two hints). The hints helped a lot! That is a bit surprising: surely the models in some sense \"knew\" the two hints – they had seen lots of example problems where each of the hints were applied – but they failed to properly apply what they \"knew\" without the hint. So (at least this time) the human programmer came out on top.\n",
|
||||
"\n",
|
||||
"|Author|Lines 1|Time 1|Style 1|Lines 2|Time 2|Style 2|\n",
|
||||
"|-----|---:|---:|:---:|---:|---:|:---:|\n",
|
||||
"|Human |10| 0.7|Good|– |– |– |\n",
|
||||
"|Gemini |19| 7.5|Good|42|2.5|Okay|\n",
|
||||
"|GPT |60|10.1|Bad |25|1.1|Good|\n",
|
||||
"|Claude |62|16.4|Bad |20|1.9|Good|\n",
|
||||
"|Kimi |62|14.6|Bad |31|1.0|Good|\n",
|
||||
"\n",
|
||||
"Interestingly, Gemini, which had the best and most concise code with the first prompt, ended up with the least-good and most verbose code after the hints (the hint doubled the lines of code for Gemini and cut it in half for the others)."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "78009bb9-88ba-4d91-9029-09c16288da46",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Analysis\n",
|
||||
"\n",
|
||||
"Here are some facts I discovered while writing my code:\n",
|
||||
" - If *N* is odd, each palindrome must have an odd digit-length, with an odd middle digit.\n",
|
||||
" - If *N* is even, each palindrome can have an even length, or an odd length with an even middle digit.\n",
|
||||
" - The number of solutions for *N* = 2*m* is the same as for *N* = 2*m* + 1. Here's why:\n",
|
||||
" - For each even-length 2*m* solution, you can insert 1 as the middle digit.\n",
|
||||
" - For each odd-length 2*m* solution, you can increase the (even) middle digit by 1.\n",
|
||||
" - The number of solutions for *N* + 2 is either double or slightly less than double the number of solutions for *N*.\n",
|
||||
" - The doubling breaks down when we run out of digits. For example, with *N* = 7, one solution is \"7\", and for *N* + 2 we can change that solution to either \"171\" or \"9\", doubling the count (and similarly for every other palindrome with digit sum *N* = 7). But for the case when *N* = 8, one solution is \"8\", and we can get \"181\", but we can't add 2 to the digit \"8\", so we get one less than double the number of solutions.\n",
|
||||
"\n",
|
||||
"Exploring the counts of number of valid palindromes, and whether they double when *N* increases by 2:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"id": "886cca47-a2d7-4d8b-8e87-90d1c48f56e1",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"count( 2)/count( 0) = 2/1 = 2.000\n",
|
||||
"count( 3)/count( 1) = 2/1 = 2.000\n",
|
||||
"count( 4)/count( 2) = 4/2 = 2.000\n",
|
||||
"count( 5)/count( 3) = 4/2 = 2.000\n",
|
||||
"count( 6)/count( 4) = 8/4 = 2.000\n",
|
||||
"count( 7)/count( 5) = 8/4 = 2.000\n",
|
||||
"count( 8)/count( 6) = 16/8 = 2.000\n",
|
||||
"count( 9)/count( 7) = 16/8 = 2.000\n",
|
||||
"count(10)/count( 8) = 31/16 = 1.938\n",
|
||||
"count(11)/count( 9) = 31/16 = 1.938\n",
|
||||
"count(12)/count(10) = 62/31 = 2.000\n",
|
||||
"count(13)/count(11) = 62/31 = 2.000\n",
|
||||
"count(14)/count(12) = 124/62 = 2.000\n",
|
||||
"count(15)/count(13) = 124/62 = 2.000\n",
|
||||
"count(16)/count(14) = 248/124 = 2.000\n",
|
||||
"count(17)/count(15) = 248/124 = 2.000\n",
|
||||
"count(18)/count(16) = 496/248 = 2.000\n",
|
||||
"count(19)/count(17) = 496/248 = 2.000\n",
|
||||
"count(20)/count(18) = 991/496 = 1.998\n",
|
||||
"count(21)/count(19) = 991/496 = 1.998\n",
|
||||
"count(22)/count(20) = 1980/991 = 1.998\n",
|
||||
"count(23)/count(21) = 1980/991 = 1.998\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"def count(N) -> int: return len(palindromes_with_digit_sum(N))\n",
|
||||
"\n",
|
||||
"# Does the count double when N increases by 2?\n",
|
||||
"for N in range(22):\n",
|
||||
" print(f'count({N+2:2})/count({N:2}) = {count(N+2):4}/{count(N):<4} '\n",
|
||||
" f'= {count(N+2) / count(N):.3f}')"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "9798c834-da89-4ff6-99e7-ae393dde9e93",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Below are the solutions found by all four coding agents, for both prompts. They each chose their own function names, but I appended the model name so we can keep track, and included a `_2_` in the name of functions that came from the second prompt.\n",
|
||||
"\n",
|
||||
"# Gemini \n",
|
||||
"\n",
|
||||
"For the first prompt:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"id": "f2e63e96-7cd9-4459-a626-f09f22bd8e84",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def gemini_generate_palindromes(target_sum):\n",
|
||||
" \"\"\"\n",
|
||||
" Yields all positive integer palindromes (as strings) with no zero digits\n",
|
||||
" whose digits add up to the target_sum.\n",
|
||||
" \"\"\"\n",
|
||||
" # Base case 1: The remaining sum is exactly 0 (even-length palindrome center)\n",
|
||||
" if target_sum == 0:\n",
|
||||
" yield \"\"\n",
|
||||
" \n",
|
||||
" # Base case 2: The remaining sum is a single non-zero digit (odd-length center)\n",
|
||||
" if 1 <= target_sum <= 9:\n",
|
||||
" yield str(target_sum)\n",
|
||||
" \n",
|
||||
" # Recursive step: Wrap matching pairs of digits (1 through 9) around the outside\n",
|
||||
" for d in range(1, 10):\n",
|
||||
" # We need enough remaining sum for two 'd' digits\n",
|
||||
" if target_sum >= 2 * d:\n",
|
||||
" for inner in gemini_generate_palindromes(target_sum - 2 * d):\n",
|
||||
" yield f\"{d}{inner}{d}\""
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "3487c9dd-e943-478b-b0a2-e1580f09bd09",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"For the second prompt, with the two hints:"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"id": "d3454173-ed42-4258-925a-bb9125ea9072",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from functools import lru_cache\n",
|
||||
"from typing import List\n",
|
||||
"\n",
|
||||
"def gemini_2_zero_free_palindromes(n: int) -> List[int]:\n",
|
||||
" \"\"\"\n",
|
||||
" Returns a sorted list of integer palindromes (no zero digits) \n",
|
||||
" whose digits sum exactly to `n`.\n",
|
||||
" \"\"\"\n",
|
||||
" if n <= 0:\n",
|
||||
" return []\n",
|
||||
"\n",
|
||||
" @lru_cache(None)\n",
|
||||
" def build_palindromes(target_sum: int) -> List[str]:\n",
|
||||
" # If the sum overshoots, this path is invalid\n",
|
||||
" if target_sum < 0:\n",
|
||||
" return []\n",
|
||||
" \n",
|
||||
" # Base case for even-length palindromes: the sum is perfectly depleted,\n",
|
||||
" # so the innermost \"middle\" is simply an empty string.\n",
|
||||
" if target_sum == 0:\n",
|
||||
" return [\"\"]\n",
|
||||
" \n",
|
||||
" results = []\n",
|
||||
" \n",
|
||||
" # Base case for odd-length palindromes: a single digit in the exact middle.\n",
|
||||
" # Because zero digits are disallowed, it must strictly be 1 through 9.\n",
|
||||
" if 1 <= target_sum <= 9:\n",
|
||||
" results.append(str(target_sum))\n",
|
||||
" \n",
|
||||
" # Recursive step: wrap a smaller palindrome with a pair of identical digits (1-9)\n",
|
||||
" # on the left and right, subtracting their combined value from the target sum.\n",
|
||||
" for digit in range(1, 10):\n",
|
||||
" remaining_sum = target_sum - (2 * digit)\n",
|
||||
" \n",
|
||||
" if remaining_sum >= 0:\n",
|
||||
" for inner_pal in build_palindromes(remaining_sum):\n",
|
||||
" results.append(str(digit) + inner_pal + str(digit))\n",
|
||||
" \n",
|
||||
" return results\n",
|
||||
"\n",
|
||||
" # Generate the string representations, convert to integers, and return sorted\n",
|
||||
" return sorted(int(p) for p in build_palindromes(n))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "b2794730-0eba-41c2-9cc8-38bf0250ce26",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# GPT "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"id": "1fa05867-c009-44a1-b084-6198c3798123",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from collections.abc import Iterator\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def _halves_with_sum(weights: tuple[int, ...], target: int) -> Iterator[str]:\n",
|
||||
" \"\"\"Yield digit strings whose weighted digit sum is ``target``.\"\"\"\n",
|
||||
"\n",
|
||||
" suffix_weight = [0] * (len(weights) + 1)\n",
|
||||
" for index in range(len(weights) - 1, -1, -1):\n",
|
||||
" suffix_weight[index] = suffix_weight[index + 1] + weights[index]\n",
|
||||
"\n",
|
||||
" digits: list[str] = []\n",
|
||||
"\n",
|
||||
" def visit(index: int, remaining: int) -> Iterator[str]:\n",
|
||||
" if index == len(weights):\n",
|
||||
" if remaining == 0:\n",
|
||||
" yield \"\".join(digits)\n",
|
||||
" return\n",
|
||||
"\n",
|
||||
" weight = weights[index]\n",
|
||||
" rest_weight = suffix_weight[index + 1]\n",
|
||||
" for digit in range(1, 10):\n",
|
||||
" after = remaining - weight * digit\n",
|
||||
" # Every remaining digit is in [1, 9].\n",
|
||||
" if rest_weight <= after <= 9 * rest_weight:\n",
|
||||
" digits.append(str(digit))\n",
|
||||
" yield from visit(index + 1, after)\n",
|
||||
" digits.pop()\n",
|
||||
"\n",
|
||||
" yield from visit(0, target)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def GPT_palindromes_with_digit_sum(n: int) -> Iterator[int]:\n",
|
||||
" \"\"\"Yield positive zero-free decimal palindromes with digit sum ``n``.\n",
|
||||
"\n",
|
||||
" Results are yielded in increasing numeric order. The sequence is finite:\n",
|
||||
" since every digit is at least one, a result can contain at most ``n``\n",
|
||||
" digits.\n",
|
||||
" \"\"\"\n",
|
||||
"\n",
|
||||
" if n < 1:\n",
|
||||
" return\n",
|
||||
"\n",
|
||||
" for length in range(1, n + 1):\n",
|
||||
" paired_digits = length // 2\n",
|
||||
" weights = (2,) * paired_digits\n",
|
||||
" if length % 2:\n",
|
||||
" weights += (1,)\n",
|
||||
"\n",
|
||||
" minimum_sum = sum(weights)\n",
|
||||
" maximum_sum = 9 * minimum_sum\n",
|
||||
" if not minimum_sum <= n <= maximum_sum:\n",
|
||||
" continue\n",
|
||||
"\n",
|
||||
" for half in _halves_with_sum(weights, n):\n",
|
||||
" left = half[:paired_digits]\n",
|
||||
" if length % 2:\n",
|
||||
" palindrome = half + left[::-1]\n",
|
||||
" else:\n",
|
||||
" palindrome = half + half[::-1]\n",
|
||||
" yield int(palindrome)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 7,
|
||||
"id": "585a1991-342b-4b7d-b75c-4ceb9284cce2",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from functools import cache\n",
|
||||
"\n",
|
||||
"@cache\n",
|
||||
"def GPT_2_palindromes(n):\n",
|
||||
" \"\"\"Return all positive base-10 palindromes with no zero digits\n",
|
||||
" whose digits sum to n.\n",
|
||||
" \"\"\"\n",
|
||||
" if n == 0:\n",
|
||||
" return (\"\",) # empty palindrome, useful for constructing 11, 22, ...\n",
|
||||
"\n",
|
||||
" result = []\n",
|
||||
"\n",
|
||||
" # A one-digit palindrome.\n",
|
||||
" if 1 <= n <= 9:\n",
|
||||
" result.append(str(n))\n",
|
||||
"\n",
|
||||
" # Wrap a smaller palindrome with matching digits d ... d.\n",
|
||||
" # This adds 2*d to the digit sum.\n",
|
||||
" for d in range(1, 10):\n",
|
||||
" if 2 * d > n:\n",
|
||||
" break\n",
|
||||
" for middle in GPT_2_palindromes(n - 2 * d):\n",
|
||||
" result.append(str(d) + middle + str(d))\n",
|
||||
"\n",
|
||||
" return tuple(result)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "71ed927d-de25-4f9d-85a7-18c60fdab010",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Claude "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"id": "98d21fdc-ac2f-4f46-bab2-e87dfb84db27",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def gen_digit_sequences(length, target):\n",
|
||||
" \"\"\"\n",
|
||||
" Yield every list of `length` digits, each in 1..9, that sums to `target`.\n",
|
||||
" Uses feasibility pruning (min/max achievable sum) to avoid dead branches.\n",
|
||||
" \"\"\"\n",
|
||||
" if length == 0:\n",
|
||||
" if target == 0:\n",
|
||||
" yield []\n",
|
||||
" return\n",
|
||||
"\n",
|
||||
" remaining_positions = length - 1\n",
|
||||
" min_possible = remaining_positions * 1\n",
|
||||
" max_possible = remaining_positions * 9\n",
|
||||
"\n",
|
||||
" for d in range(1, 10):\n",
|
||||
" rem = target - d\n",
|
||||
" if min_possible <= rem <= max_possible:\n",
|
||||
" for tail in gen_digit_sequences(remaining_positions, rem):\n",
|
||||
" yield [d] + tail\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def claude_find_palindromes(n):\n",
|
||||
" \"\"\"\n",
|
||||
" Return a sorted list of ints: all zero-free palindromes whose digits\n",
|
||||
" sum to n.\n",
|
||||
" \"\"\"\n",
|
||||
" if n < 1:\n",
|
||||
" return []\n",
|
||||
"\n",
|
||||
" palindromes = []\n",
|
||||
" max_length = n # each digit >= 1, so length can't exceed the target sum\n",
|
||||
"\n",
|
||||
" for L in range(1, max_length + 1):\n",
|
||||
" if L % 2 == 0:\n",
|
||||
" h = L // 2\n",
|
||||
" if n % 2 != 0:\n",
|
||||
" continue # 2*sum(half) is always even\n",
|
||||
" target = n // 2\n",
|
||||
" for half in gen_digit_sequences(h, target):\n",
|
||||
" full_digits = half + half[::-1]\n",
|
||||
" palindromes.append(full_digits)\n",
|
||||
" else:\n",
|
||||
" h = (L + 1) // 2\n",
|
||||
" if h == 1:\n",
|
||||
" # single-digit palindrome: the digit itself must equal n\n",
|
||||
" if 1 <= n <= 9:\n",
|
||||
" palindromes.append([n])\n",
|
||||
" continue\n",
|
||||
" # half = prefix (h-1 digits) + middle digit m\n",
|
||||
" # n = 2*sum(prefix) + m\n",
|
||||
" for m in range(1, 10):\n",
|
||||
" rem = n - m\n",
|
||||
" if rem < 0 or rem % 2 != 0:\n",
|
||||
" continue\n",
|
||||
" prefix_target = rem // 2\n",
|
||||
" for prefix in gen_digit_sequences(h - 1, prefix_target):\n",
|
||||
" half = prefix + [m]\n",
|
||||
" full_digits = half + half[:-1][::-1]\n",
|
||||
" palindromes.append(full_digits)\n",
|
||||
"\n",
|
||||
" numbers = sorted(int(\"\".join(map(str, digits))) for digits in palindromes)\n",
|
||||
" return numbers"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"id": "f3eb9970-4dbf-43c1-a3dc-235be81861ed",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from functools import lru_cache\n",
|
||||
"\n",
|
||||
"@lru_cache(maxsize=None)\n",
|
||||
"def claude_2_palindromes_with_digit_sum(n):\n",
|
||||
" \"\"\"Palindromic integers in base 10, with no zero digits,\n",
|
||||
" whose digits add up to n, as a sorted tuple.\"\"\"\n",
|
||||
" if n < 1:\n",
|
||||
" return ()\n",
|
||||
" result = []\n",
|
||||
" if n <= 9:\n",
|
||||
" result.append(n) # single digit\n",
|
||||
" for d in range(1, min(9, n // 2) + 1):\n",
|
||||
" rest = n - 2 * d\n",
|
||||
" if rest == 0:\n",
|
||||
" result.append(11 * d) # just \"dd\"\n",
|
||||
" else:\n",
|
||||
" for p in claude_2_palindromes_with_digit_sum(rest): # smaller subproblem\n",
|
||||
" k = len(str(p))\n",
|
||||
" result.append(d * 10**(k + 1) + p * 10 + d) # wrap: d p d\n",
|
||||
" return tuple(sorted(result))"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "549ea6b6-21f3-49c8-b54a-e70710edbfd5",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Kimi "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"id": "0754167e-a8bb-45a7-80a3-8dd15645e7a2",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"def kimi_palindromes_with_digit_sum(digit_sum, *, as_int=False):\n",
|
||||
" \"\"\"\n",
|
||||
" Return every positive base-10 palindrome with no zero digits whose\n",
|
||||
" digit sum equals `digit_sum`.\n",
|
||||
"\n",
|
||||
" Returns a numerically sorted list of strings by default.\n",
|
||||
" Set as_int=True to return integers.\n",
|
||||
" \"\"\"\n",
|
||||
" if not isinstance(digit_sum, int) or isinstance(digit_sum, bool):\n",
|
||||
" raise TypeError(\"digit_sum must be an integer\")\n",
|
||||
" if digit_sum < 1:\n",
|
||||
" return []\n",
|
||||
"\n",
|
||||
" def compositions(total, parts):\n",
|
||||
" \"\"\"Generate compositions of total into parts, each between 1 and 9.\"\"\"\n",
|
||||
" if parts == 1:\n",
|
||||
" if 1 <= total <= 9:\n",
|
||||
" yield (total,)\n",
|
||||
" return\n",
|
||||
"\n",
|
||||
" lo = max(1, total - 9 * (parts - 1))\n",
|
||||
" hi = min(9, total - (parts - 1))\n",
|
||||
"\n",
|
||||
" for first in range(lo, hi + 1):\n",
|
||||
" for rest in compositions(total - first, parts - 1):\n",
|
||||
" yield (first,) + rest\n",
|
||||
"\n",
|
||||
" found = set()\n",
|
||||
"\n",
|
||||
" # Single-digit palindromes.\n",
|
||||
" if digit_sum <= 9:\n",
|
||||
" found.add(str(digit_sum))\n",
|
||||
"\n",
|
||||
" # Even-length palindromes: left + reverse(left).\n",
|
||||
" # These exist only when the digit sum is even.\n",
|
||||
" if digit_sum % 2 == 0:\n",
|
||||
" half_sum = digit_sum // 2\n",
|
||||
"\n",
|
||||
" for half_length in range(1, half_sum + 1):\n",
|
||||
" for left in compositions(half_sum, half_length):\n",
|
||||
" found.add(\"\".join(map(str, left + left[::-1])))\n",
|
||||
"\n",
|
||||
" # Odd-length palindromes: left + center + reverse(left).\n",
|
||||
" # These must be checked for every digit sum because the center digit\n",
|
||||
" # can make the total digit sum either even or odd.\n",
|
||||
" for half_length in range(1, (digit_sum - 1) // 2 + 1):\n",
|
||||
" for center in range(1, 10):\n",
|
||||
" remaining = digit_sum - center\n",
|
||||
"\n",
|
||||
" if remaining < 0 or remaining % 2:\n",
|
||||
" continue\n",
|
||||
"\n",
|
||||
" left_sum = remaining // 2\n",
|
||||
" if not half_length <= left_sum <= 9 * half_length:\n",
|
||||
" continue\n",
|
||||
"\n",
|
||||
" for left in compositions(left_sum, half_length):\n",
|
||||
" digits = left + (center,) + left[::-1]\n",
|
||||
" found.add(\"\".join(map(str, digits)))\n",
|
||||
"\n",
|
||||
" result = sorted(found, key=lambda x: (len(x), x))\n",
|
||||
" return [int(x) for x in result] if as_int else result"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 11,
|
||||
"id": "18c3302d-d6c8-48fa-899f-65991473513a",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from functools import cache\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"@cache\n",
|
||||
"def kimi_2_palindromes_with_digit_sum(n: int) -> tuple[str, ...]:\n",
|
||||
" \"\"\"\n",
|
||||
" Return all palindromic strings composed of nonzero digits\n",
|
||||
" whose digits sum to n.\n",
|
||||
"\n",
|
||||
" The empty string is included only for n == 0 so that it can be\n",
|
||||
" used to construct two-digit palindromes such as 11 and 22.\n",
|
||||
" \"\"\"\n",
|
||||
" if n == 0:\n",
|
||||
" return (\"\",)\n",
|
||||
"\n",
|
||||
" result = []\n",
|
||||
"\n",
|
||||
" # A one-digit palindrome.\n",
|
||||
" if 1 <= n <= 9:\n",
|
||||
" result.append(str(n))\n",
|
||||
"\n",
|
||||
" # Surround a smaller palindrome with equal nonzero digits.\n",
|
||||
" # The digit sum increases by 2*d.\n",
|
||||
" for d in range(1, 10):\n",
|
||||
" if 2 * d > n:\n",
|
||||
" break\n",
|
||||
"\n",
|
||||
" for middle in kimi_2_palindromes_with_digit_sum(n - 2 * d):\n",
|
||||
" result.append(f\"{d}{middle}{d}\")\n",
|
||||
"\n",
|
||||
" return tuple(result)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "d0199f94-d8d6-4728-8dc6-11a3ee191abc",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Unit Tests and Timing Results\n",
|
||||
"\n",
|
||||
"My function `test` runs a series of test cases for various values of *N* up to 42 (for which there are 2,007,494 valid palindromes) and returns the run time in seconds. I use the local function `f` for canonicalization: an implementation might reasonably choose to produce ints instead of strings, and might return (or yield) the palindromes in any order, not necessarily lexicographic order. If the palindrome function has a cache, it is cleared at the start. "
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"id": "0034dd91-13ab-498d-9e9c-9f7debfd00e7",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"import time\n",
|
||||
"\n",
|
||||
"def test(palindrome_function) -> float:\n",
|
||||
" \"\"\"Tests for the palindrome digit sum problem; return total run time.\"\"\"\n",
|
||||
"\n",
|
||||
" if hasattr(palindrome_function, 'cache_clear'):\n",
|
||||
" palindrome_function.cache_clear()\n",
|
||||
" \n",
|
||||
" def f(N: int) -> list[str]: \n",
|
||||
" \"\"\"Canonicalize the output of palindrome_function(N).\"\"\"\n",
|
||||
" return sorted(map(str, palindrome_function(N)))\n",
|
||||
"\n",
|
||||
" start_time = time.perf_counter()\n",
|
||||
"\n",
|
||||
" # Tests for small values of N\n",
|
||||
" assert f(0) == [''] or f(0) == [] # Unspecified; either answer is acceptable\n",
|
||||
" assert f(1) == ['1']\n",
|
||||
" assert f(2) == ['11', '2']\n",
|
||||
" assert f(3) == ['111', '3']\n",
|
||||
" assert f(4) == ['1111', '121', '22', '4']\n",
|
||||
" assert f(5) == ['11111', '131', '212', '5']\n",
|
||||
" assert f(6) == ['111111', '11211', '1221', '141', '2112', '222', '33', '6']\n",
|
||||
" assert f(7) == ['1111111', '11311', '12121', '151', '21112', '232', '313', '7']\n",
|
||||
"\n",
|
||||
" # Try the function on some larger N and check the number of palindromes.\n",
|
||||
" for N, number in {38: 502_864, 40: 1_004_737, 42: 2_007_494}.items():\n",
|
||||
" assert len(f(N)) == number == len(f(N + 1))\n",
|
||||
"\n",
|
||||
" return time.perf_counter() - start_time # Total run time"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"id": "d2fd7ae9-6a16-404d-aeb9-81ed52ae2a01",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"Here we see the time each function takes to run the test suite. Note all assertions were valid for all functions."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 13,
|
||||
"id": "e23fcf62-dc2c-4c95-9f21-d07197d70743",
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
" 0.7 sec palindromes_with_digit_sum\n",
|
||||
" 7.5 sec gemini_generate_palindromes\n",
|
||||
"10.1 sec GPT_palindromes_with_digit_sum\n",
|
||||
"16.4 sec claude_find_palindromes\n",
|
||||
"14.6 sec kimi_palindromes_with_digit_sum\n",
|
||||
" 2.5 sec gemini_2_zero_free_palindromes\n",
|
||||
" 1.1 sec GPT_2_palindromes\n",
|
||||
" 1.9 sec claude_2_palindromes_with_digit_sum\n",
|
||||
" 1.0 sec kimi_2_palindromes_with_digit_sum\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"functions = (palindromes_with_digit_sum,\n",
|
||||
" gemini_generate_palindromes,\n",
|
||||
" GPT_palindromes_with_digit_sum,\n",
|
||||
" claude_find_palindromes,\n",
|
||||
" kimi_palindromes_with_digit_sum,\n",
|
||||
" gemini_2_zero_free_palindromes,\n",
|
||||
" GPT_2_palindromes,\n",
|
||||
" claude_2_palindromes_with_digit_sum,\n",
|
||||
" kimi_2_palindromes_with_digit_sum)\n",
|
||||
"\n",
|
||||
"for fn in functions:\n",
|
||||
" print(f'{test(fn):4.1f} sec {fn.__name__}')"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.13.9"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
+2
-1
@@ -7,8 +7,9 @@ notebooks = {
|
||||
'Large Language Models': [
|
||||
("AlphaCode Automated Programming", 2022, 'AlphaCode.ipynb', "Analysis of AlphaCode's automated solution to a coding problem"),
|
||||
('Advent of Code 2025: AI LLM Edition', 2025, 'Advent-2025-AI.ipynb', 'Using large language coding models to solve AoC puzzles'),
|
||||
("Effectiveness of Language Models", 2019, 'Goldberg.ipynb', "A re-implementation in Python 3 of Yoav Goldberg's unreasonably effective character-level n-gram language model."),
|
||||
('Effectiveness of Language Models', 2019, 'Goldberg.ipynb', "A re-implementation in Python 3 of Yoav Goldberg's unreasonably effective character-level n-gram language model."),
|
||||
('How to Do Things with Words: NLP in Python', 2018, 'How%20to%20Do%20Things%20with%20Words.ipynb', 'Spelling Correction, Secret Codes, Word Segmentation, and more'),
|
||||
('Integer Palindromes', 2026, 'palsum.ipynb', 'Finding all palindromic integers with no zero digit (human vs LLM solutions)'),
|
||||
('The Languages of English, Math, and Programming', 2024, 'Triplets.ipynb', 'LLMs do better at producing a program to solve this puzzle than they do at solving it directly'),
|
||||
("LLMs, Theory of Mind, and Cheryl's Birthday", 2024, 'CherylMind.ipynb', "Do LLMs have enough theory of mind to solve the Cheryl's Birthday puzzle?"),
|
||||
("One Letter Off", 2023, 'OneLetterOff.ipynb', "Word game; use of large language model word embeddings to generate clues."),
|
||||
|
||||
Reference in new issue
Block a user