Compare commits

...

2 Commits

Author SHA1 Message Date
Peter Norvig
5cbe0f27b7 Add reviews section for pytudes
Added reviews and comments about pytudes from various individuals.
2026-03-05 23:28:53 -08:00
Peter Norvig
24bbe552a5 typos and cleanup in stubborn.ipynb 2026-03-05 15:48:52 -08:00
2 changed files with 145 additions and 163 deletions

View File

@@ -7,24 +7,24 @@
"source": [
"<div style=\"text-align: right\">Peter Norvig<br>Mar 2024</div> \n",
"\n",
"# Stubborn Number Endings\n",
"# Stubborn Number Endings (e.g. 5² = 25)\n",
"\n",
"[Francis Su](https://www.francissu.com/)'s book *Mathematics for Human Flourishing* mentions the fact that numbers that end in \"5\" have a square that also ends in \"5\". \n",
"[Francis Su](https://www.francissu.com/)'s fine book *[Mathematics for Human Flourishing](https://www.francissu.com/flourishing)* mentions the fact that numbers that end in \"5\" have a square that also ends in \"5\". \n",
"\n",
"For example, 5² = 25, 15² = 225, and 25² = 625. \n",
"\n",
"This leads to some questions:\n",
"\n",
"- Is there an easy way to calculate the square of a number ending in \"5\"?\n",
"- Is there an easy way to calculate the square of a number ending in \"5\" without using a calculator?\n",
"- What should we call this property of \"square has same ending\"?\n",
"- Are there other digits besides 5 that have this property?\n",
"- Can we prove the property, not just show some examples?\n",
"- Are there multi-digit endings that have this property?\n",
"- Can we prove it, not just show some examples?\n",
"\n",
"\n",
"## Is there an easy way to calculate the square of a number ending in \"5\"?\n",
"\n",
"Let's make a table of {number: square} pairs and try to see a pattern:"
"Let's make a table of {number: square} pairs and look for a pattern:"
]
},
{
@@ -111,36 +111,34 @@
"\n",
"## What should we call this property?\n",
"\n",
"Let's define an **ending** as the rightmost-digits (zero or more) of a number in decimal notation. \n",
"Let's define an **ending** as the rightmost-digits of an integer in decimal notation. \n",
"\n",
"Then we can say that an ending is **stubborn** if every number with that ending has a square with the same ending.\n",
"Then we can say that an ending is **stubborn** if every integer with that ending has the same ending when squared.\n",
"\n",
"## Are there other digits besides 5 that are stubborn?\n",
"\n",
"We could work this out in our heads, or with paper and pencil, or we can compute it with an expression:"
"We could work this out in our heads, or with paper and pencil, or we can compute it:"
]
},
{
"cell_type": "code",
"execution_count": 15,
"id": "c86f99ed-48fa-4954-9467-b72d67252d73",
"execution_count": 3,
"id": "ddcdafa8-bfa6-4707-951c-6683701e06c2",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'0', '1', '5', '6'}"
"{0, 1, 5, 6}"
]
},
"execution_count": 15,
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"digits = '0123456789'\n",
"\n",
"{e for e in digits if str(int(e) ** 2).endswith(e)}"
"{e for e in range(10) if str(e ** 2).endswith(str(e))}"
]
},
{
@@ -152,23 +150,27 @@
"\n",
"## Can we prove stubborness?\n",
"\n",
"We have seen that 0² ends in 0, 1² ends in 1, 5² ends in 5, and 6² ends in 6. And we have checked some numbers with those endings, for example, 245² ends in 5. But can we **prove** that **every** number ending in 0, 1, 5, or 6 has a square that ends in the same digit?\n",
"We have seen that 0² ends in 0, 1² ends in 1, 5² ends in 5, and 6² ends in 6. And we have checked some numbers with those endings, for example, 245² ends in 5. But can we **prove** that **every** integer ending in 0, 1, 5, or 6 has a square that ends in the same digit?\n",
"\n",
"Some notation: I'll use quote marks, as in \"*se*\" to mean the string of staring digits \"*s*\" followed by the string of ending digits \"*e*\".\n",
"Some notation: I'll use quote marks, as in \"*se*\" to mean the string of starting digits \"*s*\" followed by the string of ending digits \"*e*\".\n",
"\n",
"With a little bit of algebra we can see that if *s* is any string of digits and *e* is a single ending digit, then:\n",
"\n",
"\"*se*\"² = (10⋅*s* + *e*)² = (10⋅*s*)² + 2⋅(10⋅*s**e*) + *e*² = 10 ⋅ (10⋅*s*² + 2⋅*s*⋅*e*) + *e*²\n",
"\"*se*\"² = (10⋅*s* + *e*)² = (10⋅*s*)² + 2⋅(10⋅*s**e*) + *e*² = 10 ⋅ (10⋅*s*² + 2⋅*s*⋅*e*) + *e*² = *e*² (mod 10)\n",
"\n",
"This is ten times some integer, plus *e*², so \"*se*\"² ends in the digit *e* if and only if *e*² ends in *e*, and we know that is true for 0, 1, 5, and 6, and for no other digits.\n",
"In other words, \"*se*\"² is 10 times some integer plus *e*², so \"*se*\"² ends in the digit *e* if and only if *e*² ends in *e*. \n",
"\n",
"We know that is true for 0, 1, 5, and 6, and for no other digits.\n",
"\n",
"## Are there multi-digit endings that are stubborn?\n",
"\n",
"The algebraic argument above can be extended to work with an ending string *e* that is *k* digits long:\n",
"\n",
"\"*se*\"² = (10<sup>*k*</sup>⋅*s* + *e*)² = (10<sup>*k*</sup>⋅*s*)² + 2⋅(10<sup>*k*</sup>⋅*s* ⋅ *e*) + *e*² = 10<sup>*k*</sup> ⋅ (10<sup>*k*</sup>⋅*s*² + 2⋅*s*⋅*e*) + *e*²\n",
"\"*se*\"² = (10<sup>*k*</sup>⋅*s* + *e*)² = (10<sup>*k*</sup>⋅*s*)² + 2⋅(10<sup>*k*</sup>⋅*s* ⋅ *e*) + *e*² = 10<sup>*k*</sup> ⋅ (10<sup>*k*</sup>⋅*s*² + 2⋅*s*⋅*e*) + *e*² = *e*² (mod 10)\n",
"\n",
"This is 10<sup>*k*</sup> times some integer, plus *e*², so again \"*se*\"² ends in *e* if and only if *e*² ends in *e*. To put it another way, to test whether *e* is stubborn, all we have to do is square *e* and see if the result ends in *e*. There is one complication: we'd like to say that \"00\" is stubborn, because any number ending in \"00\", when squared, also ends in \"00\", for example 200² = 40000. But 00² is zero, which we write as \"0\", not as \"00\" or \"0000\". To make sure that \"00\" is considered stubborn, I will define the predicate function `stubborn(ending)` to test the square of \"1\" followed by the ending (I could have chosen any other starting digit string and the result would be the same). \n"
"Again, \"*se*\"² ends in *e* if and only if *e*² ends in *e*. To test whether *e* is a stubborn ending (with all possible starting digit strings), all we have to do is square *e* and see if the result ends in *e*. \n",
"\n",
"There is one **complication**: we'd like to say that \"0\" and \"00\" are two distinct stubborn endings, but 0 and 00 are the same integer. To distinguish them, I will describe endings as strings, not integers. I will define the predicate function `stubborn(ending: str)`:"
]
},
{
@@ -179,8 +181,10 @@
"outputs": [],
"source": [
"def stubborn(ending: str) -> bool:\n",
" \"\"\"Does the square of any number with this ending also end with this ending?\"\"\"\n",
" return str(int(\"1\" + ending) ** 2).endswith(ending)"
" \"\"\"Does every integer with this ending, when squared, also end with this ending?\"\"\"\n",
" start = \"1\" # Arbitrary choice of start; could be any non-zero digit string\n",
" square = str(int(start + ending) ** 2)\n",
" return square.endswith(ending)"
]
},
{
@@ -193,7 +197,7 @@
},
{
"cell_type": "code",
"execution_count": 17,
"execution_count": 5,
"id": "57e64e31-0674-4849-bf72-f6c6b437009a",
"metadata": {},
"outputs": [
@@ -203,13 +207,16 @@
"['00', '01', '25', '76']"
]
},
"execution_count": 17,
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"[s + e for s in digits for e in digits if stubborn(s + e)]"
"digits = '0123456789'\n",
"endings = [s + e for s in digits for e in digits]\n",
"\n",
"list(filter(stubborn, endings))"
]
},
{
@@ -217,12 +224,12 @@
"id": "61a2b8e2-2bdf-458f-adaf-ffde56125862",
"metadata": {},
"source": [
"We could easily continue on in this way, enumerating all three-, four- or even six-digit endings, and checking each one to see if it is stubborn. There are only a million six-digit endings. But there are a quadrillion 15-digit endings, so it would take a *long* time to check all of those. And 100-digit endings? Forget about it. Instead we need to rely on a simplification:\n",
"We could easily continue on in this way, enumerating all three-, four-, or more-digit endings, and checking each one to see if it is stubborn. There are only a million possible six-digit endings; we could quickly check them all. But there are a quadrillion 15-digit endings, so it would take a *long* time to check all of those. And 100-digit endings? Forget about it. Instead we need to rely on an optimization:\n",
"- Any two-digit stubborn ending ('00', '01', '25', '76') has to end in a one-digit-stubborn ending ('0', '1', '5', '6').\n",
"- In general, any *d*-digit stubborn ending has to end in a (*d*-1)-digit stubborn ending.\n",
"- So, to find the stubborn endings of length 100, I don't need to generate and test all 10<sup>100</sup> endings, I only need to consider the stubborn endings of length 99 and check each one of them. (If this simplification is not obvious, stop and convince yourself it is true.)\n",
"- So, to find the stubborn endings of length 100, I don't need to generate and test all 10<sup>100</sup> endings, I only need to consider the stubborn endings of length 99, prepend each of the ten digits to each of these endings to get a list of 100-digit endings, and then check each one for stubborness. \n",
"\n",
"Using this simplification we can efficiently compute all stubborn-endings of a given length *d* as follows (caching greatly improves efficiency):"
"Using this optimization we can efficiently compute all stubborn-endings of a given length *d* as follows (caching greatly improves efficiency):"
]
},
{
@@ -232,17 +239,16 @@
"metadata": {},
"outputs": [],
"source": [
"from functools import lru_cache\n",
"from functools import cache\n",
"\n",
"@lru_cache(None)\n",
"def stubborn_endings(d: int) -> list:\n",
"@cache\n",
"def stubborn_endings(d: int) -> list[str]:\n",
" \"\"\"A list of all stubborn endings of length `d` digits.\"\"\"\n",
" if d == 0:\n",
" return [''] # The empty ending is the sole stubborn ending of length 0.\n",
" return [''] # The empty ending is the only stubborn ending of length 0.\n",
" else:\n",
" return [(s + e) for e in stubborn_endings(d - 1) \n",
" for s in digits \n",
" if stubborn(s + e)]"
" endings = [s + e for e in stubborn_endings(d - 1) for s in digits]\n",
" return list(filter(stubborn, endings))"
]
},
{
@@ -262,7 +268,16 @@
{
"data": {
"text/plain": [
"['000', '001', '625', '376']"
"{0: [''],\n",
" 1: ['0', '1', '5', '6'],\n",
" 2: ['00', '01', '25', '76'],\n",
" 3: ['000', '001', '625', '376'],\n",
" 4: ['0000', '0001', '0625', '9376'],\n",
" 5: ['00000', '00001', '90625', '09376'],\n",
" 6: ['000000', '000001', '890625', '109376'],\n",
" 7: ['0000000', '0000001', '2890625', '7109376'],\n",
" 8: ['00000000', '00000001', '12890625', '87109376'],\n",
" 9: ['000000000', '000000001', '212890625', '787109376']}"
]
},
"execution_count": 7,
@@ -271,19 +286,22 @@
}
],
"source": [
"stubborn_endings(3)"
"{d: stubborn_endings(d) for d in range(10)}"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "d1dc31eb-e5dd-405e-b16d-eceef5c9493c",
"id": "38c26d56-d916-481e-bc04-51af7a52b1e8",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"['0000', '0001', '0625', '9376']"
"['000000000000000000000000000000000000000000000000000000000000',\n",
" '000000000000000000000000000000000000000000000000000000000001',\n",
" '863811000557423423230896109004106619977392256259918212890625',\n",
" '136188999442576576769103890995893380022607743740081787109376']"
]
},
"execution_count": 8,
@@ -292,73 +310,31 @@
}
],
"source": [
"stubborn_endings(4)"
"stubborn_endings(60)"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "81730e6b-7218-4fca-9107-2a9539f62ea3",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"['00000', '00001', '90625', '09376']"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"stubborn_endings(5)"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "288506c8-6dc1-40d5-842a-eb65ed94ae98",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"['000000', '000001', '890625', '109376']"
]
},
"execution_count": 10,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"stubborn_endings(6)"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "73491ea5-a4c8-4221-8fc7-d6ce44d17bb4",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"['0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',\n",
" '0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001',\n",
" '3953007319108169802938509890062166509580863811000557423423230896109004106619977392256259918212890625',\n",
" '6046992680891830197061490109937833490419136188999442576576769103890995893380022607743740081787109376']"
"['000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',\n",
" '000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001',\n",
" '055462996814764263903953007319108169802938509890062166509580863811000557423423230896109004106619977392256259918212890625',\n",
" '944537003185235736096046992680891830197061490109937833490419136188999442576576769103890995893380022607743740081787109376']"
]
},
"execution_count": 11,
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"stubborn_endings(100)"
"stubborn_endings(120)"
]
},
{
@@ -366,26 +342,35 @@
"id": "37a45341-8dac-4720-be7d-388fa47eec1a",
"metadata": {},
"source": [
"## More questions!\n",
"\n",
"This leads to a few new questions.\n",
"\n",
"## Can each stubborn ending be extended exactly one way?\n",
"\n",
"We know that each stubborn ending of length *d* has to build on the endings of length *d - 1*, but is it always the case that for any length *d* there will be exactly four endings? Might there be a case where two digits work with one of the endings, or no digits?\n",
"We know that each stubborn ending of length *d* has to build on the endings of length *d* - 1, and so far, there has always been exactly one way (out of the ten possible digits) to extend each of the length *d* - 1 endings. Will that always be true? Might there be a case where two digits work with one of the endings, or no digits?\n",
"\n",
"I can show that there are always 4 endings up to length *d* = 2000, but I leave it to you to describe a proof that this will always be true."
"I can show that for all lengths *d* up to 2000, the result of `stubborn_endings(d)` is always four strings that end with '0', '1', '5', and '6' in that order:"
]
},
{
"cell_type": "code",
"execution_count": 12,
"execution_count": 18,
"id": "654d53af-e58f-4ed2-874b-c4573f3a9a7a",
"metadata": {},
"outputs": [],
"outputs": [
{
"data": {
"text/plain": [
"Counter({('0', '1', '5', '6'): 2000})"
]
},
"execution_count": 18,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"for d in range(1, 2000):\n",
" assert len(stubborn_endings(d)) == 4 "
"Counter(tuple(ending[-1] for ending in stubborn_endings(d))\n",
" for d in range(1, 2001))"
]
},
{
@@ -393,37 +378,47 @@
"id": "537556a3-d570-4a50-93bf-9632b40f9b47",
"metadata": {},
"source": [
"One cool implication: if it is true that a stubborn ending of any length can always be extended, that means there is an infinitely long integer whose square ends with itself!\n",
"I leave it up to the reader to find a proof ofr numbers beyond 2000.\n",
"\n",
"## What digits are used to extend each ending?\n",
"\n",
"There doesn't seem to be a pattern, and all digits seemingly get used roughly evenly. \n",
"\n",
"I don't have a theory of which digit comes next, but I can count how many times each digit appears in the 2000-digit endings that end in \"5\" and \"6\", and see that each of the ten digits appears about 200 times:"
"I don't have a theory of which digit is used to extend each stubborn ending, but I can count:"
]
},
{
"cell_type": "code",
"execution_count": 13,
"execution_count": 11,
"id": "4dbd7ace-8b41-4ea7-b71f-8f7e318243e1",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Counter({'0': 205,\n",
" '3': 173,\n",
"{'0': Counter({'0': 1999}),\n",
" '1': Counter({'0': 1999}),\n",
" '5': Counter({'8': 214,\n",
" '2': 208,\n",
" '6': 197,\n",
" '9': 198,\n",
" '5': 197,\n",
" '4': 206,\n",
" '8': 214,\n",
" '0': 205,\n",
" '7': 205,\n",
" '1': 197})"
" '9': 198,\n",
" '6': 197,\n",
" '1': 197,\n",
" '5': 196,\n",
" '3': 173}),\n",
" '6': Counter({'1': 214,\n",
" '7': 208,\n",
" '5': 206,\n",
" '9': 205,\n",
" '2': 205,\n",
" '0': 198,\n",
" '3': 197,\n",
" '8': 197,\n",
" '4': 196,\n",
" '6': 173})}"
]
},
"execution_count": 13,
"execution_count": 11,
"metadata": {},
"output_type": "execute_result"
}
@@ -431,47 +426,23 @@
"source": [
"from collections import Counter\n",
"\n",
"zero, one, five, six = stubborn_endings(2000)\n",
"\n",
"Counter(five)"
"{e[-1]: Counter(e[:-1]) for e in stubborn_endings(2000)}"
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "96bdab37-c5a3-4597-b4ca-fb560e4c3163",
"cell_type": "markdown",
"id": "35e84df9-0b76-4d8d-aa13-2c4e73a86643",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"Counter({'9': 205,\n",
" '6': 174,\n",
" '7': 208,\n",
" '3': 197,\n",
" '0': 198,\n",
" '4': 196,\n",
" '5': 206,\n",
" '1': 214,\n",
" '2': 205,\n",
" '8': 197})"
]
},
"execution_count": 14,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"Counter(six)"
"This is saying that the stubborn endings \"0\" and \"1\" are always extended by prepending a \"0\", but for \"5\" and \"6\", it seems to be pretty random; each of the ten digits appears roughly 200 times."
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"display_name": "Python [conda env:base] *",
"language": "python",
"name": "python3"
"name": "conda-base-py"
},
"language_info": {
"codemirror_mode": {
@@ -483,7 +454,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8.15"
"version": "3.13.9"
}
},
"nbformat": 4,

View File

@@ -233,7 +233,6 @@ This project contains ***pytudes***—Python programs, usually short, for perfec
To continue the musical analogy, some people think of programming like [Spotify](http://spotify.com): they want to know how to install the app, find a good playlist, and hit the "play" button; after that they don't want to think about it. There are plenty of other tutorials that will tell you how to do the equivalent of that for various programming tasks—this one won't help. But if you think of programming like playing the piano—a craft that can take [years](https://norvig.com/21-days.html) to perfect—then I hope this collection can help.
# Index of Jupyter (IPython) Notebooks
For each notebook you can hover on the title to see a description, or click the title to view on github, or click one of the letters in the left column to launch the notebook on
@@ -250,15 +249,27 @@ For each notebook you can hover on the title to see a description, or click the
{format_pythons()}
# Etudes for Programmers
I got the idea for the *"etudes"* part of the name from
this [1978 book](https://books.google.com/books/about/Etudes_for_programmers.html?id=u89WAAAAMAAJ)
by [Charles Wetherell](http://demin.ws/blog/english/2012/08/25/interview-with-charles-wetherell)
that was very influential to me when I was first learning to program. I still have my copy.
that was very influential to me when I was first learning to program. I still have my copy, but
it is now easier to find a [pdf](txt/Etudes.pdf) than a hard copy.
![](https://images-na.ssl-images-amazon.com/images/I/51ZnZH29dvL._SX394_BO1,204,203,200_.jpg)
"""
output = 'README.md'
print(f'Wrote {open(output, "w").write(body)} characters to {output}')
print('Checking...')
check()
# Reviews of pytudes
Here's what some people are saying about `pytudes`:
- "What I find interesting is how Peter builds bottom-up solutions using low-level utilities... Reading his code is educational." - [Jeremey Howard](https://en.wikipedia.org/wiki/Jeremy_Howard_(entrepreneur)), co-founder of fast.ai and chief scientist at Kaggle
- "Everything I see from Peter Norvig is just always so incredibly well written and coded." — [Jonathan](https://news.ycombinator.com/user?id=jypepin), [Hacker News](https://news.ycombinator.com/item?id=27379366)
- "Peter Norvig is my go to recommendation when someone is interested in becoming better at solving day to day problems ... I feel his skill of dividing a problem into small pieces and expressing them in code in a natural way is unparalleled." — [mikevin](https://news.ycombinator.com/user?id=mikevin), [Hacker News](https://news.ycombinator.com/item?id=27379366)
- "I've never seen Peter Norvig choose anything but the most elegant and perfect data model for the problem at hand." — [spoonjim](https://news.ycombinator.com/user?id=spoonjim), [Hacker News](https://news.ycombinator.com/item?id=27379366)
- "I just find Norvig's style of "functional Python" lovely in its own way (with noted disregard of Pep8 and other "best practices")" —[raverbashing](https://news.ycombinator.com/user?id=raverbashing), [Hacker News](https://news.ycombinator.com/item?id=25654955)
- "You should check out Norvig's design of computer programs [course on Udacity](https://imp.i115008.net/c/2331964/788805/11298?u=https://www.udacity.com/course/design-of-computer-programs--cs212) where he uses these kinds of puzzle programs to teach programming design concepts. It is a hard but really rewarding course. — [nafizh](https://news.ycombinator.com/user?id=nafizh), [HN ACademy](https://yahnd.com/academy/r/udacity.com/course/design-of-computer-programs--cs212/)
- "Often enough I would think of something [a possible improvement[, but if you worked it out in detail there was some less-obvious reason the code was the way it was... All the code is pretty short, and it's not really 'production code', but it's enough to be an education in craftsmanship at every level."
- What code samples should programmers read? "anything else implemented by Norvig, he's one of the best programmers that I've had the pleasure of reading code from." - [jacquesm](https://news.ycombinator.com/user?id=jacquesm) on [Hacker News](https://news.ycombinator.com/item?id=14487724)"""
with open('README.md', "w") as out:
print(f'Wrote {out.write(body)} characters to {out.name}')
#print('Checking...'); check()