diff --git a/ipynb/Stubborn.ipynb b/ipynb/Stubborn.ipynb index 0482c8a..704fafc 100644 --- a/ipynb/Stubborn.ipynb +++ b/ipynb/Stubborn.ipynb @@ -7,24 +7,24 @@ "source": [ "
Peter Norvig
Mar 2024
\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*k*⋅*s* + *e*)² = (10*k*⋅*s*)² + 2⋅(10*k*⋅*s* ⋅ *e*) + *e*² = 10*k* ⋅ (10*k*⋅*s*² + 2⋅*s*⋅*e*) + *e*²\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", - "This is 10*k* 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 10100 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 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 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,19 +310,22 @@ } ], "source": [ - "stubborn_endings(4)" + "stubborn_endings(60)" ] }, { "cell_type": "code", "execution_count": 9, - "id": "81730e6b-7218-4fca-9107-2a9539f62ea3", + "id": "73491ea5-a4c8-4221-8fc7-d6ce44d17bb4", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "['00000', '00001', '90625', '09376']" + "['000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',\n", + " '000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001',\n", + " '055462996814764263903953007319108169802938509890062166509580863811000557423423230896109004106619977392256259918212890625',\n", + " '944537003185235736096046992680891830197061490109937833490419136188999442576576769103890995893380022607743740081787109376']" ] }, "execution_count": 9, @@ -313,43 +334,88 @@ } ], "source": [ - "stubborn_endings(5)" + "stubborn_endings(120)" + ] + }, + { + "cell_type": "markdown", + "id": "37a45341-8dac-4720-be7d-388fa47eec1a", + "metadata": {}, + "source": [ + "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, 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 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": 10, - "id": "288506c8-6dc1-40d5-842a-eb65ed94ae98", + "execution_count": 18, + "id": "654d53af-e58f-4ed2-874b-c4573f3a9a7a", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "['000000', '000001', '890625', '109376']" + "Counter({('0', '1', '5', '6'): 2000})" ] }, - "execution_count": 10, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "stubborn_endings(6)" + "Counter(tuple(ending[-1] for ending in stubborn_endings(d))\n", + " for d in range(1, 2001))" + ] + }, + { + "cell_type": "markdown", + "id": "537556a3-d570-4a50-93bf-9632b40f9b47", + "metadata": {}, + "source": [ + "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", + "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": 11, - "id": "73491ea5-a4c8-4221-8fc7-d6ce44d17bb4", + "id": "4dbd7ace-8b41-4ea7-b71f-8f7e318243e1", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "['0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',\n", - " '0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001',\n", - " '3953007319108169802938509890062166509580863811000557423423230896109004106619977392256259918212890625',\n", - " '6046992680891830197061490109937833490419136188999442576576769103890995893380022607743740081787109376']" + "{'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": 11, @@ -357,121 +423,26 @@ "output_type": "execute_result" } ], - "source": [ - "stubborn_endings(100)" - ] - }, - { - "cell_type": "markdown", - "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", - "\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." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "654d53af-e58f-4ed2-874b-c4573f3a9a7a", - "metadata": {}, - "outputs": [], - "source": [ - "for d in range(1, 2000):\n", - " assert len(stubborn_endings(d)) == 4 " - ] - }, - { - "cell_type": "markdown", - "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", - "\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:" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "4dbd7ace-8b41-4ea7-b71f-8f7e318243e1", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Counter({'0': 205,\n", - " '3': 173,\n", - " '2': 208,\n", - " '6': 197,\n", - " '9': 198,\n", - " '5': 197,\n", - " '4': 206,\n", - " '8': 214,\n", - " '7': 205,\n", - " '1': 197})" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], "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,