{ "cells": [ { "cell_type": "markdown", "id": "fc5197e8-5f15-4809-8356-8eb70471637a", "metadata": {}, "source": [ "
Peter Norvig
Mar 2024
\n", "\n", "# Stubborn Number Endings (e.g. 5² = 25)\n", "\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\" 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", "- 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 look for a pattern:" ] }, { "cell_type": "code", "execution_count": 1, "id": "fd3c4f25-fb63-43e5-925c-e4b7b8f134ec", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{15: 225,\n", " 25: 625,\n", " 35: 1225,\n", " 45: 2025,\n", " 55: 3025,\n", " 65: 4225,\n", " 75: 5625,\n", " 85: 7225,\n", " 95: 9025}" ] }, "execution_count": 1, "metadata": {}, "output_type": "execute_result" } ], "source": [ "{i: i ** 2 for i in range(15, 100, 10)}" ] }, { "cell_type": "markdown", "id": "44d2cf09-b397-46d7-ab4f-e068e571895d", "metadata": {}, "source": [ "I see a pattern here: for example 95 squared is 9025, which you get by taking the \"9\", multiplying it by one more than itself to get 9⋅10 = 90, then squaring 5 to get 25, then putting the \"90\" next to the \"25\" to get \"9025\".\n", "\n", "Does the trick also work for numbers with more digits? Let's investigate:" ] }, { "cell_type": "code", "execution_count": 2, "id": "7681cb5f-bf5a-4317-b842-e0be21745d90", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{105: 11025,\n", " 115: 13225,\n", " 125: 15625,\n", " 135: 18225,\n", " 145: 21025,\n", " 155: 24025,\n", " 165: 27225,\n", " 175: 30625,\n", " 185: 34225,\n", " 195: 38025,\n", " 205: 42025,\n", " 215: 46225,\n", " 225: 50625,\n", " 235: 55225,\n", " 245: 60025}" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "{i: i ** 2 for i in range(105, 250, 10)}" ] }, { "cell_type": "markdown", "id": "89b1e783-0546-4305-989c-b3b83781cf92", "metadata": {}, "source": [ "Yes, it looks like the trick still works: 105 squared is 10⋅11 = \"110\", followed by \"25,\" to make \"11025\". And 245 squared is 24⋅25 = \"600\", followed by \"25\", to make \"60025\". \n", "\n", "\n", "## What should we call this property?\n", "\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 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:" ] }, { "cell_type": "code", "execution_count": 3, "id": "ddcdafa8-bfa6-4707-951c-6683701e06c2", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{0, 1, 5, 6}" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "{e for e in range(10) if str(e ** 2).endswith(str(e))}" ] }, { "cell_type": "markdown", "id": "f4cb87cf-2fa8-4d2e-bc24-94c378930792", "metadata": {}, "source": [ "These are the four digits whose square ends in the same digit.\n", "\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** 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 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*² = *e*² (mod 10)\n", "\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*k*⋅*s* + *e*)² = (10*k*⋅*s*)² + 2⋅(10*k*⋅*s* ⋅ *e*) + *e*² = 10*k* ⋅ (10*k*⋅*s*² + 2⋅*s*⋅*e*) + *e*² = *e*² (mod 10)\n", "\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)`:" ] }, { "cell_type": "code", "execution_count": 4, "id": "1419b122-257d-475b-90c6-e832c0aae8cf", "metadata": {}, "outputs": [], "source": [ "def stubborn(ending: str) -> bool:\n", " \"\"\"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)" ] }, { "cell_type": "markdown", "id": "9ec3ae19-96cb-4bea-ae6f-075b3ade7d3b", "metadata": {}, "source": [ "Now we can find all two-digit stubborn endings as follows:" ] }, { "cell_type": "code", "execution_count": 5, "id": "57e64e31-0674-4849-bf72-f6c6b437009a", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['00', '01', '25', '76']" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "digits = '0123456789'\n", "endings = [s + e for s in digits for e in digits]\n", "\n", "list(filter(stubborn, endings))" ] }, { "cell_type": "markdown", "id": "61a2b8e2-2bdf-458f-adaf-ffde56125862", "metadata": {}, "source": [ "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 10100 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 optimization we can efficiently compute all stubborn-endings of a given length *d* as follows (caching greatly improves efficiency):" ] }, { "cell_type": "code", "execution_count": 6, "id": "abeba9cb-1357-4e2b-a661-8616d0d3f768", "metadata": {}, "outputs": [], "source": [ "from functools import cache\n", "\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 only stubborn ending of length 0.\n", " else:\n", " endings = [s + e for e in stubborn_endings(d - 1) for s in digits]\n", " return list(filter(stubborn, endings))" ] }, { "cell_type": "markdown", "id": "05c524e7-6fa8-4fdd-875d-ef5cc0453560", "metadata": {}, "source": [ "For example:" ] }, { "cell_type": "code", "execution_count": 7, "id": "3943be37-ec8a-4ec0-b946-acb2f449dd72", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{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, "metadata": {}, "output_type": "execute_result" } ], "source": [ "{d: stubborn_endings(d) for d in range(10)}" ] }, { "cell_type": "code", "execution_count": 8, "id": "38c26d56-d916-481e-bc04-51af7a52b1e8", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "['0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',\n", " '0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001',\n", " '3953007319108169802938509890062166509580863811000557423423230896109004106619977392256259918212890625',\n", " '6046992680891830197061490109937833490419136188999442576576769103890995893380022607743740081787109376']" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "stubborn_endings(100)" ] }, { "cell_type": "markdown", "id": "37a45341-8dac-4720-be7d-388fa47eec1a", "metadata": {}, "source": [ "This leads to a few new questions.\n", "\n", "## Can each stubborn ending always be extended exactly one way?\n", "\n", "There are four stubborn endings of length 1: '0', '1', '5', and '6'. So far, every time we try, each of the four can be extended from length *d* to length *d*+1 in exactly one way. Will that always be the case? I leave it up to you to provide a proof, but I can show that it is true for every *d* up to 2000:" ] }, { "cell_type": "code", "execution_count": 9, "id": "654d53af-e58f-4ed2-874b-c4573f3a9a7a", "metadata": {}, "outputs": [], "source": [ "def last_digits(endings) -> list[str]: \n", " \"\"\"A list of the last digits of these endings.\"\"\"\n", " return [e[-1] for e in endings]\n", " \n", "for d in range(1, 2000):\n", " assert last_digits(stubborn_endings(d)) == ['0', '1', '5', '6']" ] }, { "cell_type": "markdown", "id": "537556a3-d570-4a50-93bf-9632b40f9b47", "metadata": {}, "source": [ "## What digits are used to extend each ending?\n", "\n", "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": 10, "id": "4dbd7ace-8b41-4ea7-b71f-8f7e318243e1", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'0': Counter({'0': 1999}),\n", " '1': Counter({'0': 1999}),\n", " '5': Counter({'8': 214,\n", " '2': 208,\n", " '4': 206,\n", " '0': 205,\n", " '7': 205,\n", " '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": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "from collections import Counter\n", "\n", "{end: Counter(start) for (*start, end) in stubborn_endings(2000)}" ] }, { "cell_type": "markdown", "id": "35e84df9-0b76-4d8d-aa13-2c4e73a86643", "metadata": {}, "source": [ "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 [conda env:base] *", "language": "python", "name": "conda-base-py" }, "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 }