From 083374aca9cb29a8903fbd6bb9d5e8f923990148 Mon Sep 17 00:00:00 2001 From: Peter Norvig Date: Wed, 8 May 2024 17:41:19 -0700 Subject: [PATCH] Add files via upload --- ipynb/Countdown.ipynb | 454 ++++++----- ipynb/Fred Buns.ipynb | 1752 ++++++++++++++++++++++------------------- 2 files changed, 1220 insertions(+), 986 deletions(-) diff --git a/ipynb/Countdown.ipynb b/ipynb/Countdown.ipynb index 92710f3..2466f5f 100644 --- a/ipynb/Countdown.ipynb +++ b/ipynb/Countdown.ipynb @@ -11,9 +11,9 @@ } }, "source": [ - "
Peter Norvig
Jan 2016
revised 2018, 2020, 2021, 2023
\n", + "
Peter Norvig
Jan 2016
last revised 2024
\n", "\n", - "# Number Expression Puzzles: Countdowns, Four 4s, Five 5s, ...\n", + "# Number Expression Puzzles: Countdowns, Four 4s, Five 5s, etc.\n", "\n", "In this notebook we solve a range of related puzzles that all involve making mathematical expressions by combining numbers and operations in various ways to make target numeric values. First some imports, and then we can look at the first problem." ] @@ -32,10 +32,10 @@ "from math import sqrt, factorial, floor, ceil\n", "from operator import add, sub, mul, neg, truediv as div\n", "from typing import List, Tuple, Dict, Union, Sequence, Collection, Set, Optional\n", - "import functools\n", + "from functools import lru_cache\n", "import re\n", "\n", - "cache = functools.lru_cache(None)" + "cache = lru_cache(None)" ] }, { @@ -55,7 +55,7 @@ "\n", "*Fill in the blanks so that this equation makes arithmetical sense:*\n", "\n", - "*10 ␣ 9 ␣ 8 ␣ 7 ␣ 6 ␣ 5 ␣ 4 ␣ 3 ␣ 2 ␣ 1 = 2016*\n", + " 10 ␣ 9 ␣ 8 ␣ 7 ␣ 6 ␣ 5 ␣ 4 ␣ 3 ␣ 2 ␣ 1 = 2016\n", "\n", "*You are allowed to use *only* the four basic arithmetical operations: +, -, ×, ÷. But brackets (parentheses) can be used wherever needed. So, for example, the solution could begin as either:*\n", "\n", @@ -93,9 +93,9 @@ }, "outputs": [], "source": [ - "Numbers = Tuple[int] # A sequence of integers, like (10, 9, 8)\n", + "Numbers = Tuple[int, ...] # A sequence of integers, like (10, 9, 8)\n", "Exp = str # An expression is represented as a string, like '(10+(9-8))'\n", - "ExpTable = Dict[float, Exp] # A table is a dict, like {11: '(10+(9-8))', ...}\n", + "ExpTable = Dict[float, Exp] # A table is a {value: exp} dict, like {11: '(10+(9-8))', ...}\n", " \n", "@cache\n", "def expressions(numbers: Numbers) -> ExpTable:\n", @@ -114,10 +114,10 @@ " table[L + R] = Lexp + '+' + Rexp\n", " return table\n", " \n", - "def splits(sequence) -> List[Tuple[Sequence, Sequence]]:\n", - " \"\"\"Split sequence into two non-empty parts, in all ways.\"\"\"\n", - " return [(sequence[:i], sequence[i:]) \n", - " for i in range(1, len(sequence))]" + "def splits(numbers) -> List[Tuple[Numbers, Numbers]]:\n", + " \"\"\"Split numbers into two non-empty subsequences, in all possible ways.\"\"\"\n", + " return [(numbers[:i], numbers[i:]) \n", + " for i in range(1, len(numbers))]" ] }, { @@ -164,8 +164,8 @@ "assert expressions((2, 1)) == {1: '(2-1)', 2: '(2*1)', 3: '(2+1)'}\n", "assert expressions((3, 2)) == {1.5: '(3/2)', 6: '(3*2)', 1: '(3-2)', 5: '(3+2)'}\n", "assert expressions((3, 2, 1)) == {0: '((3-2)-1)', 0.5: '((3/2)-1)', 1: '((3-2)*1)',\n", - " 1.5: '((3/2)*1)', 2: '((3-2)+1)', 2.5: '((3/2)+1)', 3: '(3*(2-1))',\n", - " 4: '((3+2)-1)', 5: '((3+2)*1)', 6: '((3+2)+1)', 7: '((3*2)+1)', 9: '(3*(2+1))'}\n", + " 1.5: '((3/2)*1)', 2: '((3-2)+1)', 2.5: '((3/2)+1)', 3: '(3*(2-1))', 4: '((3+2)-1)', \n", + " 5: '((3+2)*1)', 6: '((3+2)+1)', 7: '((3*2)+1)', 9: '(3*(2+1))'}\n", "assert expressions((3, 2, 1))[7] == '((3*2)+1)'\n", "\n", "assert splits((3, 2, 1)) == [((3,), (2, 1)), ((3, 2), (1,))]\n", @@ -208,8 +208,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 21.9 s, sys: 667 ms, total: 22.6 s\n", - "Wall time: 22.7 s\n" + "CPU times: user 19.4 s, sys: 511 ms, total: 19.9 s\n", + "Wall time: 19.9 s\n" ] }, { @@ -273,7 +273,8 @@ " 2026: '((((10+((((9*8)*7)*(6-5))*4))+3)-2)-1)',\n", " 2027: '((((10+((((9*8)*7)*(6-5))*4))+3)-2)*1)',\n", " 2028: '((((10+((((9*8)*7)*(6-5))*4))+3)-2)+1)',\n", - " 2029: '(((((((10*9)+8)*7)*6)-((5*4)*3))/2)+1)'}" + " 2029: '(((((((10*9)+8)*7)*6)-((5*4)*3))/2)+1)',\n", + " 2030: '(((((((10-(9/8))+(7*6))*5)*4)-3)*2)+1)'}" ] }, "execution_count": 5, @@ -282,7 +283,7 @@ } ], "source": [ - "{y: expressions(c10)[y] for y in range(2010, 2030)}" + "{y: expressions(c10)[y] for y in range(2010, 2031)}" ] }, { @@ -475,32 +476,11 @@ "id": "CyTarLLEKEA-", "outputId": "da5d0ef3-f4d5-4be5-99ee-192fe0efad6e" }, - "outputs": [ - { - "data": { - "text/plain": [ - "'Fraction(1)/(Fraction(5)-Fraction(2))'" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "def exact(exp) -> str: return re.sub(r\"([0-9]+)\", r\"Fraction(\\1)\", exp)\n", "\n", - "exact('1/(5-2)')" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": { - "id": "ij0zaBmYKEA-" - }, - "outputs": [], - "source": [ + "assert exact('1/(5-2)') == 'Fraction(1)/(Fraction(5)-Fraction(2))'\n", "assert eval(exact('1/(5-2)')) == Fraction(1, 3)" ] }, @@ -515,7 +495,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 11, "metadata": { "id": "V29kQB9DKEA-", "outputId": "71a1824a-763d-4a9c-8462-beb881773d08" @@ -527,7 +507,7 @@ "44499" ] }, - "execution_count": 12, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } @@ -535,7 +515,7 @@ "source": [ "sum(expression_counts(c10)[y] \n", " for y, exp in expressions(c10).items()\n", - " if abs(y - 2016) < 1e-3 and eval(exact(exp)) == 2016)" + " if abs(y - 2016) < 1e-10 and eval(exact(exp)) == 2016)" ] }, { @@ -549,7 +529,9 @@ } }, "source": [ - "I can claim that the answer is **44,499**, but I don't have complete confidence in that claim. It was easy to verify that `'(((((((((10*9)+8)*7)-6)-5)-4)*3)+2)+1)'` is a correct solution for `expressions(c10)[2016]` by doing simple arithmetic. But there is no simple test to verify that 44,499 is correct; I would want code reviews and tests, and hopefully an independent implementation. And of course, if you have a different definition of \"distinct solution,\" you will get a different answer." + "I believe the correct answer is **44,499**. \n", + "\n", + "However I don't have complete confidence in that claim. It was easy to verify that `'(((((((((10*9)+8)*7)-6)-5)-4)*3)+2)+1)'` is a correct solution for `expressions(c10)[2016]` by doing simple arithmetic. But there is no simple test to verify that 44,499 is correct; I would want code reviews and tests, and hopefully an independent implementation. And of course, if you have a different definition of \"distinct solution,\" you will get a different answer." ] }, { @@ -573,7 +555,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 12, "metadata": { "id": "EtbIiPJtKEA_", "outputId": "86fcbddb-4773-4132-e7f4-9dda388881c2" @@ -594,7 +576,7 @@ " 9: '(((4/4)+4)+4)'}" ] }, - "execution_count": 13, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -659,7 +641,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 13, "metadata": { "id": "YO8f1DE_KEA_" }, @@ -691,7 +673,7 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 14, "metadata": { "id": "JvaPXzvYKEA_" }, @@ -737,7 +719,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 15, "metadata": { "button": false, "id": "valCpAKPKEA_", @@ -801,11 +783,11 @@ " \"\"\"All the operations in OPERATIONS with given arity whose code symbol is one of `ops`.\"\"\"\n", " return [op for op in OPERATIONS[arity] if op.symbol in ops]\n", "\n", - "def splits(sequence, start=1) -> List[Tuple[Sequence, Sequence]]:\n", - " \"\"\"Split sequence into two parts, in all ways.\n", + "def splits(numbers, start=1) -> List[Tuple[Numbers, Numbers]]:\n", + " \"\"\"Split numbers into two subsequences, in all ways.\n", " If start=1, the two parts are non-empty. If start=0, allow empty left part.\"\"\"\n", - " return [(sequence[:i], sequence[i:]) \n", - " for i in range(start, len(sequence))]" + " return [(numbers[:i], numbers[i:]) \n", + " for i in range(start, len(numbers))]" ] }, { @@ -819,7 +801,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 16, "metadata": { "id": "AEynbSq9KEA_" }, @@ -842,7 +824,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 17, "metadata": { "id": "mJJ8AxFXKEA_" }, @@ -868,16 +850,16 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 18, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{1: '1', 2: '2', 0.5: '(1/2)', -1: '(1-2)', 3: '(1+2)'}" + "{1: '1', 2: '2', -1: '(1-2)', 0.5: '(1/2)', 3: '(1+2)'}" ] }, - "execution_count": 19, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } @@ -902,7 +884,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 19, "metadata": { "button": false, "id": "ndmqhDXnKEBA", @@ -953,7 +935,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 20, "metadata": { "id": "N43Lq2hKKEBA", "outputId": "51a24d77-2e45-453b-b329-0e9f24b4efed" @@ -963,9 +945,9 @@ "name": "stdout", "output_type": "stream", "text": [ - "Can make 0 to 72 with (4, 4, 4, 4), ops=\"+-*/^_√!.,\", permute=False. [721,878 table entries]\n", + "Can make 0 to 72 with (4, 4, 4, 4), ops=\"+-*/^_√!.,\", permute=False. [721,700 table entries]\n", "\n", - " 0: 44-44 15: 4+(44/4) 30: (4+(4+4))/.4 45: 44+(4/4) 60: 44+(4*4) \n", + " 0: 44-44 15: 4+(44/4) 30: (4*(4+4))-√4 45: 44+(4/4) 60: 44+(4*4) \n", " 1: 44/44 16: .4*(44-4) 31: 4!+((4+4!)/4) 46: 4-(√4-44) 61: (4/4)+(4!/.4) \n", " 2: (4+44)/4! 17: (4!+44)/4 32: 44-(4!/√4) 47: 4!+(4!-(4/4)) 62: (4*(4*4))-√4 \n", " 3: √((44/4)-√4) 18: (44/√4)-4 33: 4+(4!+(√4/.4)) 48: 44+√(4*4) 63: ((4^4)-4)/4 \n", @@ -1003,7 +985,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 21, "metadata": { "button": false, "id": "ZWCn9F1_KEBA", @@ -1020,7 +1002,7 @@ "'((4+4)!/(4!-4))'" ] }, - "execution_count": 22, + "execution_count": 21, "metadata": {}, "output_type": "execute_result" } @@ -1063,7 +1045,7 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 22, "metadata": { "id": "e807aDInKEBA", "outputId": "e672d88a-bc1d-456d-da6b-b720c878e939" @@ -1073,13 +1055,13 @@ "name": "stdout", "output_type": "stream", "text": [ - "Can make 0 to 30 with (2, 2, 2, 2), ops=\"+-*/^_√!.,\", permute=False. [112,539 table entries]\n", + "Can make 0 to 30 with (2, 2, 2, 2), ops=\"+-*/^_√!.,\", permute=False. [112,543 table entries]\n", "\n", - " 0: 22-22 6: √((22/2)-2)! 12: (2+22)/2 18: 22-(2^2) 24: 2+(√22^2) 30: (2+(2^2))/.2 \n", + " 0: 22-22 6: √((22/2)-2)! 12: (2+22)/2 18: 22-(2*2) 24: 2+(√22^2) 30: (2+(2*2))/.2 \n", " 1: 22/22 7: 2+(2/(2*.2)) 13: 2+(22/2) 19: (2+(2-.2))/.2 25: (2-2.2)^-2 \n", - " 2: (2^2)!-22 8: 2+(2+(2^2)) 14: (2^(2^2))-2 20: 22-(√2^2) 26: 2+(2+22) \n", + " 2: (2*2)!-22 8: 2+(2+(2*2)) 14: (2^(2*2))-2 20: 22-(√2^2) 26: 2+(2+22) \n", " 3: √((22/2)-2) 9: (22/2)-2 15: (2+(2/2))/.2 21: 22-(2/2) 27: 22+(√.2^-2) \n", - " 4: .2*(22-2) 10: 22/2.2 16: 2*(2*(2^2)) 22: √22*√22 28: 2+(2+(2^2)!) \n", + " 4: .2*(22-2) 10: 22/2.2 16: 2*(2*(2*2)) 22: √22*√22 28: 2+(2+(2*2)!) \n", " 5: 2+(2+(2/2)) 11: 22/(√2^2) 17: 22-(√.2^-2) 23: 22+(2/2) 29: 2+(2+(.2^-2))\n" ] } @@ -1099,7 +1081,7 @@ }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 23, "metadata": { "id": "Ll4xl6TOKEBA", "outputId": "850ea56d-da8b-4f1f-c684-ba75f1c60afb" @@ -1109,7 +1091,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Can make 0 to 61 with (9, 9, 9, 9), ops=\"+-*/^_√!.,\", permute=False. [791,279 table entries]\n", + "Can make 0 to 61 with (9, 9, 9, 9), ops=\"+-*/^_√!.,\", permute=False. [791,247 table entries]\n", "\n", " 0: 99-99 13: 9+(√9+(9/9)) 26: (9*√9)-(9/9) 39: √9!+(99/√9) 52: √9!*(9-(√9/9)) \n", " 1: 99/99 14: √9+(99/9) 27: 9*√(9.9-.9) 40: (9-√9)!/(9+9) 53: (9*√9!)-(9/9) \n", @@ -1142,7 +1124,7 @@ }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 24, "metadata": { "button": false, "id": "Mmed82RmKEBA", @@ -1157,14 +1139,14 @@ "name": "stdout", "output_type": "stream", "text": [ - "Can make 0 to 38 with (5, 5, 5, 5), ops=\"+-*/^_√!.,\", permute=False. [345,331 table entries]\n", + "Can make 0 to 38 with (5, 5, 5, 5), ops=\"+-*/^_√!.,\", permute=False. [345,303 table entries]\n", "\n", " 0: 55-55 7: 5+((5+5)/5) 14: (5!/5)-(5+5) 21: (5+5.5)/.5 28: .5+(5*5.5) 35: (5!+55)/5 \n", " 1: 55/55 8: 5.5+(5*.5) 15: (5*5)-(5+5) 22: 55/(5*.5) 29: 5+(5-(5/5))! 36: (.5*5!)-(5!/5)\n", " 2: 5!/(5+55) 9: 5+(5-(5/5)) 16: 5+(55/5) 23: 55-(.5^-5) 30: 55-(5*5) 37: 5+(.5^-√(5*5))\n", " 3: 5.5-(5*.5) 10: 55/5.5 17: 5+(5!/(5+5)) 24: √(5+(55/5))! 31: 55-(5!/5) 38: ((5!/5)-5)/.5 \n", " 4: √(5+(55/5)) 11: 5.5+5.5 18: ((5!-5)/5)-5 25: .5*(55-5) 32: (5.5-5)^-5 \n", - " 5: (.5*5!)-55 12: (5+55)/5 19: (5!-(5*5))/5 26: (5/5)+(5*5) 33: .5*(5!*.55) \n", + " 5: (.5*5!)-55 12: (5+55)/5 19: (5!-(5*5))/5 26: (5*5)+(5/5) 33: .5*(5!*.55) \n", " 6: (55/5)-5 13: (5!-55)/5 20: 5*(5-(5/5)) 27: (5*5.5)-.5 34: 5+(5+(5!/5)) \n" ] } @@ -1196,24 +1178,24 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 25, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "10: 2-(0-(1*8)) 20-(1+9) 20/(2-0) 20/(2/1) 20/√(2+2) √(20*(2+3)) 20*(2/4) \n", + "10: 2-(0-(1*8)) 20-(1+9) 20/(2-0) 20/(2*1) 20/√(2*2) √(20*(2+3)) 20*(2/4) \n", " 9: 2+(0-(1-8)) (2*0)+(1*9) (20/2)-0! (20/2)-1 (20-2)/2 2+(0!+(2*3)) (20-2)/√4 \n", " 8: (2*0)+(1*8) (2*0)-(1-9) 2-(0-(2+0!)!) 2-(0-(2+1)!) (20/2)-2 20-(2*3!) (20/2)-√4 \n", - " 7: (2*0)-(1-8) (20+1)/√9 (2*(0!+2))+0! (2*(0!+2))+1 2+(0!+(2+2)) (20/2)-3 2+√(0!+24) \n", - " 6: √(2*(0+18)) (2+(0/19)!)! (2+(0/20)!)! (2+(0/21)!)! √((20-2)*2) (-20+23)! (20/2)-4 \n", - " 5: 2-(0-√(1+8)) 20/(1+√9) 2-(0-(2+0!)) 2-(0-(2+1)) 20/(2+2) √(2-(0-23)) 20/(2+√4) \n", - " 4: √(-2-(0-18)) √(20-(1+√9)) 2-(0-(2-0)) 2-(0-(2/1)) √(20-(2+2)) 20/(2+3) -20+24 \n", - " 3: 2+(0/18)! 2+(0/19)! 2+(0/20)! 2+(0/21)! 2+(0/22)! -20+23 2+(0/24)! \n", - " 2: 20-18 2-(0/19) 2-(0/20) 2-(0/21) -20+22 2-(0/23) √(-20+24) \n", - " 1: 2-(0/18)! 20-19 20/20 -20+21 2-(0/22)! 2-(0/23)! 2-(0/24)! \n", - " 0: 2*(0/18) 2*(0/19) 20-20 2*(0/21) 2*(0/22) 2*(0/23) 2*(0/24) \n" + " 7: (2*0)-(1-8) (20+1)/√9 (2*(0!+2))+0! (2*(0!+2))+1 2+(0!+(2*2)) (20/2)-3 2+√(0!+24) \n", + " 6: √(2*(0+18)) (2+(0*19)!)! (2+(0*20)!)! (2+(0*21)!)! √((20-2)*2) (-20+23)! (20/2)-4 \n", + " 5: 2-(0-√(1+8)) 20/(1+√9) 2-(0-(2+0!)) 2-(0-(2+1)) 20/(2*2) √(2-(0-23)) 20/(2*√4) \n", + " 4: √(-2-(0-18)) √(20-(1+√9)) 2-(0-(2-0)) 2-(0-(2*1)) √(20-(2*2)) 20/(2+3) -20+24 \n", + " 3: 2+(0*18)! 2+(0*19)! 2+(0*20)! 2+(0*21)! 2+(0*22)! -20+23 2+(0*24)! \n", + " 2: 20-18 2-(0*19) 2-(0*20) 2-(0*21) -20+22 2-(0*23) √(-20+24) \n", + " 1: 2-(0*18)! 20-19 20/20 -20+21 2-(0*22)! 2-(0*23)! 2-(0*24)! \n", + " 0: 2*(0*18) 2*(0*19) 20-20 2*(0*21) 2*(0*22) 2*(0*23) 2*(0*24) \n" ] } ], @@ -1244,7 +1226,7 @@ }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 26, "metadata": {}, "outputs": [ { @@ -1253,7 +1235,7 @@ "'(((3^2)-3)*4)'" ] }, - "execution_count": 27, + "execution_count": 26, "metadata": {}, "output_type": "execute_result" } @@ -1275,7 +1257,7 @@ }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 27, "metadata": { "id": "dLZMMnH-KEBB", "outputId": "9e964cef-f10d-49fd-faf6-bc171de07344" @@ -1285,14 +1267,14 @@ "data": { "text/plain": [ "{'(((3^2)-3)*4)',\n", - " '(((4/2)^3)*3)',\n", - " '(3*((4/2)^3))',\n", + " '(((4-2)^3)*3)',\n", + " '(3*((4-2)^3))',\n", " '(3*(4^(3/2)))',\n", " '(3/((2/4)^3))',\n", " '(4*((3^2)-3))'}" ] }, - "execution_count": 28, + "execution_count": 27, "metadata": {}, "output_type": "execute_result" } @@ -1317,7 +1299,7 @@ }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 28, "metadata": { "id": "snhb7k8OKEBB", "outputId": "44f3cf49-aa56-4900-f510-3eedb4970307" @@ -1331,7 +1313,7 @@ " '((2^3)+(8+8))',\n", " '((8-(3+2))*8)',\n", " '((8^2)/(8/3))',\n", - " '(3*((8^2)/8))',\n", + " '(3*((8*2)-8))',\n", " '(3/(2/(8+8)))',\n", " '(3/(8/(8^2)))',\n", " '(8*(8-(3+2)))',\n", @@ -1339,7 +1321,7 @@ " '(8+(8+(2^3)))'}" ] }, - "execution_count": 29, + "execution_count": 28, "metadata": {}, "output_type": "execute_result" } @@ -1350,7 +1332,7 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 29, "metadata": { "id": "gia8RdaUKEBB", "outputId": "46852452-3ebd-4718-ad7a-52d4fc6de9bf" @@ -1368,7 +1350,7 @@ " '(10-(2*(3-10)))'}" ] }, - "execution_count": 30, + "execution_count": 29, "metadata": {}, "output_type": "execute_result" } @@ -1379,7 +1361,7 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 30, "metadata": { "id": "VhhMQ33sKEBB", "outputId": "66fe6857-f940-409b-aa2a-04fdb00e064e" @@ -1391,7 +1373,7 @@ "{'(8/(3-(8/3)))'}" ] }, - "execution_count": 31, + "execution_count": 30, "metadata": {}, "output_type": "execute_result" } @@ -1402,7 +1384,7 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 31, "metadata": { "id": "nbcNqMdFKEBB", "outputId": "e3d17354-e297-4ded-9193-5fe615902072" @@ -1417,15 +1399,15 @@ " '(2*(6*(6/3)))',\n", " '(2*(6/(3/6)))',\n", " '(2/(3/(6*6)))',\n", + " '(6*(2*(6/3)))',\n", " '(6*(2/(3/6)))',\n", - " '(6*(2^(6/3)))',\n", " '(6*(6*(2/3)))',\n", " '(6*(6/(3/2)))',\n", " '(6/(3/(2*6)))',\n", " '(6/(3/(6*2)))'}" ] }, - "execution_count": 32, + "execution_count": 31, "metadata": {}, "output_type": "execute_result" } @@ -1436,7 +1418,7 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": 32, "metadata": { "id": "L8SAsmSrKEBB", "outputId": "33874782-4dcd-460e-dfaf-140fe71bf502" @@ -1448,7 +1430,7 @@ "{'((5^2)-(0^0))'}" ] }, - "execution_count": 33, + "execution_count": 32, "metadata": {}, "output_type": "execute_result" } @@ -1470,7 +1452,7 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": 33, "metadata": { "id": "VDlW5eWVKEBB", "outputId": "d2d3e78c-485f-407a-8888-579ba423f1d2" @@ -1482,7 +1464,7 @@ "{'(10-(6/(13-11))!)!'}" ] }, - "execution_count": 34, + "execution_count": 33, "metadata": {}, "output_type": "execute_result" } @@ -1493,7 +1475,7 @@ }, { "cell_type": "code", - "execution_count": 35, + "execution_count": 34, "metadata": { "id": "5GYEEXhnKEBB", "outputId": "bdee3db6-f560-4b4f-ba2a-036b4d8ee1b3" @@ -1505,7 +1487,7 @@ "{'(8/((1^9)+1))!', '(8/(1+(1^9)))!'}" ] }, - "execution_count": 35, + "execution_count": 34, "metadata": {}, "output_type": "execute_result" } @@ -1516,7 +1498,7 @@ }, { "cell_type": "code", - "execution_count": 36, + "execution_count": 35, "metadata": { "id": "pqZLLXXCKEBB", "outputId": "97da7ef1-2033-45d0-f636-1ea7bc14d126" @@ -1528,7 +1510,7 @@ "{'(9!/(7!+(7!+7!)))'}" ] }, - "execution_count": 36, + "execution_count": 35, "metadata": {}, "output_type": "execute_result" } @@ -1551,7 +1533,7 @@ }, { "cell_type": "code", - "execution_count": 37, + "execution_count": 36, "metadata": {}, "outputs": [ { @@ -1563,79 +1545,79 @@ "0038: 0-(0-(3*8)) 1149: (1-4)*(1-9) 1458: (1+5)*(8-4) 2348: (2-(3-4))*8 3355: (5*5)-(3/3) 4477: (4-(4/7))*7 \n", "0046: 0-(0-(4*6)) 1155: 1*((5*5)-1) 1459: ((4-1)*5)+9 2349: 2/(3/(4*9)) 3356: 3+((3*5)+6) 4478: 4+((4*7)-8) \n", "0128: 0+((1+2)*8) 1156: 1*((5-1)*6) 1466: ((1+4)*6)-6 2355: 2-(3-(5*5)) 3357: 3*((3*5)-7) 4479: 4*(4-(7-9)) \n", - "0136: 0+((1+3)*6) 1157: (1+1)*(5+7) 1467: (1-(4-7))*6 2356: (2-(3-5))*6 3359: (3+3)*(9-5) 4488: 4+(4+(8+8)) \n", + "0136: 0+((1+3)*6) 1157: (1+1)*(5+7) 1467: (1-(4-7))*6 2356: (2*(3*5))-6 3359: (3+3)*(9-5) 4488: 4+(4+(8+8)) \n", "0137: 0+((1+7)*3) 1158: (5-(1+1))*8 1468: (1-(4-6))*8 2357: 2+((3*5)+7) 3366: 3*((6/3)+6) 4489: (4*9)-(4+8) \n", - "0138: 0+(1*(3*8)) 1166: (1+1)*(6+6) 1469: 6*(9-(1+4)) 2358: (2*(3+5))+8 3367: 3-((3-6)*7) 4555: 4*(5+(5/5)) \n", - "0139: 0-((1-9)*3) 1168: 6/((1+1)/8) 1477: (1+7)*(7-4) 2359: 2*(3*(9-5)) 3368: ((3*3)-6)*8 4556: 4*(5/(5/6)) \n", + "0138: 0+(1*(3*8)) 1166: (1+1)*(6+6) 1469: 6*(9-(1+4)) 2358: (2*(3+5))+8 3367: 3-((3-6)*7) 4555: 4-(5-(5*5)) \n", + "0139: 0-((1-9)*3) 1168: 6/((1+1)/8) 1477: (1+7)*(7-4) 2359: 2*(3*(9-5)) 3368: ((3*3)-6)*8 4556: 4/(5/(5*6)) \n", "0145: 0+((1+5)*4) 1169: ((1+1)*9)+6 1478: 1*((7-4)*8) 2366: 2/(3/(6*6)) 3369: (3*3)+(6+9) 4557: 4*(7-(5/5)) \n", "0146: 0+(1*(4*6)) 1188: ((1+1)*8)+8 1479: (1-9)*(4-7) 2367: ((2*7)-6)*3 3377: (3+(3/7))*7 4558: (4-(5/5))*8 \n", "0147: 0-((1-7)*4) 1224: (1+2)*(2*4) 1488: 1*((4*8)-8) 2368: ((2+8)*3)-6 3378: (3*3)+(7+8) 4559: 4-(5*(5-9)) \n", - "0148: 0-((1-4)*8) 1225: (1+5)*(2+2) 1489: 1+((4*8)-9) 2369: 2*(6-(3-9)) 3379: 3+(7/(3/9)) 4566: 4*(5+(6/6)) \n", + "0148: 0-((1-4)*8) 1225: (1+5)*(2*2) 1489: 1+((4*8)-9) 2369: 2*(6-(3-9)) 3379: 3+(7/(3/9)) 4566: 4*(5+(6/6)) \n", "0155: 0-(1-(5*5)) 1226: 1*(2*(2*6)) 1555: (5-(1/5))*5 2377: (2*7)+(3+7) 3388: 8/(3-(8/3)) 4567: 4*(5-(6-7)) \n", "0156: 0-((1-5)*6) 1227: 2*(2*(7-1)) 1556: ((1+5)*5)-6 2378: 2*(7-(3-8)) 3389: (3*(3+8))-9 4568: (4+(5-6))*8 \n", "0226: 0+(2*(2*6)) 1228: (2-(1-2))*8 1559: (1+5)*(9-5) 2379: 2*((3*7)-9) 3399: 3+(3+(9+9)) 4569: 4+(5+(6+9)) \n", "0234: 0+(2*(3*4)) 1229: (1+(2+9))*2 1566: 1*((5*6)-6) 2388: ((2*8)-8)*3 3444: ((3+4)*4)-4 4577: 4*(5+(7/7)) \n", "0236: 0+((2+6)*3) 1233: (1+3)*(2*3) 1567: 1+((5*6)-7) 2389: 8/(2/(9-3)) 3445: 3+((4*4)+5) 4578: 4*(5-(7-8)) \n", - "0238: (0/2)+(3*8) 1234: 1*(2*(3*4)) 1568: (1-(5-8))*6 2399: (2*3)+(9+9) 3446: (3+(4/4))*6 4579: (4*7)+(5-9) \n", + "0238: (0*2)+(3*8) 1234: 1*(2*(3*4)) 1568: (1-(5-8))*6 2399: (2*3)+(9+9) 3446: (3+(4/4))*6 4579: (4*7)+(5-9) \n", "0239: 0+(2*(3+9)) 1235: (1+2)*(3+5) 1569: 1*(6*(9-5)) 2444: 2*(4+(4+4)) 3447: 3*((4/4)+7) 4588: 4*(5+(8/8)) \n", - "0244: 0+((2+4)*4) 1236: 1*((2+6)*3) 1578: (1-(5-7))*8 2445: ((2+5)*4)-4 3448: 3*(4/(4/8)) 4589: 4*(5-(8-9)) \n", - "0246: (0/2)+(4*6) 1237: 1+(2+(3*7)) 1579: (1-7)*(5-9) 2446: 2+((4*4)+6) 3449: 3*(9-(4/4)) 4599: 4*(5+(9/9)) \n", - "0248: 0+(2*(4+8)) 1238: (1+3)*(8-2) 1588: 1*((8-5)*8) 2447: 2*(4*(7-4)) 3455: 3-(4-(5*5)) 4666: 4*(6/(6/6)) \n", - "0257: 0+(2*(5+7)) 1239: 1*(2*(3+9)) 1589: (1-9)*(5-8) 2448: (2+(4/4))*8 3456: (3-(4-5))*6 4667: 4*(6/(7-6)) \n", + "0244: 0+((2+4)*4) 1236: 1*((2+6)*3) 1578: (1-(5-7))*8 2445: ((2*5)-4)*4 3448: 3/(4/(4*8)) 4589: 4*(5-(8-9)) \n", + "0246: (0*2)+(4*6) 1237: 1+(2+(3*7)) 1579: (1-7)*(5-9) 2446: 2+((4*4)+6) 3449: 3*(9-(4/4)) 4599: 4*(5+(9/9)) \n", + "0248: 0+(2*(4+8)) 1238: (1+3)*(8-2) 1588: 1*((8-5)*8) 2447: 2*(4*(7-4)) 3455: 3-(4-(5*5)) 4666: 4/(6/(6*6)) \n", + "0257: 0+(2*(5+7)) 1239: 1*(2*(3+9)) 1589: (1-9)*(5-8) 2448: (2*(4*4))-8 3456: (3-(4-5))*6 4667: 4*(6*(7-6)) \n", "0258: 0-((2-5)*8) 1244: 1*((2+4)*4) 1599: 1+(5+(9+9)) 2449: (4*(9-2))-4 3457: (3*4)+(5+7) 4668: 4+(6+(6+8)) \n", - "0266: 0+(2*(6+6)) 1245: (2-(1-5))*4 1666: ((6-1)*6)-6 2455: (2*(5+5))+4 3458: 3/((5-4)/8) 4669: (4*9)-(6+6) \n", - "0268: 0+(6/(2/8)) 1246: (2-1)*(4*6) 1668: 6/(1-(6/8)) 2456: (2*(4+5))+6 3459: 3*(4-(5-9)) 4677: 4*(6/(7/7)) \n", + "0266: 0+(2*(6+6)) 1245: (2-(1-5))*4 1666: ((6-1)*6)-6 2455: (2*(5+5))+4 3458: 3*((5-4)*8) 4669: (4*9)-(6+6) \n", + "0268: 0+(6/(2/8)) 1246: (2-1)*(4*6) 1668: 6/(1-(6/8)) 2456: (2*(4+5))+6 3459: 3*(4-(5-9)) 4677: 4*(6-(7-7)) \n", "0269: 0+((2*9)+6) 1247: (1-(2-7))*4 1669: (1-(6-9))*6 2457: 4/(2/(5+7)) 3466: (3*4)+(6+6) 4678: (4+(6-7))*8 \n", "0288: 0+((2*8)+8) 1248: 1*(2*(4+8)) 1679: (1+7)*(9-6) 2458: 2*((4*5)-8) 3468: 3*(4*(8-6)) 4679: 6/(4/(7+9)) \n", - "0334: 0+((3+3)*4) 1249: ((1+9)*2)+4 1688: (1-(6-8))*8 2459: (2+4)*(9-5) 3469: (3-(6-9))*4 4688: 4*(6/(8/8)) \n", - "0335: 0+(3*(3+5)) 1255: 1-(2-(5*5)) 1689: 1+(6+(8+9)) 2466: (2-(4-6))*6 3477: 3-((4-7)*7) 4689: 4*(6/(9-8)) \n", - "0337: 0+(3+(3*7)) 1256: (1-(2-5))*6 1699: 1*(6+(9+9)) 2467: 2+((4*7)-6) 3478: (4*(7-3))+8 4699: 4*(6/(9/9)) \n", - "0338: (0/3)+(3*8) 1257: 1*(2*(5+7)) 1779: 1+(7+(7+9)) 2468: 2/(4/(6*8)) 3479: (3*(4+7))-9 4777: 4*(7-(7/7)) \n", + "0334: 0+((3+3)*4) 1249: ((1+9)*2)+4 1688: (1-(6-8))*8 2459: (2+4)*(9-5) 3469: (3-(6-9))*4 4688: 4*(6-(8-8)) \n", + "0335: 0+(3*(3+5)) 1255: 1-(2-(5*5)) 1689: 1+(6+(8+9)) 2466: (2-(4-6))*6 3477: 3+((4*7)-7) 4689: 4*(6*(9-8)) \n", + "0337: 0+(3+(3*7)) 1256: (1-(2-5))*6 1699: 1*(6+(9+9)) 2467: 2+((4*7)-6) 3478: (4*(7-3))+8 4699: 4*(6-(9-9)) \n", + "0338: (0*3)+(3*8) 1257: 1*(2*(5+7)) 1779: 1+(7+(7+9)) 2468: 2/(4/(6*8)) 3479: (3*(4+7))-9 4777: 4*(7-(7/7)) \n", "0339: 0-(3-(3*9)) 1258: 1*((5-2)*8) 1788: 1+(7+(8+8)) 2469: (2+(4/6))*9 3489: 3+(4+(8+9)) 4778: 4*(7+(7-8)) \n", "0344: 0+(3*(4+4)) 1259: ((1+2)*5)+9 1789: 1*(7+(8+9)) 2477: (2*(7+7))-4 3499: (3*(9-4))+9 4788: 4*(7-(8/8)) \n", - "0346: (0/3)+(4*6) 1266: 1*(2*(6+6)) 1799: 7-(1-(9+9)) 2478: ((2*7)-8)*4 3556: (3+(5/5))*6 4789: 4*(7+(8-9)) \n", - "0348: (0/4)+(3*8) 1267: (1-7)*(2-6) 1888: 1*(8+(8+8)) 2479: (2*4)+(7+9) 3557: 3*((5/5)+7) 4799: 4*(7-(9/9)) \n", - "0349: 0-((3-9)*4) 1268: 1/(2/(6*8)) 1889: 8-(1-(8+9)) 2488: (2*4)+(8+8) 3558: 3*(5/(5/8)) 4888: (4-(8/8))*8 \n", - "0358: (0/5)+(3*8) 1269: 1*((2*9)+6) 2223: 2*(2*(2*3)) 2489: 8*(9-(2+4)) 3559: 3*(9-(5/5)) 4889: (4+(8-9))*8 \n", + "0346: (0*3)+(4*6) 1266: 1*(2*(6+6)) 1799: 7-(1-(9+9)) 2478: ((2*7)-8)*4 3556: (3+(5/5))*6 4789: 4*(7+(8-9)) \n", + "0348: (0*4)+(3*8) 1267: (1-7)*(2-6) 1888: 1*(8+(8+8)) 2479: (2*4)+(7+9) 3557: 3*((5/5)+7) 4799: 4*(7-(9/9)) \n", + "0349: 0-((3-9)*4) 1268: 1/(2/(6*8)) 1889: 8-(1-(8+9)) 2488: (2*4)+(8+8) 3558: 3/(5/(5*8)) 4888: (4-(8/8))*8 \n", + "0358: (0*5)+(3*8) 1269: 1*((2*9)+6) 2223: 2*(2*(2*3)) 2489: 8*(9-(2+4)) 3559: 3*(9-(5/5)) 4889: (4+(8-9))*8 \n", "0359: 0+((3*5)+9) 1277: ((7*7)-1)/2 2224: 2*(2*(2+4)) 2499: 2+(4+(9+9)) 3566: (3-(5-6))*6 4899: (4-(9/9))*8 \n", "0366: 0+((3*6)+6) 1278: 1+((2*8)+7) 2225: 2*(2+(2*5)) 2557: (2*7)+(5+5) 3567: 3*(6-(5-7)) 5555: (5*5)-(5/5) \n", - "0367: 0-((3-7)*6) 1279: 1+((2*7)+9) 2227: 2*((2*7)-2) 2558: (2+(5/5))*8 3568: 3/((6-5)/8) 5556: 5+((5*5)-6) \n", + "0367: 0-((3-7)*6) 1279: 1+((2*7)+9) 2227: 2*((2*7)-2) 2558: (2+(5/5))*8 3568: 3*((6-5)*8) 5556: 5+((5*5)-6) \n", "0368: 0-((3-6)*8) 1288: 1*((2*8)+8) 2228: 2*(2+(2+8)) 2559: (2*5)+(5+9) 3569: 3*(5-(6-9)) 5559: 5+(5+(5+9)) \n", - "0378: (0/7)+(3*8) 1289: (2*8)-(1-9) 2229: 2+(2*(2+9)) 2566: ((2*5)-6)*6 3578: 3-((5-8)*7) 5566: (5*5)-(6/6) \n", - "0388: (0/8)+(3*8) 1333: (1+3)*(3+3) 2233: 2*(2*(3+3)) 2567: (2-(5-7))*6 3579: 3+(5+(7+9)) 5567: (5*5)+(6-7) \n", + "0378: (0*7)+(3*8) 1289: (2*8)-(1-9) 2229: 2+(2*(2+9)) 2566: ((2*5)-6)*6 3578: 3-((5-8)*7) 5566: (5*5)-(6/6) \n", + "0388: (0*8)+(3*8) 1333: (1+3)*(3+3) 2233: 2*(2*(3+3)) 2567: (2-(5-7))*6 3579: 3+(5+(7+9)) 5567: (5*5)+(6-7) \n", "0389: 0+(8/(3/9)) 1334: 1*((3+3)*4) 2234: (2+(2+4))*3 2568: 2+((5*6)-8) 3588: 3+(5+(8+8)) 5568: 5+(5+(6+8)) \n", "0445: 0+(4+(4*5)) 1335: 1*(3*(3+5)) 2235: ((2*5)-2)*3 2569: (5/(2/6))+9 3589: (3*9)+(5-8) 5577: 5+(5+(7+7)) \n", - "0446: (0/4)+(4*6) 1336: ((1+6)*3)+3 2236: 2*((2*3)+6) 2577: (2*5)+(7+7) 3599: (3-9)*(5-9) 5578: (5*5)+(7-8) \n", + "0446: (0*4)+(4*6) 1336: ((1+6)*3)+3 2236: 2*((2*3)+6) 2577: (2*5)+(7+7) 3599: (3-9)*(5-9) 5578: (5*5)+(7-8) \n", "0447: 0-(4-(4*7)) 1337: 1*(3+(3*7)) 2237: 2*(2+(3+7)) 2578: ((2*5)-7)*8 3666: (3+(6/6))*6 5588: (5*5)-(8/8) \n", - "0448: 0+((4*4)+8) 1338: ((1+8)*3)-3 2238: 2+(2*(3+8)) 2579: (5*7)-(2+9) 3667: 3*((6/6)+7) 5589: (5*5)+(8-9) \n", - "0456: (0/5)+(4*6) 1339: 1*((3*9)-3) 2239: (2+(2/3))*9 2588: (5*8)-(2*8) 3668: 3*(6/(6/8)) 5599: (5*5)-(9/9) \n", - "0466: (0/6)+(4*6) 1344: 1*(3*(4+4)) 2244: 2*((2*4)+4) 2589: 2+(5+(8+9)) 3669: 3+(6+(6+9)) 5666: (5-(6/6))*6 \n", - "0467: (0/7)+(4*6) 1345: 1+(3+(4*5)) 2245: 2+(2+(4*5)) 2666: (2*6)+(6+6) 3677: 3*(7-(6-7)) 5667: 5+(6+(6+7)) \n", - "0468: 0-((4-8)*6) 1346: 6/(1-(3/4)) 2246: 2*(2+(4+6)) 2667: (6+(6*7))/2 3678: 3+(6+(7+8)) 5668: 6-((5-8)*6) \n", - "0469: (0/9)+(4*6) 1347: ((1+3)*7)-4 2247: 2+(2*(4+7)) 2668: (2+(6/6))*8 3679: 3*(6-(7-9)) 5669: (6*9)-(5*6) \n", + "0448: 0+((4*4)+8) 1338: ((1+8)*3)-3 2238: 2-(2-(3*8)) 2579: (5*7)-(2+9) 3667: 3*((6/6)+7) 5589: (5*5)+(8-9) \n", + "0456: (0*5)+(4*6) 1339: 1*((3*9)-3) 2239: (2+(2/3))*9 2588: (5*8)-(2*8) 3668: 3/(6/(6*8)) 5599: (5*5)-(9/9) \n", + "0466: (0*6)+(4*6) 1344: 1*(3*(4+4)) 2244: 2*((2*4)+4) 2589: 2+(5+(8+9)) 3669: 3+(6+(6+9)) 5666: (5-(6/6))*6 \n", + "0467: (0*7)+(4*6) 1345: 1+(3+(4*5)) 2245: 2+(2+(4*5)) 2666: (2*6)+(6+6) 3677: 3*(7-(6-7)) 5667: 5+(6+(6+7)) \n", + "0468: 0-((4-8)*6) 1346: 6/(1-(3/4)) 2246: 2-(2-(4*6)) 2667: (6+(6*7))/2 3678: 3+(6+(7+8)) 5668: 6-((5-8)*6) \n", + "0469: (0*9)+(4*6) 1347: ((1+3)*7)-4 2247: 2+(2*(4+7)) 2668: (2+(6/6))*8 3679: 3*(6-(7-9)) 5669: (6*9)-(5*6) \n", "0478: 0-((4-7)*8) 1348: ((1+3)*4)+8 2248: (2*(2*4))+8 2669: (2+6)*(9-6) 3688: (3+(8/8))*6 5677: (5-(7/7))*6 \n", "0488: 0+((4*8)-8) 1349: 1+((3*9)-4) 2249: 2+((2*9)+4) 2678: (2-(6-7))*8 3689: (3-(8-9))*6 5678: (5+7)*(8-6) \n", "0566: 0+((5*6)-6) 1356: 1+((3*6)+5) 2255: 2*(2+(5+5)) 2679: 2+(6+(7+9)) 3699: (3*9)+(6-9) 5679: 6-((5-7)*9) \n", "0569: 0-((5-9)*6) 1357: (1+5)*(7-3) 2256: 2+(2*(5+6)) 2688: 2+(6+(8+8)) 3777: 3*(7+(7/7)) 5688: (5+(6-8))*8 \n", - "0588: 0-((5-8)*8) 1358: 1+((3*5)+8) 2257: (2*5)+(2*7) 2689: 2/(6/(8*9)) 3778: 3*(7/(7/8)) 5689: (5+(8-9))*6 \n", + "0588: 0-((5-8)*8) 1358: 1+((3*5)+8) 2257: (2*5)+(2*7) 2689: 2/(6/(8*9)) 3778: 3/(7/(7*8)) 5689: (5+(8-9))*6 \n", "0689: 0-((6-9)*8) 1359: 1*((3*5)+9) 2258: (2*(5+8))-2 2699: (2+(6/9))*9 3779: 3*(9-(7/7)) 5699: (5*(9-6))+9 \n", "0699: 0+(6+(9+9)) 1366: 1*((3*6)+6) 2259: 2*(5-(2-9)) 2778: 2+(7+(7+8)) 3788: 3*(7+(8/8)) 5779: (5+7)*(9-7) \n", - "0789: 0+(7+(8+9)) 1367: 1*(6*(7-3)) 2266: (2+6)/(2/6) 2788: (2-(7-8))*8 3789: 3*(7-(8-9)) 5788: ((7-5)*8)+8 \n", + "0789: 0+(7+(8+9)) 1367: 1*(6*(7-3)) 2266: (2*6)+(2*6) 2788: (2-(7-8))*8 3789: 3*(7-(8-9)) 5788: ((7-5)*8)+8 \n", "0888: 0+(8+(8+8)) 1368: 1*((6-3)*8) 2267: (2*(2+7))+6 2789: (2*(7+9))-8 3799: 3*(7+(9/9)) 5789: (5+(7-9))*8 \n", - "1118: (1+(1+1))*8 1369: (1-9)*(3-6) 2268: (2*(2+6))+8 2888: (2+(8/8))*8 3888: 3*(8/(8/8)) 5888: (5*8)-(8+8) \n", - "1126: (1+1)*(2*6) 1377: (1-7)*(3-7) 2269: 2*((2*9)-6) 2889: (2-(8-9))*8 3889: 3*(8/(9-8)) 5889: 8/((8-5)/9) \n", - "1127: (1+2)*(1+7) 1378: 3/(1-(7/8)) 2277: 2*(7-(2-7)) 2899: (2+(9/9))*8 3899: 3*(8/(9/9)) 6666: 6+(6+(6+6)) \n", + "1118: (1+(1+1))*8 1369: (1-9)*(3-6) 2268: (2*(2+6))+8 2888: (2+(8/8))*8 3888: 3/(8/(8*8)) 5888: (5*8)-(8+8) \n", + "1126: (1+1)*(2*6) 1377: (1-7)*(3-7) 2269: 2*((2*9)-6) 2889: (2-(8-9))*8 3889: 3*(8*(9-8)) 5889: 8/((8-5)/9) \n", + "1127: (1+2)*(1+7) 1378: 3/(1-(7/8)) 2277: 2*(7-(2-7)) 2899: (2+(9/9))*8 3899: 3*(8-(9-9)) 6666: 6+(6+(6+6)) \n", "1128: 1*((1+2)*8) 1379: (1+7)/(3/9) 2278: 2+((2*7)+8) 3333: (3*(3*3))-3 3999: 3*(9-(9/9)) 6668: 6*(6+(6-8)) \n", - "1129: (1+2)*(9-1) 1388: ((1+3)*8)-8 2288: (2*(2*8))-8 3334: 3+(3*(3+4)) 4444: 4+(4+(4*4)) 6669: 6*(6*(6/9)) \n", + "1129: (1+2)*(9-1) 1388: ((1+3)*8)-8 2288: (2*(2*8))-8 3334: 3+(3*(3+4)) 4444: 4+(4+(4*4)) 6669: 6-(6*(6-9)) \n", "1134: (1+1)*(3*4) 1389: 1/(3/(8*9)) 2289: (2*9)-(2-8) 3335: (3*3)+(3*5) 4445: 4*((4/4)+5) 6679: 6*(6+(7-9)) \n", - "1135: (1+3)*(1+5) 1399: (9-1)/(3/9) 2333: 2*(3+(3*3)) 3336: 3+(3+(3*6)) 4446: 4*(4/(4/6)) 6688: 6/((8-6)/8) \n", + "1135: (1+3)*(1+5) 1399: (9-1)/(3/9) 2333: 2*(3+(3*3)) 3336: 3+(3+(3*6)) 4446: 4-(4-(4*6)) 6688: 6/((8-6)/8) \n", "1136: 1*((1+3)*6) 1444: ((1+4)*4)+4 2335: 2*((3*5)-3) 3337: 3*((3/3)+7) 4447: (4+4)*(7-4) 6689: 6-((6-8)*9) \n", - "1137: 1*((1+7)*3) 1445: 1*(4+(4*5)) 2336: 2*(3+(3+6)) 3338: 3*(3/(3/8)) 4448: (4-(4/4))*8 6789: 6*(8/(9-7)) \n", - "1138: 1/(1/(3*8)) 1446: ((1+6)*4)-4 2337: 2*(3*(7-3)) 3339: 3*(9-(3/3)) 4449: 4-(4*(4-9)) 6799: 6-((7-9)*9) \n", + "1137: 1*((1+7)*3) 1445: 1*(4+(4*5)) 2336: 2*(3+(3+6)) 3338: 3-(3-(3*8)) 4448: (4-(4/4))*8 6789: 6*(8/(9-7)) \n", + "1138: 1*(1*(3*8)) 1446: ((1+6)*4)-4 2337: 2*(3*(7-3)) 3339: 3*(9-(3/3)) 4449: 4-(4*(4-9)) 6799: 6-((7-9)*9) \n", "1139: (1+1)*(3+9) 1447: 1+((4*4)+7) 2338: (2+(3/3))*8 3344: 3*((3*4)-4) 4455: (4+(4/5))*5 6888: 8-((6-8)*8) \n", - "1144: (1+(1+4))*4 1448: 1*((4*4)+8) 2339: ((2+3)*3)+9 3345: ((3/3)+5)*4 4456: 4/((5-4)/6) 6889: (8+8)/(6/9) \n", - "1145: 1*((1+5)*4) 1449: (1-(4-9))*4 2344: ((2+3)*4)+4 3346: 3/(3/(4*6)) 4457: 4*(4-(5-7)) 6899: 8/(6/(9+9)) \n", - "1146: 1/(1/(4*6)) 1455: 4-((1-5)*5) 2345: 2*(3+(4+5)) 3347: 3*(4-(3-7)) 4458: (4+(4-5))*8 7889: 8-((7-9)*8) \n", + "1144: (1+(1+4))*4 1448: 1*((4*4)+8) 2339: ((2+3)*3)+9 3345: ((3/3)+5)*4 4456: 4*((5-4)*6) 6889: (8+8)/(6/9) \n", + "1145: 1*((1+5)*4) 1449: (1-(4-9))*4 2344: ((2+3)*4)+4 3346: 3-(3-(4*6)) 4457: 4*(4-(5-7)) 6899: 8/(6/(9+9)) \n", + "1146: 1*(1*(4*6)) 1455: 4-((1-5)*5) 2345: 2*(3+(4+5)) 3347: 3*(4-(3-7)) 4458: (4+(4-5))*8 7889: 8-((7-9)*8) \n", "1147: 1*(4*(7-1)) 1456: 4/(1-(5/6)) 2346: 2+((3*6)+4) 3348: (3+3)*(8-4) 4468: 4*(4-(6-8)) \n", "1148: (1+1)*(4+8) 1457: 1+((4*7)-5) 2347: (2-(3-7))*4 3349: 3*(3-(4-9)) 4469: 4*(4/(6/9)) \n" ] @@ -1671,7 +1653,7 @@ }, { "cell_type": "code", - "execution_count": 38, + "execution_count": 37, "metadata": { "id": "CoKnbYBxKEBB", "outputId": "dd72b838-7b22-4080-9b29-739585881ce7" @@ -1681,22 +1663,22 @@ "name": "stdout", "output_type": "stream", "text": [ - "6: (0!+(0!+0!))!\n", - "6: (1+(1+1))!\n", - "6: (2+(2+2))\n", - "6: ((3*3)-3)\n", - "6: (4+(4/√4))\n", - "6: (5+(5/5))\n", - "6: (6/(6/6))\n", - "6: (7-(7/7))\n", - "6: (8-√√(8+8))\n", - "6: (9-(9/√9))\n" + "6 = (0!+(0!+0!))!\n", + "6 = (1+(1+1))!\n", + "6 = (2+(2*2))\n", + "6 = ((3*3)-3)\n", + "6 = (4+(4-√4))\n", + "6 = (5+(5/5))\n", + "6 = (6-(6-6))\n", + "6 = (7-(7/7))\n", + "6 = (8-√√(8+8))\n", + "6 = (9-(9/√9))\n" ] } ], "source": [ "for n in range(10):\n", - " print(f\"6: {expressions((n, n, n), '+-*/√!').get(6)}\")" + " print(f\"6 = {expressions((n, n, n), '+-*/√!').get(6)}\")" ] }, { @@ -1715,7 +1697,7 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": 38, "metadata": { "id": "jbJsFkNZKEBC", "outputId": "fea9ae24-2e75-4a11-8098-162790f91625" @@ -1725,7 +1707,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Can make 0 to 1644 with (4, 4, 4, 4), ops=\"+-*/^_√!.,⌊⌈\", permute=False. [1,205,301 table entries]\n", + "Can make 0 to 1644 with (4, 4, 4, 4), ops=\"+-*/^_√!.,⌊⌈\", permute=False. [1,205,099 table entries]\n", "\n", " 0: 44-44 60: 44+(4*4) 120: ⌈4.444⌉! 180: 4+(4*44) 240: √4*⌈4.44⌉! \n", " 1: 44/44 61: ⌈(.44^-⌈4.4⌉)⌉ 121: (44/4)^√4 181: ⌊(4*(√√4+44))⌋ 241: (.4+(4*4!))/.4 \n", @@ -1744,7 +1726,7 @@ " 14: ⌈(√44+√44)⌉ 74: 4+⌈(44/√.4)⌉ 134: 4!+(44/.4) 194: ⌈(44*4.4)⌉ 254: √4-(4-(4^4)) \n", " 15: 4+(44/4) 75: ⌊((4+44)/√.4)⌋ 135: ⌊(4!/.44)⌋/.4 195: ⌊(√4!*(44-4))⌋ 255: (4^4)-(4/4) \n", " 16: 4*⌊4.44⌋ 76: ⌈4.4⌉!-44 136: √4*(4!+44) 196: 4+(4!*(4+4)) 256: 4^⌊4.44⌋ \n", - " 17: ⌊(4*4.44)⌋ 77: ⌈(44*(4^.4))⌉ 137: ⌈(4!/(.4*.44))⌉ 197: ⌈(44*√(4!-4))⌉ 257: (4^4)+(4/4) \n", + " 17: ⌊(4*4.44)⌋ 77: ⌈(44*(4^.4))⌉ 137: ⌈(4!/(.4*.44))⌉ 197: ⌈(44*√(4!-4))⌉ 257: (4/4)+(4^4) \n", " 18: ⌈(4*4.44)⌉ 78: ⌊(4*(4!-4.4))⌋ 138: √4*⌊(44/√.4)⌋ 198: √4*⌊(√4^√44)⌋ 258: 4-(√4-(4^4)) \n", " 19: ⌈(444/4!)⌉ 79: ⌈(4*(4!-4.4))⌉ 139: ⌊(4^(4-.44))⌋ 199: ⌈(√4*(√4^√44))⌉ 259: ⌊(√44/(.4^4))⌋ \n", " 20: 4*⌈4.44⌉ 80: 4*(44-4!) 140: 44+(4*4!) 200: 4!+(4*44) 260: 4+(4^⌊4.4⌋) \n", @@ -1757,7 +1739,7 @@ " 27: ⌈(4*√44.4)⌉ 87: (√4*44)-⌈.4⌉ 147: ⌊(44^(√4^.4))⌋ 207: ⌈(44^√√4)⌉-4 267: ⌊(4!*(√4!/.44))⌋ \n", " 28: 44-(4*4) 88: 44+44 148: 4+(4!*⌊√44⌋) 208: ⌈(4.4^(4-.4))⌉ 268: ⌊(4!^(4*.44))⌋ \n", " 29: 4!+⌈4.44⌉ 89: ⌈(√4*44.4)⌉ 149: ⌊(.4*(4.4^4))⌋ 209: ⌈(√⌈4.4⌉^√44)⌉ 269: ⌈(4!^(4*.44))⌉ \n", - " 30: ⌈4.44⌉!/4 90: √4*⌈44.4⌉ 150: .4*⌈(4.4^4)⌉ 210: ⌊(44^√(4/√4))⌋ 270: ⌈(4!^(4^√(4/4!)))⌉\n", + " 30: ⌈4.44⌉!/4 90: √4*⌈44.4⌉ 150: .4*⌈(4.4^4)⌉ 210: ⌊(44^√(4-√4))⌋ 270: ⌈(4!^(4^√(4/4!)))⌉\n", " 31: 4!+⌈√44.4⌉ 91: ⌈(444/√4!)⌉ 151: ⌊(4!*√(44-4))⌋ 211: ⌊(√4!*44)⌋-4 271: ⌊((√4!^4.4)/4)⌋ \n", " 32: √4^⌈4.44⌉ 92: 4+(√4*44) 152: (4*44)-4! 212: (4^4)-44 272: 4*(4!+44) \n", " 33: ⌊(4*(4+4.4))⌋ 93: ⌊((4.4^4)/4)⌋ 153: ⌊(4!*(√4+4.4))⌋ 213: ⌊(44.4^√√4)⌋ 273: ⌊(4^(4!^.44))⌋ \n", @@ -1787,8 +1769,8 @@ " 57: ⌈(√√4*(44-4))⌉ 117: ⌈(44*√⌈√44⌉)⌉ 177: ⌊(4*44.4)⌋ 237: ⌊(√.4*(4.4^4))⌋ 297: ⌈(√√4*⌊(44^√√4)⌋)⌉\n", " 58: ⌊(.4^-4.44)⌋ 118: ⌈4.44⌉!-√4 178: ⌈(4*44.4)⌉ 238: ⌊((4+44)^√√4)⌋ 298: (⌈4.4⌉!/.4)-√4 \n", " 59: ⌈(.4^-4.44)⌉ 119: ⌈4.4⌉!-(4/4) 179: ⌈(4*(√.4+44))⌉ 239: ⌈((4+44)^√√4)⌉ 299: (⌈4.4⌉!-.4)/.4 \n", - "CPU times: user 14.4 s, sys: 120 ms, total: 14.5 s\n", - "Wall time: 14.6 s\n" + "CPU times: user 14.6 s, sys: 70.4 ms, total: 14.7 s\n", + "Wall time: 14.7 s\n" ] } ], @@ -1812,7 +1794,7 @@ }, { "cell_type": "code", - "execution_count": 40, + "execution_count": 39, "metadata": { "id": "4MHPG5L8KEBC", "outputId": "63bffec7-8b22-4bb2-cd44-6fd03f124ccf" @@ -1822,7 +1804,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Can make 0 to 171 with (5, 5, 5, 5, 5), ops=\"+-*/^_√!.,\", permute=False. [14,827,653 table entries]\n", + "Can make 0 to 171 with (5, 5, 5, 5, 5), ops=\"+-*/^_√!.,\", permute=False. [14,827,355 table entries]\n", "\n", " 0: 5*(55-55) 43: 55-(5!/(5+5)) 86: (55/.5)-(5!/5) 129: (5!-55.5)/.5 \n", " 1: 5^(55-55) 44: 55-(55/5) 87: (555-5!)/5 130: 5!+(55/5.5) \n", @@ -1832,7 +1814,7 @@ " 5: 5.55-.55 48: 5!/(5*(5.5-5)) 91: (5*5)+(5!*.55) 134: (5!/5)+(55/.5) \n", " 6: 5+(55/55) 49: 55-(.5+5.5) 92: 5+(55+(.5^-5)) 135: (5!+555)/5 \n", " 7: ((5+55)/5)-5 50: 55.5-5.5 93: .5+(5!-(5*5.5)) 136: 5+(5!+(55/5)) \n", - " 8: .5*(5+(55/5)) 51: .5-(5-55.5) 94: 5!-((5/5)+(5*5)) 137: 5+(5!/(5/5.5)) \n", + " 8: .5*(5+(55/5)) 51: .5-(5-55.5) 94: 5!-((5*5)+(5/5)) 137: 5+(5!/(5/5.5)) \n", " 9: 5!-(555/5) 52: 55-(.5+(5*.5)) 95: (55/.55)-5 138: .5+(5*(5*5.5)) \n", " 10: 5!-(55+55) 53: 55.5-(5*.5) 96: 5!-√(5+(55/5))! 139: ((.5+5.5)!/5)-5 \n", " 11: 55/(5.5-.5) 54: 55-(5^(5-5)) 97: 5!-(55-(.5^-5)) 140: .5*(5+(5*55)) \n", @@ -1841,7 +1823,7 @@ " 14: (5*5)-(55/5) 57: 55+((5+5)/5) 100: (55/.5)-(5+5) 143: 5!+(55-(.5^-5)) \n", " 15: 5+(55/5.5) 58: (5*.5)+55.5 101: (55.5-5)/.5 144: ((55/5)-5)!/5 \n", " 16: 5+(5.5+5.5) 59: 5-((5/5)-55) 102: 5+(5!+((5-5!)/5)) 145: 5!+(.5*(55-5)) \n", - " 17: 5+((5+55)/5) 60: 5+(√55*√55) 103: 55+(5!/(5*.5)) 146: 5!+((5/5)+(5*5)) \n", + " 17: 5+((5+55)/5) 60: 5+(√55*√55) 103: 55+(5!/(5*.5)) 146: 5!+((5*5)+(5/5)) \n", " 18: 5+((5!-55)/5) 61: 5.5+55.5 104: 5!-(5+(55/5)) 147: 5!-(.5-(5*5.5)) \n", " 19: (5*5)-(.5+5.5) 62: (55-(5!/5))/.5 105: 55-(5-55) 148: .5+(5!+(5*5.5)) \n", " 20: 55/(5*.55) 63: 5.5-(.5*(5-5!)) 106: (555/5)-5 149: 5+((.5+5.5)!/5) \n", @@ -1867,8 +1849,8 @@ " 40: 55-(5+(5+5)) 83: (.5*5!)-((5-5!)/5) 126: 5!+((55/5)-5) 169: 5!+((5*5)+(5!/5)) \n", " 41: 5!-(55+(5!/5)) 84: 5+(55+(5!/5)) 127: (5!/(5/5.5))-5 170: 5!-(√(5*5)-55) \n", " 42: (5+5.5)/(.5*.5) 85: 5+(55+(5*5)) 128: 5!+(5.5+(5*.5)) 171: (5.5/(.5^5))-5 \n", - "CPU times: user 2min 21s, sys: 2.05 s, total: 2min 23s\n", - "Wall time: 2min 24s\n" + "CPU times: user 2min 13s, sys: 1.09 s, total: 2min 14s\n", + "Wall time: 2min 14s\n" ] } ], @@ -1876,6 +1858,124 @@ "%time show((5, 5, 5, 5, 5))" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Integer Complexity\n", + "\n", + "In number theory, the **complexity of an integer** is the number of \"1\"s needed to make the integer. Different versions of the concept use different sets of operators, but in the [Wikipedia article on Integer complexity](https://en.wikipedia.org/wiki/Integer_complexity), they allow only addition and multiplication. For example with one \"1\" we can only make 1, but with two \"1\"s we can make 1 and 2, and with seven \"1\"s we can make all the integers from 1 to 12 except 11:\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{1: '(1*1)', 2: '(1+1)'}" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "expressions((1, 1), '+*')" + ] + }, + { + "cell_type": "code", + "execution_count": 41, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{1: '(1*(1*(1*(1*(1*(1*1))))))',\n", + " 2: '(1+(1*(1*(1*(1*(1*1))))))',\n", + " 3: '(1+(1+(1*(1*(1*(1*1))))))',\n", + " 4: '(1+(1+(1+(1*(1*(1*1))))))',\n", + " 5: '(1+(1+(1+(1+(1*(1*1))))))',\n", + " 6: '(1+(1+(1+(1+(1+(1*1))))))',\n", + " 7: '(1+(1+(1+(1+(1+(1+1))))))',\n", + " 8: '(1+(1+((1+1)*(1+(1+1)))))',\n", + " 9: '(1+((1+1)*(1+(1+(1+1)))))',\n", + " 10: '(1+((1+(1+1))*(1+(1+1))))',\n", + " 12: '((1+1)*((1+1)*(1+(1+1))))'}" + ] + }, + "execution_count": 41, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "expressions(7 * (1,), '+*')" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "metadata": {}, + "outputs": [], + "source": [ + "@cache\n", + "def complexity(end=17) -> dict:\n", + " \"\"\"A table of {i: complexity_number} for all integers i with complexity < `end`.\"\"\"\n", + " results = {}\n", + " for n in range(1, end):\n", + " for x in expressions(n * (1,), '+*'):\n", + " if x not in results:\n", + " results[x] = n\n", + " return results" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 1: 1 | 21: 9 | 41: 12 | 61: 13 | 81: 12 | 101: 15 | 121: 15 | 141: 16 | 161: 16 | 186: 16 | 225: 16 |\n", + " 2: 2 | 22: 10 | 42: 11 | 62: 13 | 82: 13 | 102: 14 | 122: 15 | 142: 16 | 162: 14 | 189: 15 | 228: 16 |\n", + " 3: 3 | 23: 11 | 43: 12 | 63: 12 | 83: 14 | 103: 15 | 123: 15 | 143: 16 | 163: 15 | 190: 16 | 234: 16 |\n", + " 4: 4 | 24: 9 | 44: 12 | 64: 12 | 84: 13 | 104: 14 | 124: 15 | 144: 14 | 164: 15 | 192: 15 | 240: 16 |\n", + " 5: 5 | 25: 10 | 45: 11 | 65: 13 | 85: 14 | 105: 14 | 125: 15 | 145: 15 | 165: 15 | 193: 16 | 243: 15 |\n", + " 6: 5 | 26: 10 | 46: 12 | 66: 13 | 86: 14 | 106: 15 | 126: 14 | 146: 15 | 166: 16 | 194: 16 | 244: 16 |\n", + " 7: 6 | 27: 9 | 47: 13 | 67: 14 | 87: 14 | 107: 16 | 127: 15 | 147: 15 | 168: 15 | 195: 16 | 246: 16 |\n", + " 8: 6 | 28: 10 | 48: 11 | 68: 13 | 88: 14 | 108: 13 | 128: 14 | 148: 15 | 169: 16 | 196: 16 | 252: 16 |\n", + " 9: 6 | 29: 11 | 49: 12 | 69: 14 | 89: 15 | 109: 14 | 129: 15 | 149: 16 | 170: 16 | 198: 16 | 256: 16 |\n", + " 10: 7 | 30: 10 | 50: 12 | 70: 13 | 90: 13 | 110: 14 | 130: 15 | 150: 15 | 171: 15 | 200: 16 | 270: 16 |\n", + " 11: 8 | 31: 11 | 51: 12 | 71: 14 | 91: 14 | 111: 14 | 131: 16 | 151: 16 | 172: 16 | 204: 16 | 288: 16 |\n", + " 12: 7 | 32: 10 | 52: 12 | 72: 12 | 92: 14 | 112: 14 | 132: 15 | 152: 15 | 174: 16 | 208: 16 | 324: 16 |\n", + " 13: 8 | 33: 11 | 53: 13 | 73: 13 | 93: 14 | 113: 15 | 133: 15 | 153: 15 | 175: 16 | 210: 16 |\n", + " 14: 8 | 34: 11 | 54: 11 | 74: 13 | 94: 15 | 114: 14 | 134: 16 | 154: 16 | 176: 16 | 216: 15 |\n", + " 15: 8 | 35: 11 | 55: 12 | 75: 13 | 95: 14 | 115: 15 | 135: 14 | 155: 16 | 180: 15 | 217: 16 |\n", + " 16: 8 | 36: 10 | 56: 12 | 76: 13 | 96: 13 | 116: 15 | 136: 15 | 156: 15 | 181: 16 | 218: 16 |\n", + " 17: 9 | 37: 11 | 57: 12 | 77: 14 | 97: 14 | 117: 14 | 137: 16 | 157: 16 | 182: 16 | 219: 16 |\n", + " 18: 8 | 38: 11 | 58: 13 | 78: 13 | 98: 14 | 118: 15 | 138: 15 | 158: 16 | 183: 16 | 220: 16 |\n", + " 19: 9 | 39: 11 | 59: 14 | 79: 14 | 99: 14 | 119: 15 | 139: 16 | 159: 16 | 184: 16 | 222: 16 |\n", + " 20: 9 | 40: 11 | 60: 12 | 80: 13 | 100: 14 | 120: 14 | 140: 15 | 160: 15 | 185: 16 | 224: 16 |\n" + ] + } + ], + "source": [ + "def show_complexity(end=17) -> None:\n", + " \"\"\"Show a table of integer: complexity_number.\"\"\"\n", + " table = complexity(end)\n", + " show_columns([f'{x:3}: {table[x]:3} |' for x in sorted(table)])\n", + "\n", + "show_complexity()" + ] + }, { "cell_type": "markdown", "metadata": { @@ -1889,7 +1989,7 @@ }, { "cell_type": "code", - "execution_count": 41, + "execution_count": 44, "metadata": {}, "outputs": [], "source": [ @@ -1901,7 +2001,7 @@ }, { "cell_type": "code", - "execution_count": 42, + "execution_count": 45, "metadata": {}, "outputs": [ { @@ -1936,7 +2036,7 @@ "- **Percent**: `50%` = 0.5\n", "- **Absolute value**: `|1-3|` = 2 (redundant if you have unary minus, but add it if you like it)\n", "- **Repeating decimal**: `.4...` = .44444444... = 4/9\n", - "- **Comparison operations**: `1>(2<3)` = 0, because `(2<3)` is True, which is treated as `1`, and `1>1` is False, or `0`\n", + "- **Comparison operations**: `(1<2)+(3=4)`= 0, because `(1<2)` is True, which is treated as `1`, and `3=4` is False, or `0`\n", "- **Double factorial**: `9!!` = 9 × 7 × 5 × 3 × 1 = 945; not the same as `(9!)!`\n", "- **Gamma function**: `Γ(n)` = (n − 1)! and works for non-integers\n", "- **Prime counting function**: `π(n)` = number of primes ≲ n; e.g. `π(5)` = 3\n", diff --git a/ipynb/Fred Buns.ipynb b/ipynb/Fred Buns.ipynb index ffd2887..04cb95a 100644 --- a/ipynb/Fred Buns.ipynb +++ b/ipynb/Fred Buns.ipynb @@ -4,7 +4,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "
Peter Norvig, 15 June 2015
" + "
Peter Norvig, June 2015
(Updated April 2024)
" ] }, { @@ -18,33 +18,31 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The [June 15, 2015 post](http://bikesnobnyc.blogspot.com/2015/06/lets-get-this-show-on-road-once-we-all.html) on *Bike Snob NYC* leads with \"*Let's talk about bike locks.*\" Here's what I want to talk about: in my local bike shop, I saw a combination lock called *WordLock®*,\n", - "which replaces digits with letters. I classified this as a Fred lock,\n", - "\"[Fred](http://bikesnobnyc.blogspot.com/2014/06/a-fred-too-far.html)\" being the term for an amateurish cyclist with inappropriate equipment.\n", - "I tried the combination \"FRED,\" and was amused with the result:\n", + "The [June 15, 2015 post](http://bikesnobnyc.blogspot.com/2015/06/lets-get-this-show-on-road-once-we-all.html) on [*Bike Snob NYC*](http://bikesnobnyc.blogspot.com) leads with \"*Let's talk about bike locks.*\" Here's what I want to talk about: in my local bike shop, I saw a combination lock called *WordLock®*,\n", + "which replaces digits with letters. There are 4 discs in the lock, each of which has 10 distinct letters. I classified this as a Fred lock,\n", + "\"[Fred](http://bikesnobnyc.blogspot.com/2014/06/a-fred-too-far.html)\" being the term for an amateurish cyclist with inappropriate equipment. I played around with it and got this:\n", + "\n", "![](http://norvig.com/ipython/fredbuns.jpg)\n", - "FRED BUNS! Naturally I set all the other locks on the rack to FRED BUNS as well. But my curiosity was raised ... \n", + "\n", + "\n", + "Naturally I set the other locks on the rack to FRED BUNS as well. But I have questions ... \n", "\n", "# Questions\n", "\n", "1. How many words can the WordLock® make?\n", - "3. Can a lock with different letters on the tumblers make more words? \n", - "4. How many words can be made simultaneously? For example, with the tumbler set to \"FRED\", the lock\n", - "above also makes \"BUNS\" in the next line, but with \"SOMN\", fails to make a word in the third line.\n", - "Could different letters make words in every horizontal line?\n", - "5. Is it a coincidence that the phrase \"FRED BUNS\" appears, or was it planted there by mischievous WordLock® designers? \n", + "3. Can a lock with different letters on the discs make more words? \n", + "4. How many words can be made simultaneously? The photo above shows the words \"FRED\" and \"BUNS,\" but \"SOMN\" is not a word.\n", + "5. Is it a coincidence that the phrase \"FRED BUNS\" appears, or was it planted there by WordLock® designers? \n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Preliminaries\n", "\n", - "# Vocabulary\n", - "\n", - "\n", - "Before we can answer the questions, we'll need to be clear about the vocabulary of the problem and how to represent concepts in code:\n", - "\n", - "* **Lock**: For our purposes a lock can be modeled as a `list` of 4 **tumblers**. \n", - "* **Tumbler:** Each tumbler has 10 distinct letters. I will represent a tumbler as a `str` of 10 letters.\n", - "* **Combination**: Choosing a letter from each tumbler gives a combination, such as \"FRED\" or \"BUNS\". There are 104 = 10,000 combinations.\n", - "* **Word**: Some combinations (such as \"BUNS\") are *words*; others (such as \"SOMN\") are not words. We'll need a collection of dictionary words.\n", - "\n", - "Now on to the code! First the imports I will need and the vocabulary concepts:" + "First, set the stage with some (1) imports, (2) constants, and (3) type definitions:\n" ] }, { @@ -53,21 +51,33 @@ "metadata": {}, "outputs": [], "source": [ - "from __future__ import division, print_function # To work in Python 2.x or 3.x\n", - "from collections import Counter, defaultdict\n", + "from collections import Counter\n", + "from typing import *\n", + "from functools import lru_cache\n", "import itertools\n", "import random \n", + "import re\n", + "import textwrap\n", "\n", - "Lock = list # A Lock is a list of tumblers\n", - "Tumbler = ''.join # A Tumbler is 10 characters joined into a str\n", - "Word = ''.join # A word is 4 letters joined into a str" + "wordlock = ('SPHMTWDLFB', 'LEYHNRUOAI', 'ENMLRTAOSK', 'DSNMPYLKTE') # The lock in the photo above\n", + "ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' # The 26 letters\n", + "DISCS = 4 # A lock has 4 discs.\n", + "LETTERS = 10 # A disc has 10 letters.\n", + "\n", + "Letter = str # A single letter from the ALPHABET.\n", + "Disc = str # A sequence of letters (joined into a string) forms a disc.\n", + "Lock = tuple # A tuple of discs forms a lock.\n", + "Word = str # A 4-letter string that is in the list of valid words. (Some strings are non-words.)\n", + "Regex = str # A regular expression (used to find all the words that can be made by a lock)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "The only remaining vocabulary concept is `combinations`. I will define `fredbuns` to be a lock with four tumblers, but with each tumbler consisting of not all ten letters, but only the two letters that spell \"FRED BUNS\": " + "# The Word List\n", + "\n", + "I happen to have a file of four-letter words (no, not *[that](http://en.wikipedia.org/wiki/Four-letter_word)* kind of four-letter word). It is the union of an official Scrabble® word list with a list of proper names. The following shell command tests if the file has already been downloaded to the local directory and if not, fetches it from the web:" ] }, { @@ -75,120 +85,6 @@ "execution_count": 2, "metadata": {}, "outputs": [], - "source": [ - "fredbuns = ['FB', 'RU', 'EN', 'DS'] # A lock with two letters on each of four tumblers" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We need a way to get the combinations that can be made from this lock. It turns out that the built-in function `itertools.product` does most of the job; it generates the product of all 2 × 2 × 2 × 2 = 16 combinations of letters:" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[('F', 'R', 'E', 'D'),\n", - " ('F', 'R', 'E', 'S'),\n", - " ('F', 'R', 'N', 'D'),\n", - " ('F', 'R', 'N', 'S'),\n", - " ('F', 'U', 'E', 'D'),\n", - " ('F', 'U', 'E', 'S'),\n", - " ('F', 'U', 'N', 'D'),\n", - " ('F', 'U', 'N', 'S'),\n", - " ('B', 'R', 'E', 'D'),\n", - " ('B', 'R', 'E', 'S'),\n", - " ('B', 'R', 'N', 'D'),\n", - " ('B', 'R', 'N', 'S'),\n", - " ('B', 'U', 'E', 'D'),\n", - " ('B', 'U', 'E', 'S'),\n", - " ('B', 'U', 'N', 'D'),\n", - " ('B', 'U', 'N', 'S')]" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "list(itertools.product(*fredbuns))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "I would prefer to deal with the string `'BUNS'` rather than the tuple `('B', 'U', 'N', 'S')`, so I will define a function, `combinations`, that takes a lock as input and returns a set of strings representing the combinations:" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [], - "source": [ - "def combinations(lock):\n", - " \"Return a list of all combinations that can be made by this lock.\"\n", - " return {Word(combo) for combo in itertools.product(*lock)}" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'BRED',\n", - " 'BRES',\n", - " 'BRND',\n", - " 'BRNS',\n", - " 'BUED',\n", - " 'BUES',\n", - " 'BUND',\n", - " 'BUNS',\n", - " 'FRED',\n", - " 'FRES',\n", - " 'FRND',\n", - " 'FRNS',\n", - " 'FUED',\n", - " 'FUES',\n", - " 'FUND',\n", - " 'FUNS'}" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "combinations(fredbuns)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Dictionary Words\n", - "===\n", - "\n", - "I happen to have handy a file of four-letter words (no, not *[that](http://en.wikipedia.org/wiki/Four-letter_word)* kind of four-letter word). It is the union of an official Scrabble® word list and a list of proper names. The following shell command tests if the file has already been downloaded to our local directory and if not, fetches it from the web:" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "metadata": {}, - "outputs": [], "source": [ "! [ -e words4.txt ] || curl -O http://norvig.com/ngrams/words4.txt" ] @@ -197,54 +93,22 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Here are the first few lines of the file:" + "I will use this word list two ways: `WORDSTR` is a big string of words separated by newlines. `WORDS` is a list of the individual words in the file:" ] }, { "cell_type": "code", - "execution_count": 7, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "AADI\r\n", - "AAHS\r\n", - "AALS\r\n", - "ABAS\r\n", - "ABBA\r\n", - "ABBE\r\n", - "ABBY\r\n", - "ABED\r\n", - "ABEL\r\n", - "ABET\r\n" - ] - } - ], - "source": [ - "! head words4.txt" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Python can make a set of words:" - ] - }, - { - "cell_type": "code", - "execution_count": 8, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ - "WORDS = set(open('words4.txt').read().split())" + "WORDSTR = open('words4.txt').read()\n", + "WORDS = WORDSTR.split()" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 4, "metadata": {}, "outputs": [ { @@ -253,7 +117,7 @@ "4360" ] }, - "execution_count": 9, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -266,123 +130,210 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "So that means that no lock could ever make more than 4,360 words. Let's define `words_from(lock)` to be the intersection of the dictionary words and the words that can be made with a lock:" + "Here is a random sampling of the words:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['STOW',\n", + " 'DRUM',\n", + " 'AIDS',\n", + " 'CUSS',\n", + " 'BAYS',\n", + " 'COSH',\n", + " 'DEED',\n", + " 'PHON',\n", + " 'KARA',\n", + " 'ANOA']" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "random.seed(1234) # for reproducability\n", + "\n", + "random.sample(WORDS, LETTERS)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Question 1: How Many Words can the WordLock® make?\n", + "\n", + "My approach:\n", + "- Given a lock, I need to compare it against a list of valid words to see which words can be made by the lock.\n", + "- I think of this as a matching problem: what words match the pattern described by the lock.\n", + "- Python's `re` module is good at matching.\n", + " - I'll define `regex` to create a regular expression that represents a lock's 104 possible combinations.\n", + " - I'll define `words_from(lock)` to match the regular expression against all the words in the word list.\n", + " - I can do this in just one call to `re.findall`.\n", + "- I'll define `word_count` to count the number of words a lock makes, and cache the results (to save time on future calls)." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "def regex(lock: Lock) -> Regex: \n", + " \"\"\"A regular expression describing all 10**4 combinations that this lock can make.\"\"\"\n", + " return cat(('[' + disc + ']') for disc in lock)\n", + "\n", + "def words_from(lock: Lock, wordstr=WORDSTR) -> List[Word]: \n", + " \"\"\"A list of all valid words that can be made by this lock.\"\"\"\n", + " return re.findall(regex(lock), wordstr)\n", + "\n", + "@lru_cache(None)\n", + "def word_count(lock) -> int: return len(words_from(lock))\n", + "\n", + "cat = ''.join # Function to concatenate strings with no space between them.\n", + "space = ' '.join # Function to join strings with a space between each one." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'[SPHMTWDLFB][LEYHNRUOAI][ENMLRTAOSK][DSNMPYLKTE]'" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "regex(wordlock)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can now answer question 1:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1118" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "word_count(wordlock)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "That's our answer: WordLock® can make 1,118 words (about 1/4 of the word list). \n", + "\n", + "# Visualizing Results\n", + "\n", + "I don't want to print a list with 1,118 lines, so I'll define `show` to print in a prettier format:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "def show(lock: Lock, width=110) -> None:\n", + " \"\"\"Show (print) a lock, the words it makes, and the word count.\"\"\"\n", + " words = words_from(lock)\n", + " print(f'{len(words):,d} words can be made by the lock ({space(lock)}):\\n')\n", + " print(textwrap.fill(space(sorted(words)), width))" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, - "outputs": [], - "source": [ - "def words_from(lock): \n", - " \"A list of words that can be made by lock.\"\n", - " return WORDS & combinations(lock)" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'BRED', 'BUND', 'BUNS', 'FRED', 'FUND', 'FUNS'}" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "words_from(fredbuns)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "I will also introduce the function `show` to print out a lock and its words:" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [], - "source": [ - "def show(lock):\n", - " \"Show a lock and the words it makes.\"\n", - " words = words_from(lock)\n", - " print('Lock: {}\\nCount: {}\\nWords: {}'\n", - " .format(space(lock), len(words), space(sorted(words))))\n", - " \n", - "space = ' '.join # Function to concatenate strings with a space between each one." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Lock: FB RU EN DS\n", - "Count: 6\n", - "Words: BRED BUND BUNS FRED FUND FUNS\n" - ] - } - ], - "source": [ - "show(fredbuns)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "For this tiny lock with just two letters on each tumbler, we find that 6 out of the 16 possible combinations are words. We're now ready to answer the real questions." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Question 1: How Many Words?" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Here is the answer:" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [], - "source": [ - "wordlock = ['SPHMTWDLFB', 'LEYHNRUOAI', 'ENMLRTAOSK', 'DSNMPYLKTE']" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Lock: SPHMTWDLFB LEYHNRUOAI ENMLRTAOSK DSNMPYLKTE\n", - "Count: 1118\n", - "Words: BAAL BAAS BAKE BALD BALE BALK BALL BALM BALS BAMS BAND BANE BANK BANS BARD BARE BARK BARM BARN BARS BASE BASK BASS BAST BATE BATS BATT BEAD BEAK BEAM BEAN BEAT BEEN BEEP BEES BEET BELL BELS BELT BEND BENE BENS BENT BERK BERM BEST BETS BIAS BIKE BILE BILK BILL BIND BINE BINS BINT BIOS BIRD BIRK BIRL BISE BISK BITE BITS BITT BLAE BLAM BLAT BLED BLET BLOT BOAS BOAT BOLD BOLE BOLL BOLT BOND BONE BONK BONY BOOK BOOM BOON BOOS BOOT BORE BORK BORN BORT BOSK BOSS BOTS BOTT BRAD BRAE BRAN BRAS BRAT BRAY BRED BREE BREN BROS BULK BULL BUMP BUMS BUND BUNK BUNN BUNS BUNT BUOY BURD BURL BURN BURP BURS BURY BUSK BUSS BUST BUSY BUTE BUTS BUTT BYES BYRE BYRL BYTE DAKS DALE DALS DAME DAMN DAMP DAMS DANE DANK DANS DARE DARK DARN DART DATE DEAD DEAL DEAN DEED DEEM DEEP DEES DEET DEKE DELE DELL DELS DELT DEME DEMY DENE DENS DENT DENY DEON DERE DERM DESK DHAK DHAL DIAL DIED DIEL DIES DIET DIKE DILL DIME DIMS DINE DINK DINS DINT DIOL DION DIRE DIRK DIRL DIRT DISK DISS DITE DITS DOAT DOES DOLE DOLL DOLS DOLT DOME DOMS DONE DONS DOOM DORE DORK DORM DORP DORS DORY DOSE DOSS DOST DOTE DOTS DOTY DRAM DRAT DRAY DREE DREK DROP DUAD DUAL DUEL DUES DUET DUKE DULL DULY DUMP DUNE DUNK DUNS DUNT DUOS DURE DURN DUSK DUST DUTY DYAD DYED DYES DYKE DYNE FAKE FALL FAME FANE FANS FARD FARE FARL FARM FART FAST FATE FATS FEAL FEAT FEED FEEL FEES FEET FELL FELT FEME FEMS FEND FENS FEOD FERE FERN FESS FEST FETE FETS FIAT FILE FILL FILM FILS FIND FINE FINK FINN FINS FIRE FIRM FIRN FIRS FIST FITS FLAK FLAM FLAN FLAP FLAT FLAY FLED FLEE FLEY FLOE FLOP FOAL FOAM FOES FOLD FOLK FOND FONS FONT FOOD FOOL FOOT FORD FORE FORK FORM FORT FOSS FRAE FRAP FRAT FRAY FRED FREE FRET FROE FROM FUEL FULL FUME FUMY FUND FUNK FUNS FURL FURS FURY FUSE FUSS FYKE HAED HAEM HAEN HAES HAET HAKE HALE HALL HALM HALT HAME HAMS HAND HANK HANS HANT HARD HARE HARK HARL HARM HARP HART HASP HAST HATE HATS HEAD HEAL HEAP HEAT HEED HEEL HELD HELL HELM HELP HEME HEMP HEMS HENS HENT HERD HERE HERL HERM HERN HERS HEST HETS HIED HIES HIKE HILL HILT HIMS HIND HINS HINT HIRE HISN HISS HIST HITS HOED HOES HOKE HOLD HOLE HOLK HOLM HOLP HOLS HOLT HOLY HOME HOMY HONE HONK HONS HOOD HOOK HOOP HOOT HORN HOSE HOST HOTS HUED HUES HULK HULL HUMP HUMS HUNK HUNS HUNT HURL HURT HUSK HUTS HYMN HYTE LAKE LAKY LALL LAME LAMP LAMS LAND LANE LANK LARD LARK LARS LASE LASS LAST LATE LATS LEAD LEAK LEAL LEAN LEAP LEAS LEEK LEES LEET LEKE LEKS LEND LENS LENT LEON LESS LEST LETS LIAM LIAN LIED LIEN LIES LIKE LILT LILY LIME LIMN LIMP LIMY LINE LINK LINN LINS LINT LINY LION LIRE LISP LIST LITE LITS LOAD LOAM LOAN LOLL LONE LOOK LOOM LOON LOOP LOOS LOOT LORD LORE LORN LORY LOSE LOSS LOST LOTS LUES LUKE LULL LUMP LUMS LUNE LUNK LUNT LUNY LURE LURK LUST LUTE LYAM LYES LYLE LYRE LYSE MAES MAKE MALE MALL MALM MALT MANE MANS MANY MARE MARK MARL MARS MART MARY MASK MASS MAST MATE MATS MATT MEAD MEAL MEAN MEAT MEED MEEK MEET MELD MELL MELS MELT MEME MEMS MEND MERE MERK MERL MESS METE MHOS MIEN MIKE MILD MILE MILK MILL MILS MILT MIME MIND MINE MINK MINT MIRE MIRK MIRS MIRY MISE MISS MIST MITE MITT MITY MOAN MOAS MOAT MOKE MOLD MOLE MOLL MOLS MOLT MOLY MOME MOMS MONK MONS MONY MOOD MOOL MOON MOOS MOOT MORE MORN MORS MORT MOSK MOSS MOST MOTE MOTS MOTT MULE MULL MUMM MUMP MUMS MUNS MUON MURE MURK MUSE MUSK MUSS MUST MUTE MUTS MUTT PALE PALL PALM PALP PALS PALY PAMS PANE PANS PANT PARD PARE PARK PARS PART PASE PASS PAST PATE PATS PATY PEAK PEAL PEAN PEAS PEAT PEED PEEK PEEL PEEN PEEP PEES PEKE PELE PELT PEND PENS PENT PEON PERE PERK PERM PERP PERT PEST PETS PHAT PHON PHOT PIAL PIAN PIAS PIED PIES PIKE PILE PILL PILY PIMP PINE PINK PINS PINT PINY PION PIRN PISS PITS PITY PLAN PLAT PLAY PLED PLOD PLOP PLOT PLOY POEM POET POKE POKY POLE POLL POLS POLY POME POMP POMS POND PONE PONS PONY POOD POOL POON POOP POOS PORE PORK PORN PORT POSE POST POSY POTS PRAM PRAT PRAY PREE PREP PREY PROD PROM PROP PROS PUKE PULE PULL PULP PULS PUMP PUNK PUNS PUNT PUNY PURE PURL PURS PUSS PUTS PUTT PYAS PYES PYRE SAAD SAKE SALE SALL SALP SALS SALT SAME SAMP SAND SANE SANK SANS SARD SARK SASS SATE SEAL SEAM SEAN SEAS SEAT SEED SEEK SEEL SEEM SEEN SEEP SEES SELL SELS SEME SEND SENE SENT SERE SERS SETS SETT SHAD SHAE SHAM SHAT SHAY SHED SHES SHOD SHOE SHOP SHOT SIAL SIKE SILD SILK SILL SILT SIMP SIMS SINE SINK SINS SIRE SIRS SITE SITS SLAM SLAP SLAT SLAY SLED SLOE SLOP SLOT SNAP SNED SNOT SOAK SOAP SOKE SOLD SOLE SOLS SOME SOMS SONE SONS SOOK SOON SOOT SORD SORE SORN SORT SOTS SUED SUES SUET SUKS SULK SUMP SUMS SUNK SUNN SUNS SURD SURE SUSS SYED SYKE SYNE TAEL TAKE TALE TALK TALL TAME TAMP TAMS TANK TANS TAOS TARE TARN TARP TARS TART TASK TASS TATE TATS TEAK TEAL TEAM TEAS TEAT TEED TEEL TEEM TEEN TEES TELE TELL TELS TEMP TEND TENS TENT TERM TERN TESS TEST TETS THAE THAN THAT THEE THEM THEN THEY TIED TIES TIKE TILE TILL TILS TILT TIME TINE TINS TINT TINY TIRE TIRL TITS TOAD TOED TOES TOKE TOLD TOLE TOLL TOME TOMS TONE TONS TONY TOOK TOOL TOOM TOON TOOT TORE TORN TORS TORT TORY TOSS TOST TOTE TOTS TRAD TRAE TRAM TRAP TRAY TREE TREK TRES TRET TREY TROD TROP TROT TROY TULE TUMP TUNE TUNS TURD TURK TURN TUSK TUTS TYEE TYES TYKE TYNE TYRE WAES WAKE WALE WALK WALL WALY WAME WAND WANE WANK WANS WANT WANY WARD WARE WARK WARM WARN WARP WARS WART WARY WASP WAST WATS WATT WEAK WEAL WEAN WEED WEEK WEEL WEEN WEEP WEES WEET WELD WELL WELT WEND WENS WENT WERE WERT WEST WETS WHAM WHAP WHAT WHEE WHEN WHET WHEY WHOM WHOP WILD WILE WILL WILT WILY WIMP WIND WINE WINK WINS WINY WIRE WIRY WISE WISP WISS WIST WITE WITS WOAD WOES WOKE WOKS WOLD WONK WONS WONT WOOD WOOL WOOS WORD WORE WORK WORM WORN WORT WOST WOTS WRAP WREN WUSS WYES WYLE WYND WYNN WYNS WYTE\n" + "1,118 words can be made by the lock (SPHMTWDLFB LEYHNRUOAI ENMLRTAOSK DSNMPYLKTE):\n", + "\n", + "BAAL BAAS BAKE BALD BALE BALK BALL BALM BALS BAMS BAND BANE BANK BANS BARD BARE BARK BARM BARN BARS BASE BASK\n", + "BASS BAST BATE BATS BATT BEAD BEAK BEAM BEAN BEAT BEEN BEEP BEES BEET BELL BELS BELT BEND BENE BENS BENT BERK\n", + "BERM BEST BETS BIAS BIKE BILE BILK BILL BIND BINE BINS BINT BIOS BIRD BIRK BIRL BISE BISK BITE BITS BITT BLAE\n", + "BLAM BLAT BLED BLET BLOT BOAS BOAT BOLD BOLE BOLL BOLT BOND BONE BONK BONY BOOK BOOM BOON BOOS BOOT BORE BORK\n", + "BORN BORT BOSK BOSS BOTS BOTT BRAD BRAE BRAN BRAS BRAT BRAY BRED BREE BREN BROS BULK BULL BUMP BUMS BUND BUNK\n", + "BUNN BUNS BUNT BUOY BURD BURL BURN BURP BURS BURY BUSK BUSS BUST BUSY BUTE BUTS BUTT BYES BYRE BYRL BYTE DAKS\n", + "DALE DALS DAME DAMN DAMP DAMS DANE DANK DANS DARE DARK DARN DART DATE DEAD DEAL DEAN DEED DEEM DEEP DEES DEET\n", + "DEKE DELE DELL DELS DELT DEME DEMY DENE DENS DENT DENY DEON DERE DERM DESK DHAK DHAL DIAL DIED DIEL DIES DIET\n", + "DIKE DILL DIME DIMS DINE DINK DINS DINT DIOL DION DIRE DIRK DIRL DIRT DISK DISS DITE DITS DOAT DOES DOLE DOLL\n", + "DOLS DOLT DOME DOMS DONE DONS DOOM DORE DORK DORM DORP DORS DORY DOSE DOSS DOST DOTE DOTS DOTY DRAM DRAT DRAY\n", + "DREE DREK DROP DUAD DUAL DUEL DUES DUET DUKE DULL DULY DUMP DUNE DUNK DUNS DUNT DUOS DURE DURN DUSK DUST DUTY\n", + "DYAD DYED DYES DYKE DYNE FAKE FALL FAME FANE FANS FARD FARE FARL FARM FART FAST FATE FATS FEAL FEAT FEED FEEL\n", + "FEES FEET FELL FELT FEME FEMS FEND FENS FEOD FERE FERN FESS FEST FETE FETS FIAT FILE FILL FILM FILS FIND FINE\n", + "FINK FINN FINS FIRE FIRM FIRN FIRS FIST FITS FLAK FLAM FLAN FLAP FLAT FLAY FLED FLEE FLEY FLOE FLOP FOAL FOAM\n", + "FOES FOLD FOLK FOND FONS FONT FOOD FOOL FOOT FORD FORE FORK FORM FORT FOSS FRAE FRAP FRAT FRAY FRED FREE FRET\n", + "FROE FROM FUEL FULL FUME FUMY FUND FUNK FUNS FURL FURS FURY FUSE FUSS FYKE HAED HAEM HAEN HAES HAET HAKE HALE\n", + "HALL HALM HALT HAME HAMS HAND HANK HANS HANT HARD HARE HARK HARL HARM HARP HART HASP HAST HATE HATS HEAD HEAL\n", + "HEAP HEAT HEED HEEL HELD HELL HELM HELP HEME HEMP HEMS HENS HENT HERD HERE HERL HERM HERN HERS HEST HETS HIED\n", + "HIES HIKE HILL HILT HIMS HIND HINS HINT HIRE HISN HISS HIST HITS HOED HOES HOKE HOLD HOLE HOLK HOLM HOLP HOLS\n", + "HOLT HOLY HOME HOMY HONE HONK HONS HOOD HOOK HOOP HOOT HORN HOSE HOST HOTS HUED HUES HULK HULL HUMP HUMS HUNK\n", + "HUNS HUNT HURL HURT HUSK HUTS HYMN HYTE LAKE LAKY LALL LAME LAMP LAMS LAND LANE LANK LARD LARK LARS LASE LASS\n", + "LAST LATE LATS LEAD LEAK LEAL LEAN LEAP LEAS LEEK LEES LEET LEKE LEKS LEND LENS LENT LEON LESS LEST LETS LIAM\n", + "LIAN LIED LIEN LIES LIKE LILT LILY LIME LIMN LIMP LIMY LINE LINK LINN LINS LINT LINY LION LIRE LISP LIST LITE\n", + "LITS LOAD LOAM LOAN LOLL LONE LOOK LOOM LOON LOOP LOOS LOOT LORD LORE LORN LORY LOSE LOSS LOST LOTS LUES LUKE\n", + "LULL LUMP LUMS LUNE LUNK LUNT LUNY LURE LURK LUST LUTE LYAM LYES LYLE LYRE LYSE MAES MAKE MALE MALL MALM MALT\n", + "MANE MANS MANY MARE MARK MARL MARS MART MARY MASK MASS MAST MATE MATS MATT MEAD MEAL MEAN MEAT MEED MEEK MEET\n", + "MELD MELL MELS MELT MEME MEMS MEND MERE MERK MERL MESS METE MHOS MIEN MIKE MILD MILE MILK MILL MILS MILT MIME\n", + "MIND MINE MINK MINT MIRE MIRK MIRS MIRY MISE MISS MIST MITE MITT MITY MOAN MOAS MOAT MOKE MOLD MOLE MOLL MOLS\n", + "MOLT MOLY MOME MOMS MONK MONS MONY MOOD MOOL MOON MOOS MOOT MORE MORN MORS MORT MOSK MOSS MOST MOTE MOTS MOTT\n", + "MULE MULL MUMM MUMP MUMS MUNS MUON MURE MURK MUSE MUSK MUSS MUST MUTE MUTS MUTT PALE PALL PALM PALP PALS PALY\n", + "PAMS PANE PANS PANT PARD PARE PARK PARS PART PASE PASS PAST PATE PATS PATY PEAK PEAL PEAN PEAS PEAT PEED PEEK\n", + "PEEL PEEN PEEP PEES PEKE PELE PELT PEND PENS PENT PEON PERE PERK PERM PERP PERT PEST PETS PHAT PHON PHOT PIAL\n", + "PIAN PIAS PIED PIES PIKE PILE PILL PILY PIMP PINE PINK PINS PINT PINY PION PIRN PISS PITS PITY PLAN PLAT PLAY\n", + "PLED PLOD PLOP PLOT PLOY POEM POET POKE POKY POLE POLL POLS POLY POME POMP POMS POND PONE PONS PONY POOD POOL\n", + "POON POOP POOS PORE PORK PORN PORT POSE POST POSY POTS PRAM PRAT PRAY PREE PREP PREY PROD PROM PROP PROS PUKE\n", + "PULE PULL PULP PULS PUMP PUNK PUNS PUNT PUNY PURE PURL PURS PUSS PUTS PUTT PYAS PYES PYRE SAAD SAKE SALE SALL\n", + "SALP SALS SALT SAME SAMP SAND SANE SANK SANS SARD SARK SASS SATE SEAL SEAM SEAN SEAS SEAT SEED SEEK SEEL SEEM\n", + "SEEN SEEP SEES SELL SELS SEME SEND SENE SENT SERE SERS SETS SETT SHAD SHAE SHAM SHAT SHAY SHED SHES SHOD SHOE\n", + "SHOP SHOT SIAL SIKE SILD SILK SILL SILT SIMP SIMS SINE SINK SINS SIRE SIRS SITE SITS SLAM SLAP SLAT SLAY SLED\n", + "SLOE SLOP SLOT SNAP SNED SNOT SOAK SOAP SOKE SOLD SOLE SOLS SOME SOMS SONE SONS SOOK SOON SOOT SORD SORE SORN\n", + "SORT SOTS SUED SUES SUET SUKS SULK SUMP SUMS SUNK SUNN SUNS SURD SURE SUSS SYED SYKE SYNE TAEL TAKE TALE TALK\n", + "TALL TAME TAMP TAMS TANK TANS TAOS TARE TARN TARP TARS TART TASK TASS TATE TATS TEAK TEAL TEAM TEAS TEAT TEED\n", + "TEEL TEEM TEEN TEES TELE TELL TELS TEMP TEND TENS TENT TERM TERN TESS TEST TETS THAE THAN THAT THEE THEM THEN\n", + "THEY TIED TIES TIKE TILE TILL TILS TILT TIME TINE TINS TINT TINY TIRE TIRL TITS TOAD TOED TOES TOKE TOLD TOLE\n", + "TOLL TOME TOMS TONE TONS TONY TOOK TOOL TOOM TOON TOOT TORE TORN TORS TORT TORY TOSS TOST TOTE TOTS TRAD TRAE\n", + "TRAM TRAP TRAY TREE TREK TRES TRET TREY TROD TROP TROT TROY TULE TUMP TUNE TUNS TURD TURK TURN TUSK TUTS TYEE\n", + "TYES TYKE TYNE TYRE WAES WAKE WALE WALK WALL WALY WAME WAND WANE WANK WANS WANT WANY WARD WARE WARK WARM WARN\n", + "WARP WARS WART WARY WASP WAST WATS WATT WEAK WEAL WEAN WEED WEEK WEEL WEEN WEEP WEES WEET WELD WELL WELT WEND\n", + "WENS WENT WERE WERT WEST WETS WHAM WHAP WHAT WHEE WHEN WHET WHEY WHOM WHOP WILD WILE WILL WILT WILY WIMP WIND\n", + "WINE WINK WINS WINY WIRE WIRY WISE WISP WISS WIST WITE WITS WOAD WOES WOKE WOKS WOLD WONK WONS WONT WOOD WOOL\n", + "WOOS WORD WORE WORK WORM WORN WORT WOST WOTS WRAP WREN WUSS WYES WYLE WYND WYNN WYNS WYTE\n" ] } ], @@ -394,43 +345,166 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# How Secure is WordLock?" + "# Aside: How Secure is WordLock?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "The lock makes 1118 words (according to my word list). You might say that an attacker who knows the combination is a word would find this lock to be only 11.18% as secure as a 4-digit lock with 10,000 combinations. But in reality, every cable lock is [vulnerable](https://www.sfbike.org/news/video-how-to-lock-your-bike/) to an attacker with wire cutters, or with a knowledge of lock-picking, so security is equally poor for WordLock® and for an equivalent lock with numbers. (You should use a hardened steel U-lock instead.)" + "The WordLock® makes 1,118 words. You might say that an attacker would find this lock to be only 11.18% as secure as a 4-digit lock with 10,000 combinations. But in reality, every cable lock is [vulnerable](https://www.sfbike.org/news/video-how-to-lock-your-bike/) to an attacker who possesses wire cutters or a knowledge of lock-picking, so security is equally terrible for WordLock® and for an equivalent lock with digits instead of letters. You really should use a hardened steel U-lock instead." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "# Baseline: Random Locks" + "# Question 2: Can a lock with different letters on the discs make more words?" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Question 2 asks if a different lock can make more words. As a baseline, before we get to improved locks, I will start with completely random locks, as produced by the function `random_lock`. I use `t=4` to say that by default there are 4 tumblers, and `c=10` to indicate 10 letters on each tumbler (\"c\" for \"circumference\"). I give `random_lock` an argument to seed the random number generator; that makes calls repeatable if desired." + "To make a lock with more words than the original WordLock®, the simplest thing I could think of is a [greedy algorithm](https://en.wikipedia.org/wiki/Greedy_algorithm):\n", + "1) Consider each of the 4 discs, one at a time.\n", + "2) Fill each disc with the 10 most common letters (across all possible words) that appear at that position.\n", + "4) When all 4 discs have been filled, we have a lock.\n", + "\n", + "How do we choose the 10 most common letters? The `Counter.most_common` method can do most of the work:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'SPTBDCLMAR'" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "def most_common(words, position, n=LETTERS) -> str:\n", + " \"\"\"The `n` most common letters in `position` of all the `words`.\"\"\"\n", + " counter = Counter(word[position] for word in words)\n", + " return cat(letter for (letter, count) in counter.most_common(n))\n", + " \n", + "most_common(WORDS, 0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In other words, the ten most common letters from the first position of all the words are SPTBDCLMAR, in that order.\n", + "\n", + "The function `greedy_lock` creates a lock with this greedy disc-filling approach. I'm not sure if the best order of discs is left-to-right or right-to-left or something else, so I'll leave the order as a parameter. Order matters because when we choose the 10 letters for one disc, we count \"across all possible words\"; we eliminate any impossible words that don't match one of those 10 letters before we move on to the next disc." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "def greedy_lock(words=WORDS, order=range(DISCS)) -> Lock:\n", + " \"\"\"Make a lock where we greedily choose the LETTERS best letters for each disc, in order.\"\"\"\n", + " lock = DISCS * [Disc()] # Initially a lock of 4 empty discs\n", + " for i in order: \n", + " # Make lock[i] be a disc whose letters cover the most words, then update `words`\n", + " lock[i] = most_common(words, i)\n", + " words = [w for w in words if w[i] in lock[i]]\n", + " return Lock(lock)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "('SPTBDCLMAR', 'OAIEURLHYN', 'RNALEOTISM', 'SETADNLKYP')" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "greedy_lock()" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "1177" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "word_count(greedy_lock())" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "That's an improvement! The original WordLock® makes 1,118 words, so 1,177 is 5% more.\n", + "\n", + "# Greedier Algorithm\n", + "\n", + "`greedy_lock` might be better if we consider the 4 discs in some other order. Let's try all possible orders and pick the best result:" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [], + "source": [ + "def greedier_lock(words=WORDS) -> Lock:\n", + " \"\"\"Choose the best greedy lock, considering all possible orderings of discs.\"\"\"\n", + " locks = [greedy_lock(words, order) for order in itertools.permutations(range(DISCS))]\n", + " return max(locks, key=word_count)" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "('BPTCMSDLGW', 'OAEIURLYHW', 'EARNLIOTSM', 'SETNDALKYP')" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "def random_lock(t=4, c=10, seed='seed'):\n", - " \"Make a lock by sampling randomly and uniformly from the alphabet.\"\n", - " random.seed(seed)\n", - " return Lock(Tumbler(random.sample(alphabet, c))\n", - " for i in range(t))\n", - "\n", - "alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'" + "greedier_lock()" ] }, { @@ -442,21 +516,100 @@ "name": "stdout", "output_type": "stream", "text": [ - "Lock: EBHLJPYKQR VNSYPCWBGR BJOHSCFMRP XCSLUNMQJI\n", - "Count: 15\n", - "Words: BROS BYRL EBBS EBON ECRU EGOS ENOL EPOS EROS ERRS HYMN HYPS KNOX PROM PROS\n" + "1,235 words can be made by the lock (BPTCMSDLGW OAEIURLYHW EARNLIOTSM SETNDALKYP):\n", + "\n", + "BAAL BAAS BAIL BAIT BALD BALE BALK BALL BALS BAMS BAND BANE BANK BANS BARD BARE BARK BARN BARS BASE BASK BASS\n", + "BAST BATE BATS BATT BEAD BEAK BEAN BEAT BEEN BEEP BEES BEET BELL BELS BELT BEMA BEND BENE BENS BENT BERK BEST\n", + "BETA BETS BIAS BILE BILK BILL BIMA BIND BINE BINS BINT BIOS BIRD BIRK BIRL BISE BISK BITE BITS BITT BLAE BLAT\n", + "BLED BLET BLIN BLIP BLOT BOAS BOAT BOIL BOLA BOLD BOLE BOLL BOLT BOND BONE BONK BONY BOOK BOON BOOS BOOT BORA\n", + "BORE BORK BORN BORT BOSK BOSS BOTA BOTS BOTT BRAD BRAE BRAN BRAS BRAT BRAY BREA BRED BREE BREN BRIA BRIE BRIN\n", + "BRIS BRIT BROS BULK BULL BUMP BUMS BUNA BUND BUNK BUNN BUNS BUNT BUOY BURA BURD BURL BURN BURP BURS BURY BUSK\n", + "BUSS BUST BUSY BUTE BUTS BUTT BYES BYRE BYRL BYTE CAEL CAID CAIN CALE CALK CALL CAME CAMP CAMS CANE CANS CANT\n", + "CARA CARD CARE CARK CARL CARN CARP CARS CART CASA CASE CASK CAST CATE CATS CEES CEIL CELL CELS CELT CENT CERE\n", + "CESS CETE CHAD CHAP CHAT CHAY CHIA CHID CHIN CHIP CHIS CHIT CHON CHOP CIAN CINE CION CIRE CIST CITE CITY CLAD\n", + "CLAN CLAP CLAY CLIP CLOD CLON CLOP CLOT CLOY COAL COAT COED COEN COIL COIN COLA COLD COLE COLS COLT COLY COMA\n", + "COME COMP CONE CONK CONN CONS CONY COOK COOL COON COOP COOS COOT CORA CORD CORE CORK CORN CORS CORY COSS COST\n", + "COSY COTE COTS CRAP CRED CRIS CRIT CROP CUED CUES CULL CULT CUNT CURD CURE CURL CURN CURS CURT CUSK CUSP CUSS\n", + "CUTE CUTS CWMS CYAN CYMA CYME CYST DAIS DALE DALS DAME DAMN DAMP DAMS DANA DANE DANK DANS DARE DARK DARN DART\n", + "DATA DATE DEAD DEAL DEAN DEED DEEP DEES DEET DEIL DELE DELL DELS DELT DEME DEMY DENE DENS DENT DENY DEON DERE\n", + "DESK DHAK DHAL DIAL DIED DIEL DIES DIET DILL DIME DIMS DINA DINE DINK DINS DINT DIOL DION DIRE DIRK DIRL DIRT\n", + "DISK DISS DITA DITE DITS DOAT DOES DOIT DOLE DOLL DOLS DOLT DOME DOMS DONA DONE DONS DORE DORK DORP DORS DORY\n", + "DOSE DOSS DOST DOTE DOTS DOTY DRAT DRAY DREE DREK DRIP DROP DUAD DUAL DUEL DUES DUET DUIT DULL DULY DUMA DUMP\n", + "DUNE DUNK DUNS DUNT DUOS DURA DURE DURN DUSK DUST DUTY DYAD DYED DYES DYNE GAED GAEL GAEN GAES GAIN GAIT GALA\n", + "GALE GALL GALS GAMA GAME GAMP GAMS GAMY GANE GAOL GARS GARY GASP GAST GATE GATS GEED GEEK GEES GELD GELS GELT\n", + "GEMS GENE GENS GENT GEST GETA GETS GHAT GHEE GHIS GIAN GIED GIEN GIES GILD GILL GILT GIMP GINA GINK GINS GIRD\n", + "GIRL GIRN GIRT GIST GITE GITS GLAD GLED GLEE GLEN GLEY GLIA GLOP GOAD GOAL GOAS GOAT GOES GOLD GONE GOOD GOOK\n", + "GOON GOOP GOOS GORE GORP GORY GRAD GRAN GRAT GRAY GREE GREY GRID GRIN GRIP GRIT GROK GROT GUAN GUID GULL GULP\n", + "GULS GUMS GUNK GUNS GUST GUTS GWEN GYMS GYRE LAID LAIN LALL LAMA LAME LAMP LAMS LANA LAND LANE LANK LARA LARD\n", + "LARK LARS LASE LASS LAST LATE LATS LEAD LEAK LEAL LEAN LEAP LEAS LEEK LEES LEET LEIA LEIS LELA LENA LEND LENS\n", + "LENT LEON LESS LEST LETS LIAN LIED LIEN LIES LILA LILT LILY LIMA LIME LIMN LIMP LIMY LINA LINE LINK LINN LINS\n", + "LINT LINY LION LIRA LIRE LISA LISP LIST LITE LITS LOAD LOAN LOID LOIN LOLA LOLL LONE LOOK LOON LOOP LOOS LOOT\n", + "LORD LORE LORN LORY LOSE LOSS LOST LOTA LOTS LUES LUIS LULL LUMA LUMP LUMS LUNA LUNE LUNK LUNT LUNY LURE LURK\n", + "LUST LUTE LYES LYLA LYLE LYRA LYRE LYSE MAES MAIA MAID MAIL MAIN MALE MALL MALT MAMA MANA MANE MANS MANY MARA\n", + "MARE MARK MARL MARS MART MARY MASA MASK MASS MAST MATE MATS MATT MEAD MEAL MEAN MEAT MEED MEEK MEET MELD MELL\n", + "MELS MELT MEME MEMS MEND MERE MERK MERL MESA MESS META METE MHOS MIEN MILA MILD MILE MILK MILL MILS MILT MIME\n", + "MINA MIND MINE MINK MINT MIRA MIRE MIRK MIRS MIRY MISE MISS MIST MITE MITT MITY MOAN MOAS MOAT MOIL MOLA MOLD\n", + "MOLE MOLL MOLS MOLT MOLY MOME MOMS MONA MONK MONS MONY MOOD MOOL MOON MOOS MOOT MORA MORE MORN MORS MORT MOSK\n", + "MOSS MOST MOTE MOTS MOTT MULE MULL MUMP MUMS MUNS MUON MURA MURE MURK MUSA MUSE MUSK MUSS MUST MUTE MUTS MUTT\n", + "MYLA MYNA MYRA PAID PAIK PAIL PAIN PALE PALL PALP PALS PALY PAMS PANE PANS PANT PARA PARD PARE PARK PARS PART\n", + "PASE PASS PAST PATE PATS PATY PEAK PEAL PEAN PEAS PEAT PEED PEEK PEEL PEEN PEEP PEES PEIN PELE PELT PEND PENS\n", + "PENT PEON PERE PERK PERP PERT PEST PETS PHAT PHIS PHON PHOT PIAL PIAN PIAS PIED PIES PILE PILL PILY PIMA PIMP\n", + "PINA PINE PINK PINS PINT PINY PION PIRN PISS PITA PITS PITY PLAN PLAT PLAY PLEA PLED PLIE PLOD PLOP PLOT PLOY\n", + "POET POIS POLE POLL POLS POLY POME POMP POMS POND PONE PONS PONY POOD POOL POON POOP POOS PORE PORK PORN PORT\n", + "POSE POST POSY POTS PRAT PRAY PREE PREP PREY PROA PROD PROP PROS PULA PULE PULL PULP PULS PUMA PUMP PUNA PUNK\n", + "PUNS PUNT PUNY PURE PURL PURS PUSS PUTS PUTT PYAS PYES PYIN PYRE SAAD SAID SAIL SAIN SALE SALL SALP SALS SALT\n", + "SAME SAMP SANA SAND SANE SANK SANS SARA SARD SARK SASS SATE SEAL SEAN SEAS SEAT SEED SEEK SEEL SEEN SEEP SEES\n", + "SEIS SELL SELS SEME SEND SENE SENT SERA SERE SERS SETA SETS SETT SHAD SHAE SHAT SHAY SHEA SHED SHES SHIN SHIP\n", + "SHIT SHOD SHOE SHOP SHOT SIAL SILD SILK SILL SILT SIMA SIMP SIMS SINE SINK SINS SIRE SIRS SITE SITS SLAP SLAT\n", + "SLAY SLED SLID SLIP SLIT SLOE SLOP SLOT SOAK SOAP SOIL SOLA SOLD SOLE SOLS SOMA SOME SOMS SONE SONS SOOK SOON\n", + "SOOT SORA SORD SORE SORN SORT SOTS SRIS SUED SUES SUET SUIT SULK SUMP SUMS SUNK SUNN SUNS SURA SURD SURE SUSS\n", + "SWAN SWAP SWAT SWAY SWOP SWOT SYED SYNE TAEL TAIL TAIN TALA TALE TALK TALL TAME TAMP TAMS TANK TANS TAOS TARA\n", + "TARE TARN TARP TARS TART TASK TASS TATE TATS TEAK TEAL TEAS TEAT TEED TEEL TEEN TEES TELA TELE TELL TELS TEMP\n", + "TEND TENS TENT TERN TESS TEST TETS THAE THAN THAT THEA THEE THEN THEY THIN THIS TIED TIES TILE TILL TILS TILT\n", + "TIME TINA TINE TINS TINT TINY TIRE TIRL TITS TOAD TOEA TOED TOES TOIL TOIT TOLA TOLD TOLE TOLL TOME TOMS TONE\n", + "TONS TONY TOOK TOOL TOON TOOT TORA TORE TORN TORS TORT TORY TOSS TOST TOTE TOTS TRAD TRAE TRAP TRAY TREE TREK\n", + "TRES TRET TREY TRIP TROD TROP TROT TROY TUIS TULE TUMP TUNA TUNE TUNS TURD TURK TURN TUSK TUTS TWAE TWAS TWAT\n", + "TWEE TWIN TWIT TWOS TYEE TYES TYIN TYNE TYRE WAES WAIL WAIN WAIT WALE WALK WALL WALY WAME WAND WANE WANK WANS\n", + "WANT WANY WARD WARE WARK WARN WARP WARS WART WARY WASP WAST WATS WATT WEAK WEAL WEAN WEED WEEK WEEL WEEN WEEP\n", + "WEES WEET WELD WELL WELT WEND WENS WENT WERE WERT WEST WETS WHAP WHAT WHEE WHEN WHET WHEY WHID WHIN WHIP WHIT\n", + "WHOA WHOP WILD WILE WILL WILT WILY WIMP WIND WINE WINK WINS WINY WIRE WIRY WISE WISP WISS WIST WITE WITS WOAD\n", + "WOES WOLD WONK WONS WONT WOOD WOOL WOOS WORD WORE WORK WORN WORT WOST WOTS WRAP WREN WRIT WUSS WYES WYLE WYND\n", + "WYNN WYNS WYTE\n", + "CPU times: user 33.8 ms, sys: 1.16 ms, total: 35 ms\n", + "Wall time: 34.4 ms\n" ] } ], "source": [ - "show(random_lock())" + "%time show(greedier_lock())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Wow, that's not very many words. Let's repeat 100 times and take the best one, with \"best\" meaning the maximum `word_count`:" + "That's another 5% improvement! We've done well, with only two dozen lines of code and under 50 milliseconds of run time. What else can we do?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Hillclimbing Algorithm" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The problem with the greedy algorithm is that it does no exploration: at every step it tries one thing, and if it makes a suboptimal choice of letters on one disc, there is no way to undo the choice. I'd like the option to try multiple choices on each disc. But there are too many locks to try all of them: (26 choose 10)4 ≈ 800 septillion. Even if we only chose from the 14 most common letters for each disc, rather than all 26, that would still be a trillion possible locks. So rather than systematically trying all possible locks, we're left with randomly sampling from possible locks. A process called **hillclimbing** makes random changesd and keeps the changes that help:\n", + "\n", + " 1. Start with some lock.\n", + " 2. Make a random change to some letter(s) in the lock.\n", + " 3. If the change yields a lock that makes more words, keep the change. Otherwise discard the change.\n", + " 4. Repeat multiple times.\n", + "\n", + "I'm not sure exactly how I want to make a random change, so for now I'll define `changed_lock` to change one random letter in one random disc, but I'll parameterize the function `hillclimb` to accept a different function to allow for different changes (and also to allow a different function for how to score the best lock):" ] }, { @@ -465,7 +618,32 @@ "metadata": {}, "outputs": [], "source": [ - "def word_count(lock): return len(words_from(lock))" + "def changed_lock(lock) -> Lock: \n", + " \"\"\"Change one random letter in one random disc in the lock.\"\"\"\n", + " # Make a mutable copy of lock. Then change lock2[i]'s `old` letter to a `new` one.\n", + " lock2 = list(lock) \n", + " i = random.randrange(DISCS)\n", + " old: Letter = random.choice(lock[i])\n", + " new: Letter = random.choice([L for L in ALPHABET if L not in lock[i]])\n", + " lock2[i] = lock2[i].replace(old, new)\n", + " return Lock(lock2)\n", + "\n", + "def hillclimb(lock, changer=changed_lock, scorer=word_count, repeat=4000) -> Lock:\n", + " \"\"\"Starting with `lock`, apply `changer` to make a new lock, keeping it if\n", + " `scorer` rates it as better than the previous best lock. Repeat.\"\"\"\n", + " best, best_score = lock, scorer(lock)\n", + " for _ in range(repeat):\n", + " candidate = changer(best)\n", + " if scorer(candidate) >= best_score:\n", + " best, best_score = candidate, scorer(candidate)\n", + " return best" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's see how hillclimbing does:" ] }, { @@ -477,125 +655,175 @@ "name": "stdout", "output_type": "stream", "text": [ - "Lock: IDQPSFOJCW MQYGODAINT NLRZPXWFBI STGWCOLJEH\n", - "Count: 259\n", - "Words: CABS CAFE CALE CALL CALO CANE CANS CANT CAPE CAPH CAPO CAPS CARE CARL CARS CART CAWS CINE CIRE COBS COFT COIL COLE COLS COLT CONE CONS COPE COPS CORE CORS COWL COWS DABS DAFT DAIS DALE DALS DANE DANG DANS DAPS DARE DART DAWS DAWT DAZE DIBS DIFS DILL DINE DING DINO DINS DINT DIPS DIPT DIRE DIRL DIRT DOIT DOLE DOLL DOLS DOLT DONE DONG DONS DOPE DORE DORS DOWS DOZE DYNE FABS FAIL FALL FANE FANG FANO FANS FARE FARL FARO FART FAZE FIBS FIFE FILE FILL FILO FILS FINE FINO FINS FIRE FIRS FIXT FOBS FOIL FONS FONT FOPS FORE FORT FOWL IDLE IMPS INFO INNS INRO IONS JABS JAIL JANE JAPE JARL JARS JAWS JIBE JIBS JILL JILT JINS JOBS JOLE JOLT JOWL JOWS OAFS OARS ODIC OGLE OGRE OILS OMIT OOPS OOZE OTIC OTIS PAIL PALE PALL PALS PANE PANG PANS PANT PAPS PARE PARS PART PAWL PAWS PILE PILL PINE PING PINS PINT PIPE PIPS POIS POLE POLL POLO POLS PONE PONG PONS POPE POPS PORE PORT POWS PYIC PYRE PYRO QOPH SABE SABS SAFE SAIL SALE SALL SALS SALT SANE SANG SANS SAPS SAWS SIBS SIFT SILL SILO SILT SINE SING SINH SINS SIPE SIPS SIRE SIRS SIZE SMIT SNIT SOBS SOFT SOIL SOLE SOLO SOLS SONE SONG SONS SOPH SOPS SORE SORT SOWS SYBO SYNC SYNE SYPH WABS WAFT WAIL WAIT WALE WALL WANE WANS WANT WAPS WARE WARS WART WAWL WAWS WIFE WILE WILL WILT WINE WING WINO WINS WIPE WIRE WONS WONT WOPS WORE WORT WOWS WYLE WYNS\n" + "1,240 words can be made by the lock (SPWMTFDLCB LEYHPRUOAI ENCLRTAOSI DSNHAYLKTE):\n", + "\n", + "BAAL BAAS BACH BACK BAIL BAIT BALD BALE BALK BALL BALS BAND BANE BANK BANS BARD BARE BARK BARN BARS BASE BASH\n", + "BASK BASS BAST BATE BATH BATS BATT BEAD BEAK BEAN BEAT BECK BEEN BEES BEET BELL BELS BELT BEND BENE BENS BENT\n", + "BERK BEST BETA BETH BETS BIAS BICE BILE BILK BILL BIND BINE BINS BINT BIOS BIRD BIRK BIRL BISE BISK BITE BITS\n", + "BITT BLAE BLAH BLAT BLED BLET BLIN BLOT BOAS BOAT BOCK BOIL BOLA BOLD BOLE BOLL BOLT BOND BONE BONK BONY BOOK\n", + "BOON BOOS BOOT BORA BORE BORK BORN BORT BOSH BOSK BOSS BOTA BOTH BOTS BOTT BRAD BRAE BRAN BRAS BRAT BRAY BREA\n", + "BRED BREE BREN BRIA BRIE BRIN BRIS BRIT BROS BUCK BULK BULL BUNA BUND BUNK BUNN BUNS BUNT BUOY BURA BURD BURL\n", + "BURN BURS BURY BUSH BUSK BUSS BUST BUSY BUTE BUTS BUTT BYES BYRE BYRL BYTE CACA CAEL CAID CAIN CALE CALK CALL\n", + "CANE CANS CANT CARA CARD CARE CARK CARL CARN CARS CART CASA CASE CASH CASK CAST CATE CATS CECA CEES CEIL CELL\n", + "CELS CELT CENT CERE CESS CETE CHAD CHAT CHAY CHIA CHID CHIN CHIS CHIT CHON CIAN CINE CION CIRE CIST CITE CITY\n", + "CLAD CLAN CLAY CLOD CLON CLOT CLOY COAL COAT COCA COCK COED COEN COIL COIN COLA COLD COLE COLS COLT COLY CONE\n", + "CONK CONN CONS CONY COOK COOL COON COOS COOT CORA CORD CORE CORK CORN CORS CORY COSH COSS COST COSY COTE COTS\n", + "CRED CRIS CRIT CUED CUES CULL CULT CUNT CURD CURE CURL CURN CURS CURT CUSK CUSS CUTE CUTS CYAN CYST DACE DAIS\n", + "DALE DALS DANA DANE DANK DANS DARE DARK DARN DART DASH DATA DATE DEAD DEAL DEAN DECK DEED DEES DEET DEIL DELE\n", + "DELL DELS DELT DENE DENS DENT DENY DEON DERE DESK DHAK DHAL DIAL DICE DICK DIED DIEL DIES DIET DILL DINA DINE\n", + "DINK DINS DINT DIOL DION DIRE DIRK DIRL DIRT DISH DISK DISS DITA DITE DITS DOAT DOCK DOCS DOES DOIT DOLE DOLL\n", + "DOLS DOLT DONA DONE DONS DORE DORK DORS DORY DOSE DOSS DOST DOTE DOTH DOTS DOTY DRAT DRAY DREE DREK DUAD DUAL\n", + "DUCE DUCK DUCT DUEL DUES DUET DUIT DULL DULY DUNE DUNK DUNS DUNT DUOS DURA DURE DURN DUSK DUST DUTY DYAD DYED\n", + "DYES DYNE FACE FACT FAIL FAIN FALL FANE FANS FARD FARE FARL FART FASH FAST FATE FATS FEAL FEAT FECK FEED FEEL\n", + "FEES FEET FELL FELT FEND FENS FEOD FERE FERN FESS FEST FETA FETE FETS FIAT FICE FILA FILE FILL FILS FIND FINE\n", + "FINK FINN FINS FIRE FIRN FIRS FISH FIST FITS FLAK FLAN FLAT FLAY FLEA FLED FLEE FLEY FLIT FLOE FOAL FOES FOIL\n", + "FOIN FOLD FOLK FOND FONS FONT FOOD FOOL FOOT FORA FORD FORE FORK FORT FOSS FRAE FRAT FRAY FRED FREE FRET FRIT\n", + "FROE FUCK FUEL FULL FUND FUNK FUNS FURL FURS FURY FUSE FUSS FYCE LACE LACK LACS LACY LAID LAIN LALL LANA LAND\n", + "LANE LANK LARA LARD LARK LARS LASE LASH LASS LAST LATE LATH LATS LEAD LEAH LEAK LEAL LEAN LEAS LECH LEEK LEES\n", + "LEET LEIA LEIS LELA LENA LEND LENS LENT LEON LESS LEST LETS LIAN LICE LICH LICK LIED LIEN LIES LILA LILT LILY\n", + "LINA LINE LINK LINN LINS LINT LINY LION LIRA LIRE LISA LIST LITE LITS LOAD LOAN LOCA LOCH LOCK LOID LOIN LOLA\n", + "LOLL LONE LOOK LOON LOOS LOOT LORD LORE LORN LORY LOSE LOSS LOST LOTA LOTH LOTS LUCA LUCE LUCK LUCY LUES LUIS\n", + "LULL LUNA LUNE LUNK LUNT LUNY LURE LURK LUSH LUST LUTE LYCH LYES LYLA LYLE LYRA LYRE LYSE MACE MACH MACK MACS\n", + "MACY MAES MAIA MAID MAIL MAIN MALE MALL MALT MANA MANE MANS MANY MARA MARE MARK MARL MARS MART MARY MASA MASH\n", + "MASK MASS MAST MATE MATH MATS MATT MEAD MEAL MEAN MEAT MEED MEEK MEET MELD MELL MELS MELT MEND MERE MERK MERL\n", + "MESA MESH MESS META METE METH MHOS MIAH MICA MICE MICK MICS MIEN MILA MILD MILE MILK MILL MILS MILT MINA MIND\n", + "MINE MINK MINT MIRA MIRE MIRK MIRS MIRY MISE MISS MIST MITE MITT MITY MOAN MOAS MOAT MOCK MOCS MOIL MOLA MOLD\n", + "MOLE MOLL MOLS MOLT MOLY MONA MONK MONS MONY MOOD MOOL MOON MOOS MOOT MORA MORE MORN MORS MORT MOSH MOSK MOSS\n", + "MOST MOTE MOTH MOTS MOTT MUCH MUCK MULE MULL MUNS MUON MURA MURE MURK MUSA MUSE MUSH MUSK MUSS MUST MUTE MUTS\n", + "MUTT MYAH MYCS MYLA MYNA MYRA MYTH PACA PACE PACK PACS PACT PACY PAID PAIK PAIL PAIN PALE PALL PALS PALY PANE\n", + "PANS PANT PARA PARD PARE PARK PARS PART PASE PASH PASS PAST PATE PATH PATS PATY PEAK PEAL PEAN PEAS PEAT PECH\n", + "PECK PECS PEED PEEK PEEL PEEN PEES PEIN PELE PELT PEND PENS PENT PEON PERE PERK PERT PEST PETS PHAT PHIS PHON\n", + "PHOT PIAL PIAN PIAS PICA PICE PICK PICS PIED PIES PILE PILL PILY PINA PINE PINK PINS PINT PINY PION PIRN PISH\n", + "PISS PITA PITH PITS PITY PLAN PLAT PLAY PLEA PLED PLIE PLOD PLOT PLOY POCK POET POIS POLE POLL POLS POLY POND\n", + "PONE PONS PONY POOD POOH POOL POON POOS PORE PORK PORN PORT POSE POSH POST POSY POTS PRAT PRAY PREE PREY PROA\n", + "PROD PROS PUCE PUCK PULA PULE PULL PULS PUNA PUNK PUNS PUNT PUNY PURE PURL PURS PUSH PUSS PUTS PUTT PYAS PYES\n", + "PYIN PYRE SAAD SACK SACS SAID SAIL SAIN SALE SALL SALS SALT SANA SAND SANE SANK SANS SARA SARD SARK SASH SASS\n", + "SATE SEAL SEAN SEAS SEAT SECS SECT SEED SEEK SEEL SEEN SEES SEIS SELL SELS SEND SENE SENT SERA SERE SERS SETA\n", + "SETH SETS SETT SHAD SHAE SHAH SHAT SHAY SHEA SHED SHES SHIN SHIT SHOD SHOE SHOT SIAL SICE SICK SICS SILD SILK\n", + "SILL SILT SINE SINH SINK SINS SIRE SIRS SITE SITH SITS SLAT SLAY SLED SLID SLIT SLOE SLOT SOAK SOCA SOCK SOIL\n", + "SOLA SOLD SOLE SOLS SONE SONS SOOK SOON SOOT SORA SORD SORE SORN SORT SOTH SOTS SPAE SPAN SPAS SPAT SPAY SPED\n", + "SPIK SPIN SPIT SPOT SPRY SRIS SUCH SUCK SUED SUES SUET SUIT SULK SUNK SUNN SUNS SURA SURD SURE SUSS SYCE SYED\n", + "SYNE TACE TACH TACK TACT TAEL TAIL TAIN TALA TALE TALK TALL TANK TANS TAOS TARA TARE TARN TARS TART TASK TASS\n", + "TATE TATS TEAK TEAL TEAS TEAT TECH TEED TEEL TEEN TEES TELA TELE TELL TELS TEND TENS TENT TERN TESS TEST TETH\n", + "TETS THAE THAN THAT THEA THEE THEN THEY THIN THIS TICK TICS TIED TIES TILE TILL TILS TILT TINA TINE TINS TINT\n", + "TINY TIRE TIRL TITS TOAD TOEA TOED TOES TOIL TOIT TOLA TOLD TOLE TOLL TONE TONS TONY TOOK TOOL TOON TOOT TORA\n", + "TORE TORN TORS TORT TORY TOSH TOSS TOST TOTE TOTS TRAD TRAE TRAY TREE TREK TRES TRET TREY TROD TROT TROY TUCK\n", + "TUIS TULE TUNA TUNE TUNS TURD TURK TURN TUSH TUSK TUTS TYCE TYEE TYES TYIN TYNE TYRE WACK WAES WAIL WAIN WAIT\n", + "WALE WALK WALL WALY WAND WANE WANK WANS WANT WANY WARD WARE WARK WARN WARS WART WARY WASH WAST WATS WATT WEAK\n", + "WEAL WEAN WEED WEEK WEEL WEEN WEES WEET WELD WELL WELT WEND WENS WENT WERE WERT WEST WETS WHAT WHEE WHEN WHET\n", + "WHEY WHID WHIN WHIT WHOA WICH WICK WILD WILE WILL WILT WILY WIND WINE WINK WINS WINY WIRE WIRY WISE WISH WISS\n", + "WIST WITE WITH WITS WOAD WOES WOLD WONK WONS WONT WOOD WOOL WOOS WORD WORE WORK WORN WORT WOST WOTS WREN WRIT\n", + "WUSS WYCH WYES WYLE WYND WYNN WYNS WYTE\n" ] } ], "source": [ - "random_locks = [random_lock(seed=i) for i in range(100)]\n", + "show(hillclimb(wordlock))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We got up to 1240 words, another improvement! But can we go beyond that? \n", "\n", - "show(max(random_locks, key=word_count))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Still not very good. We will need a more systematic approach." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Question 2: More Words (via Greedy Lock)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "My first idea for a lock with more words is this: consider each tumbler, one at a time, and fill the tumbler with the letters that make the most words. How do I determine what letters make the most words? A `Counter` does most of the work; we feed it a list of the first letter of each word, and then ask it for the ten most common letters (and their counts): " + "I'll create a list of 40 `locks`: the original wordlock, 15 random locks, and 24 greedy locks, one for each permutation of the orders." ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[('S', 373),\n", - " ('P', 268),\n", - " ('T', 268),\n", - " ('B', 267),\n", - " ('D', 251),\n", - " ('C', 248),\n", - " ('L', 246),\n", - " ('M', 239),\n", - " ('A', 236),\n", - " ('R', 203)]" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "first_letters = [w[0] for w in WORDS]\n", + "def random_lock() -> Lock:\n", + " \"\"\"A lock with randomly-chosen letters.\"\"\"\n", + " return Lock(cat(random.sample(ALPHABET, LETTERS)) for dial in range(DISCS))\n", "\n", - "Counter(first_letters).most_common(10)" + "locks = ([wordlock] + [random_lock() for _ in range(15)] +\n", + " [greedy_lock(WORDS, order) for order in itertools.permutations(range(DISCS))])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "In other words, the letters SPTBDCLMAR are the most common ways to start a word. Let's add up those counts:" + "I'll also define `lock_table`, a little function to summarize the locks and their word counts:" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "2599" - ] - }, - "execution_count": 21, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "def n_most_common(counter, n): return sum(n for (_, n) in counter.most_common(n))\n", - "\n", - "n_most_common(Counter(first_letters), 10)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "This means that SPTBDCLMAR covers 2,599 words. We don't know for sure that these are the best 10 letters to put on the first tumbler, but we do know that whatever letters are best, they can't form more than 2,599 words, so we have an upper bound on the number of words in a lock (and the 1,118 from `wordlock` is a lower bound).\n", - "\n", - "What letters should we put on the second tumbler? We will do the same thing, but this time don't consider *all* the words in the dictionary; just consider the 2,599 words that start with one of the ten letters on the first tumbler. Continue this way until we fill in all four tumblers. This is called a *greedy* approach, because when we consider each tumbler, we pick the solution that looks best right then, for that tumbler, without consideration for future tumblers." + "def lock_table(locks) -> dict: return {lock: word_count(lock) for lock in locks}" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "{('SPHMTWDLFB', 'LEYHNRUOAI', 'ENMLRTAOSK', 'DSNMPYLKTE'): 1118,\n", + " ('YAHIQUMEOG', 'KJCXLTRQAB', 'VPTECDBKXO', 'BUVTFXKGSW'): 80,\n", + " ('XOTNFCBIVS', 'ARWTHKDEJU', 'CKYQDFVWLO', 'TEMADLHYRX'): 115,\n", + " ('RFGLIXZAED', 'JEOWUVCYIM', 'OCTAVRYISU', 'YOMZCTEJPL'): 178,\n", + " ('CTIVPWDJXZ', 'REKMFSCJAT', 'KJXHUODPNB', 'IWJUGRZBOC'): 35,\n", + " ('ILOAHSFKZX', 'XIECBRHAMN', 'SYGRQUPJNZ', 'BMVRKSYCAG'): 142,\n", + " ('BRVNFOWYLH', 'MWTQHZSFCY', 'TQPGYZXOUK', 'NVXZHWBFUY'): 0,\n", + " ('KRDMTSBZIA', 'FKJAZYNTPW', 'TWXFCGAJPI', 'XLVOQFHGMN'): 73,\n", + " ('IKTENRBPSV', 'WFKYQLPJBI', 'IYCFXQUOHW', 'QAKDYFOTMG'): 56,\n", + " ('INOEVWXSLB', 'ZYXOIWQNSL', 'NUDFMAICPR', 'YNVIZALGWR'): 105,\n", + " ('DSKRBQEYXZ', 'CPEHRBAXOM', 'QRUNSTMCYV', 'AYDPKVSBIL'): 224,\n", + " ('RFICNAQEUL', 'YZQPASNLBO', 'QCDZWIONHT', 'TOFMNPKXYW'): 96,\n", + " ('AKMQVSLOHP', 'VWESKTPMLD', 'INGSRTFPAL', 'WKRIAMFSOY'): 143,\n", + " ('OBHSIZJAGW', 'BYRENOZUFV', 'DRUWEYOVHK', 'DPNLWIABTC'): 146,\n", + " ('SIXTNVFQJD', 'TODHWGKILE', 'RKGLUNAJDM', 'CVNHLGIDQF'): 111,\n", + " ('KHABFQXWRC', 'HVTEFKRUWS', 'HABDOQWGRK', 'VYNJQMWZER'): 76,\n", + " ('SPTBDCLMAR', 'OAIEURLHYN', 'RNALEOTISM', 'SETADNLKYP'): 1177,\n", + " ('SPTBDCLMAR', 'OAIEURLHYN', 'RNLEATSCIO', 'SETAYKDNLO'): 1178,\n", + " ('SPTBDCLMAR', 'OAEIURLHYW', 'ARNELOITSM', 'SETDANLKYP'): 1181,\n", + " ('SPTBDCLMAR', 'OAEIURLHYW', 'ARNELOITSM', 'SETDNALKYP'): 1181,\n", + " ('SPTBDCLMAR', 'OAIEURLHYN', 'RNLEATSCIO', 'SETAYNDKLO'): 1178,\n", + " ('SPTBDCLMAR', 'OAIEURHYLW', 'RNLEAITSOC', 'SETAYNDKLO'): 1181,\n", + " ('SBPTDCLMRG', 'AOIEURLHYN', 'RNALEOTISM', 'SETDNALKPY'): 1216,\n", + " ('SBPTDCLMRG', 'AOIEURLHYN', 'NRLEATSCIO', 'SETAKDYNLO'): 1206,\n", + " ('BPSCTMDLGF', 'AOIEURLHYN', 'RNLEAITOSM', 'SETDLNAKPY'): 1222,\n", + " ('BPCMSTDLGW', 'AOIEURLHYN', 'RNLEAITOSM', 'SETDANLKYP'): 1226,\n", + " ('BMPSDTLCRH', 'AOIEURLHYN', 'NRLEASTCIM', 'SETAKDNYLO'): 1207,\n", + " ('BMPTDCSLFR', 'AOIEURLHYN', 'NRLEATSICD', 'SETAKDNYLO'): 1208,\n", + " ('SBPTCAMDLG', 'OAEIURLHYW', 'EARNLIOTSM', 'SETDNALKYP'): 1191,\n", + " ('SBPTCAMDLG', 'OAIEURLHYW', 'EARNLIOTSM', 'SETNDALKYP'): 1191,\n", + " ('BPSCTMDLGF', 'OAEIURLHYN', 'EARNLIOTSM', 'SETDLNAKPY'): 1222,\n", + " ('BPCMSTDLGW', 'OAEIURLHYN', 'EARNLIOTSM', 'SETDANLKYP'): 1226,\n", + " ('SPBATCMDLG', 'OAIEURLHYW', 'EARNLIOTSM', 'SETNDALKYP'): 1191,\n", + " ('BPTCMSDLGW', 'OAEIURLYHW', 'EARNLIOTSM', 'SETNDALKYP'): 1235,\n", + " ('STAPBMDLCR', 'OAIEURLHYN', 'RNLEATSCIO', 'SETANDYLKO'): 1178,\n", + " ('STAPBMDLCR', 'OAIEURHYLW', 'RNLEAITSOC', 'SETANDYLKO'): 1181,\n", + " ('BMPSDTLCRH', 'AOIEURLYHN', 'NRLEASTCIM', 'SETANDYLKO'): 1207,\n", + " ('BMPTDCSLFR', 'AOIEURLYHN', 'NRLEATSICD', 'SETANDYLKO'): 1208,\n", + " ('SBAPTMCDLF', 'OAIEURLHYW', 'NRELAITSOD', 'SETANDYLKO'): 1168,\n", + " ('BPMTCSDLFW', 'OAIEURLYHN', 'NRELAITSOD', 'SETANDYLKO'): 1197}" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "def greedy_lock(t=4, c=10, words=WORDS):\n", - " \"Make a lock with t tumblers, each consisting of c letters covering the most words.\"\n", - " lock = Lock()\n", - " for i in range(t):\n", - " # Make a tumbler of c letters, such that the tumbler covers the most words.\n", - " # Then update words to only include the ones that can be made with this tumbler\n", - " counter = Counter(word[i] for word in words)\n", - " tumbler = Tumbler(L for (L, _) in counter.most_common(c))\n", - " words = {w for w in words if w[i] in tumbler}\n", - " lock.append(tumbler)\n", - " return lock" + "lock_table(locks)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now I'll do hillclimbing from each lock and display the results:" ] }, { @@ -607,76 +835,79 @@ "name": "stdout", "output_type": "stream", "text": [ - "Lock: SPTBDCLMAR OAIEURLHYN RNALEOTISM SETADNLKYP\n", - "Count: 1177\n", - "Words: AALS AEON AERY AHED AHIS AHOY AILA AILS AIMS AINS AIRN AIRS AIRT AIRY AITS ALAE ALAN ALAS ALEA ALEE ALEK ALES ALIA ALIT ALLS ALLY ALMA ALME ALMS ALOE ALTS ANAL ANAS ANAY ANES ANIL ANIS ANNA ANNE ANOA ANON ANSA ANTA ANTE ANTS ARAK AREA ARES ARIA ARID ARIE ARIL ARMS ARMY ARON ARSE ARTS ARTY AULD AUNT AURA AYAN AYES AYIN AYLA BAAL BAAS BAIL BAIT BALD BALE BALK BALL BALS BAMS BAND BANE BANK BANS BARD BARE BARK BARN BARS BASE BASK BASS BAST BATE BATS BATT BEAD BEAK BEAN BEAT BEEN BEEP BEES BEET BELL BELS BELT BEMA BEND BENE BENS BENT BERK BEST BETA BETS BIAS BILE BILK BILL BIMA BIND BINE BINS BINT BIOS BIRD BIRK BIRL BISE BISK BITE BITS BITT BLAE BLAT BLED BLET BLIN BLIP BLOT BOAS BOAT BOIL BOLA BOLD BOLE BOLL BOLT BOND BONE BONK BONY BOOK BOON BOOS BOOT BORA BORE BORK BORN BORT BOSK BOSS BOTA BOTS BOTT BRAD BRAE BRAN BRAS BRAT BRAY BREA BRED BREE BREN BRIA BRIE BRIN BRIS BRIT BROS BULK BULL BUMP BUMS BUNA BUND BUNK BUNN BUNS BUNT BUOY BURA BURD BURL BURN BURP BURS BURY BUSK BUSS BUST BUSY BUTE BUTS BUTT BYES BYRE BYRL BYTE CAEL CAID CAIN CALE CALK CALL CAME CAMP CAMS CANE CANS CANT CARA CARD CARE CARK CARL CARN CARP CARS CART CASA CASE CASK CAST CATE CATS CEES CEIL CELL CELS CELT CENT CERE CESS CETE CHAD CHAP CHAT CHAY CHIA CHID CHIN CHIP CHIS CHIT CHON CHOP CIAN CINE CION CIRE CIST CITE CITY CLAD CLAN CLAP CLAY CLIP CLOD CLON CLOP CLOT CLOY COAL COAT COED COEN COIL COIN COLA COLD COLE COLS COLT COLY COMA COME COMP CONE CONK CONN CONS CONY COOK COOL COON COOP COOS COOT CORA CORD CORE CORK CORN CORS CORY COSS COST COSY COTE COTS CRAP CRED CRIS CRIT CROP CUED CUES CULL CULT CUNT CURD CURE CURL CURN CURS CURT CUSK CUSP CUSS CUTE CUTS CYAN CYMA CYME CYST DAIS DALE DALS DAME DAMN DAMP DAMS DANA DANE DANK DANS DARE DARK DARN DART DATA DATE DEAD DEAL DEAN DEED DEEP DEES DEET DEIL DELE DELL DELS DELT DEME DEMY DENE DENS DENT DENY DEON DERE DESK DHAK DHAL DIAL DIED DIEL DIES DIET DILL DIME DIMS DINA DINE DINK DINS DINT DIOL DION DIRE DIRK DIRL DIRT DISK DISS DITA DITE DITS DOAT DOES DOIT DOLE DOLL DOLS DOLT DOME DOMS DONA DONE DONS DORE DORK DORP DORS DORY DOSE DOSS DOST DOTE DOTS DOTY DRAT DRAY DREE DREK DRIP DROP DUAD DUAL DUEL DUES DUET DUIT DULL DULY DUMA DUMP DUNE DUNK DUNS DUNT DUOS DURA DURE DURN DUSK DUST DUTY DYAD DYED DYES DYNE LAID LAIN LALL LAMA LAME LAMP LAMS LANA LAND LANE LANK LARA LARD LARK LARS LASE LASS LAST LATE LATS LEAD LEAK LEAL LEAN LEAP LEAS LEEK LEES LEET LEIA LEIS LELA LENA LEND LENS LENT LEON LESS LEST LETS LIAN LIED LIEN LIES LILA LILT LILY LIMA LIME LIMN LIMP LIMY LINA LINE LINK LINN LINS LINT LINY LION LIRA LIRE LISA LISP LIST LITE LITS LOAD LOAN LOID LOIN LOLA LOLL LONE LOOK LOON LOOP LOOS LOOT LORD LORE LORN LORY LOSE LOSS LOST LOTA LOTS LUES LUIS LULL LUMA LUMP LUMS LUNA LUNE LUNK LUNT LUNY LURE LURK LUST LUTE LYES LYLA LYLE LYRA LYRE LYSE MAES MAIA MAID MAIL MAIN MALE MALL MALT MAMA MANA MANE MANS MANY MARA MARE MARK MARL MARS MART MARY MASA MASK MASS MAST MATE MATS MATT MEAD MEAL MEAN MEAT MEED MEEK MEET MELD MELL MELS MELT MEME MEMS MEND MERE MERK MERL MESA MESS META METE MHOS MIEN MILA MILD MILE MILK MILL MILS MILT MIME MINA MIND MINE MINK MINT MIRA MIRE MIRK MIRS MIRY MISE MISS MIST MITE MITT MITY MOAN MOAS MOAT MOIL MOLA MOLD MOLE MOLL MOLS MOLT MOLY MOME MOMS MONA MONK MONS MONY MOOD MOOL MOON MOOS MOOT MORA MORE MORN MORS MORT MOSK MOSS MOST MOTE MOTS MOTT MULE MULL MUMP MUMS MUNS MUON MURA MURE MURK MUSA MUSE MUSK MUSS MUST MUTE MUTS MUTT MYLA MYNA MYRA PAID PAIK PAIL PAIN PALE PALL PALP PALS PALY PAMS PANE PANS PANT PARA PARD PARE PARK PARS PART PASE PASS PAST PATE PATS PATY PEAK PEAL PEAN PEAS PEAT PEED PEEK PEEL PEEN PEEP PEES PEIN PELE PELT PEND PENS PENT PEON PERE PERK PERP PERT PEST PETS PHAT PHIS PHON PHOT PIAL PIAN PIAS PIED PIES PILE PILL PILY PIMA PIMP PINA PINE PINK PINS PINT PINY PION PIRN PISS PITA PITS PITY PLAN PLAT PLAY PLEA PLED PLIE PLOD PLOP PLOT PLOY POET POIS POLE POLL POLS POLY POME POMP POMS POND PONE PONS PONY POOD POOL POON POOP POOS PORE PORK PORN PORT POSE POST POSY POTS PRAT PRAY PREE PREP PREY PROA PROD PROP PROS PULA PULE PULL PULP PULS PUMA PUMP PUNA PUNK PUNS PUNT PUNY PURE PURL PURS PUSS PUTS PUTT PYAS PYES PYIN PYRE RAIA RAID RAIL RAIN RAIS RALE RAMP RAMS RAND RANK RANT RARE RASE RASP RATE RATS READ REAL REAP REED REEK REEL REES REID REIN REIS RELY REMS REMY REND RENE RENT REST RETE RETS RHEA RHOS RIAL RIAN RIAS RIEL RILE RILL RIME RIMS RIMY RIND RINK RINS RIOT RISE RISK RITA RITE ROAD ROAN ROES ROIL ROLE ROLL ROME ROMP ROMS ROOD ROOK ROOT RORY ROSA ROSE ROSS ROSY ROTA ROTE ROTL ROTS RUED RUES RUIN RULE RULY RUMP RUMS RUNE RUNS RUNT RUSE RUSK RUST RUTS RYAN RYAS RYES RYND RYOT SAAD SAID SAIL SAIN SALE SALL SALP SALS SALT SAME SAMP SANA SAND SANE SANK SANS SARA SARD SARK SASS SATE SEAL SEAN SEAS SEAT SEED SEEK SEEL SEEN SEEP SEES SEIS SELL SELS SEME SEND SENE SENT SERA SERE SERS SETA SETS SETT SHAD SHAE SHAT SHAY SHEA SHED SHES SHIN SHIP SHIT SHOD SHOE SHOP SHOT SIAL SILD SILK SILL SILT SIMA SIMP SIMS SINE SINK SINS SIRE SIRS SITE SITS SLAP SLAT SLAY SLED SLID SLIP SLIT SLOE SLOP SLOT SNAP SNED SNIP SNIT SNOT SOAK SOAP SOIL SOLA SOLD SOLE SOLS SOMA SOME SOMS SONE SONS SOOK SOON SOOT SORA SORD SORE SORN SORT SOTS SRIS SUED SUES SUET SUIT SULK SUMP SUMS SUNK SUNN SUNS SURA SURD SURE SUSS SYED SYNE TAEL TAIL TAIN TALA TALE TALK TALL TAME TAMP TAMS TANK TANS TAOS TARA TARE TARN TARP TARS TART TASK TASS TATE TATS TEAK TEAL TEAS TEAT TEED TEEL TEEN TEES TELA TELE TELL TELS TEMP TEND TENS TENT TERN TESS TEST TETS THAE THAN THAT THEA THEE THEN THEY THIN THIS TIED TIES TILE TILL TILS TILT TIME TINA TINE TINS TINT TINY TIRE TIRL TITS TOAD TOEA TOED TOES TOIL TOIT TOLA TOLD TOLE TOLL TOME TOMS TONE TONS TONY TOOK TOOL TOON TOOT TORA TORE TORN TORS TORT TORY TOSS TOST TOTE TOTS TRAD TRAE TRAP TRAY TREE TREK TRES TRET TREY TRIP TROD TROP TROT TROY TUIS TULE TUMP TUNA TUNE TUNS TURD TURK TURN TUSK TUTS TYEE TYES TYIN TYNE TYRE\n" + "CPU times: user 30.9 s, sys: 92.6 ms, total: 31 s\n", + "Wall time: 31.3 s\n" ] } ], "source": [ - "show(greedy_lock())" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Remember that the `wordlock` gave 1118 words, so the greedy lock with 1177 is better, but only by 5%. Is it possible to do better still? " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Question 2: More Words (via Improved Locks)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Here's another idea to get more words from a lock:\n", - "\n", - "1. Start with some lock.\n", - "2. Pick, at random, one letter on one tumbler and change it to a new letter.\n", - "3. If the change yields more words, keep the change; otherwise discard the change.\n", - "4. Repeat.\n", - "\n", - "We can implement this strategy with the function `improved_lock`, which calls `changed_lock` to make a random change:\n", - "\n" + "%time hillclimbs = [hillclimb(lock) for lock in locks]" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "{('SPMWTCDLFB', 'LEYHWRUOAI', 'ENILRTAOSC', 'DSNYAHLKTE'): 1240,\n", + " ('FPLBSMCDTW', 'IPHLEYURAO', 'ORTEALCISN', 'LYHTKENDSA'): 1240,\n", + " ('PWSCTLBMFD', 'AROYLPEIHU', 'NOACITESLR', 'TEKADLHSYN'): 1240,\n", + " ('PMWSTLFBCD', 'LEOHURYAIP', 'SCLATRNIEO', 'YHLAKTEDNS'): 1240,\n", + " ('CTWLPGDBSM', 'REHAUILYOW', 'AOITREMSNL', 'NKLTDYSPAE'): 1235,\n", + " ('LBFDMSCPWT', 'LIEPYRHAOU', 'SLERTIAONC', 'NHLDKSAYTE'): 1240,\n", + " ('BPCWHMTDLS', 'HLPOEIARUY', 'TRSNCEAILP', 'LDATHYNSEK'): 1232,\n", + " ('PWTMCSBDLF', 'IYEARUPOLH', 'TESROLACNI', 'YLHNSTKAED'): 1240,\n", + " ('DLTFWPBMSC', 'RWHYUEOLAI', 'NRCLOEAIST', 'YEDASHNTKL'): 1240,\n", + " ('SFMWDCLPTB', 'WYROIUAELH', 'LOTCEAISNR', 'DNHTKALYSE'): 1240,\n", + " ('MSFDTBHLPC', 'LIEHRYAWOU', 'ORLNSTECAM', 'AYDTKLSNPE'): 1232,\n", + " ('BMSPFWDTCL', 'YIEPAHRLUO', 'CEOANSIRLT', 'NKLHADSETY'): 1240,\n", + " ('FDMLBSCTPW', 'RYEILWUHAO', 'IONARTCSEL', 'KDYLANTSEH'): 1240,\n", + " ('FBWMCLPSDT', 'WRAEHOIUYL', 'ARLOESTICN', 'DSKLMNAETY'): 1236,\n", + " ('SWLTCFMPBD', 'YOHURLWIAE', 'IECLONARST', 'AETYLKHDSN'): 1240,\n", + " ('WPMTFDLSBC', 'HAUELYROIW', 'CAONELISRT', 'SALKEDHYTN'): 1240,\n", + " ('SPTBDCLMWG', 'OAIEURLHYW', 'RNALEOTISM', 'SETADNLKYP'): 1235,\n", + " ('SPTBDCLMFW', 'OAIEURLHYP', 'RNLEATSCIO', 'SETAYKDNLH'): 1240,\n", + " ('SPTBDCLMHF', 'OAEIURLHYW', 'ARNELOCTSM', 'SETDANLKYP'): 1232,\n", + " ('SPTBDCLMWG', 'OAEIURLHYW', 'ARNELOITSM', 'SETDNALKYP'): 1235,\n", + " ('SPTBDCLMFW', 'OAIEURLHYW', 'RNLEATSCIO', 'SETAHNDKLY'): 1240,\n", + " ('SPTBDCLMFW', 'OAIEURHYLW', 'RNLEAITSOC', 'SETAYNDKLH'): 1240,\n", + " ('SBPTDCLMGW', 'AOIEURLHYW', 'RNALEOTISM', 'SETDNALKPY'): 1235,\n", + " ('SBPTDCLMWF', 'AOIEURLHYP', 'NRLEATSCIO', 'SETAKDYNLH'): 1240,\n", + " ('BPSCTMDLWG', 'AOIEURLHYW', 'RNLEAITOSM', 'SETDLNAKPY'): 1235,\n", + " ('BPCMSTDLGW', 'AOIEURLHYW', 'RNLEAITOSM', 'SETDANLKYP'): 1235,\n", + " ('BMPSDTLCFW', 'AOIEURLHYW', 'NRLEASTCIO', 'SETAKDNYLH'): 1240,\n", + " ('BMPTDCSLGW', 'AOIEURLHYW', 'NRLEATSIOM', 'SETAKDNYLP'): 1235,\n", + " ('SBPTCGMDLW', 'OAEIURLHYW', 'EARNLIOTSM', 'SETDNALKYP'): 1235,\n", + " ('SBPTCWMDLG', 'OAIEURLHYW', 'EARNLIOTSM', 'SETNDALKYP'): 1235,\n", + " ('BPSCTMDLGW', 'OAEIURLHYW', 'EARNLIOTSM', 'SETDLNAKPY'): 1235,\n", + " ('BPCMSTDLGW', 'OAEIURLHYW', 'EARNLIOTSM', 'SETDANLKYP'): 1235,\n", + " ('SPBWTCMDLG', 'OAIEURLHYW', 'EARNLIOTSM', 'SETNDALKYP'): 1235,\n", + " ('BPTCMSDLGW', 'OAEIURLYHW', 'EARNLIOTSM', 'SETNDALKYP'): 1235,\n", + " ('STWPBMDLCF', 'OAIEURLHYW', 'RNLEATSCIO', 'SETANDYLKH'): 1240,\n", + " ('STFPBMDLCW', 'OAIEURHYLW', 'RNLEAITSOC', 'SETANDYLKH'): 1240,\n", + " ('BMPSDTLCFH', 'AOIEURLYHW', 'NRLEASTCOM', 'SETANDYLKP'): 1232,\n", + " ('BMPTDCSLWF', 'AOIEURLYHP', 'NRLEATSICO', 'SETANDYLKH'): 1240,\n", + " ('SBGPTMCDLW', 'OAIEURLHYW', 'NRELAITSOM', 'SETANDPLKY'): 1235,\n", + " ('BPMTCSDLFW', 'OAIEURLYHW', 'NRELAITSOC', 'SETANDYLKH'): 1240}" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "def improved_lock(lock, steps=3000):\n", - " \"Randomly change letters in lock, keeping changes that improve the score.\"\n", - " score = word_count(lock)\n", - " for i in range(steps):\n", - " lock2 = changed_lock(lock)\n", - " score2 = word_count(lock2)\n", - " if score2 >= score: \n", - " lock, score = lock2, score2\n", - " return lock\n", - "\n", - "def changed_lock(lock): \n", - " \"Change one letter in one tumbler.\"\n", - " lock2 = Lock(lock)\n", - " i = random.randrange(len(lock))\n", - " old = random.choice(lock[i])\n", - " new = random.choice([L for L in alphabet if L not in lock[i]])\n", - " lock2[i] = lock2[i].replace(old, new)\n", - " return lock2" + "lock_table(hillclimbs)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Let's see how this does to improve the best lock we've seen so far, the greedy lock:" + "Here are the counts of how many times each score occurred:" ] }, { @@ -685,49 +916,50 @@ "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "Lock: SPTBDCLMWF OAIEURLHYP RNALEOTISC SETADNLKYH\n", - "Count: 1240\n", - "Words: BAAL BAAS BACH BACK BAIL BAIT BALD BALE BALK BALL BALS BAND BANE BANK BANS BARD BARE BARK BARN BARS BASE BASH BASK BASS BAST BATE BATH BATS BATT BEAD BEAK BEAN BEAT BECK BEEN BEES BEET BELL BELS BELT BEND BENE BENS BENT BERK BEST BETA BETH BETS BIAS BICE BILE BILK BILL BIND BINE BINS BINT BIOS BIRD BIRK BIRL BISE BISK BITE BITS BITT BLAE BLAH BLAT BLED BLET BLIN BLOT BOAS BOAT BOCK BOIL BOLA BOLD BOLE BOLL BOLT BOND BONE BONK BONY BOOK BOON BOOS BOOT BORA BORE BORK BORN BORT BOSH BOSK BOSS BOTA BOTH BOTS BOTT BRAD BRAE BRAN BRAS BRAT BRAY BREA BRED BREE BREN BRIA BRIE BRIN BRIS BRIT BROS BUCK BULK BULL BUNA BUND BUNK BUNN BUNS BUNT BUOY BURA BURD BURL BURN BURS BURY BUSH BUSK BUSS BUST BUSY BUTE BUTS BUTT BYES BYRE BYRL BYTE CACA CAEL CAID CAIN CALE CALK CALL CANE CANS CANT CARA CARD CARE CARK CARL CARN CARS CART CASA CASE CASH CASK CAST CATE CATS CECA CEES CEIL CELL CELS CELT CENT CERE CESS CETE CHAD CHAT CHAY CHIA CHID CHIN CHIS CHIT CHON CIAN CINE CION CIRE CIST CITE CITY CLAD CLAN CLAY CLOD CLON CLOT CLOY COAL COAT COCA COCK COED COEN COIL COIN COLA COLD COLE COLS COLT COLY CONE CONK CONN CONS CONY COOK COOL COON COOS COOT CORA CORD CORE CORK CORN CORS CORY COSH COSS COST COSY COTE COTS CRED CRIS CRIT CUED CUES CULL CULT CUNT CURD CURE CURL CURN CURS CURT CUSK CUSS CUTE CUTS CYAN CYST DACE DAIS DALE DALS DANA DANE DANK DANS DARE DARK DARN DART DASH DATA DATE DEAD DEAL DEAN DECK DEED DEES DEET DEIL DELE DELL DELS DELT DENE DENS DENT DENY DEON DERE DESK DHAK DHAL DIAL DICE DICK DIED DIEL DIES DIET DILL DINA DINE DINK DINS DINT DIOL DION DIRE DIRK DIRL DIRT DISH DISK DISS DITA DITE DITS DOAT DOCK DOCS DOES DOIT DOLE DOLL DOLS DOLT DONA DONE DONS DORE DORK DORS DORY DOSE DOSS DOST DOTE DOTH DOTS DOTY DRAT DRAY DREE DREK DUAD DUAL DUCE DUCK DUCT DUEL DUES DUET DUIT DULL DULY DUNE DUNK DUNS DUNT DUOS DURA DURE DURN DUSK DUST DUTY DYAD DYED DYES DYNE FACE FACT FAIL FAIN FALL FANE FANS FARD FARE FARL FART FASH FAST FATE FATS FEAL FEAT FECK FEED FEEL FEES FEET FELL FELT FEND FENS FEOD FERE FERN FESS FEST FETA FETE FETS FIAT FICE FILA FILE FILL FILS FIND FINE FINK FINN FINS FIRE FIRN FIRS FISH FIST FITS FLAK FLAN FLAT FLAY FLEA FLED FLEE FLEY FLIT FLOE FOAL FOES FOIL FOIN FOLD FOLK FOND FONS FONT FOOD FOOL FOOT FORA FORD FORE FORK FORT FOSS FRAE FRAT FRAY FRED FREE FRET FRIT FROE FUCK FUEL FULL FUND FUNK FUNS FURL FURS FURY FUSE FUSS FYCE LACE LACK LACS LACY LAID LAIN LALL LANA LAND LANE LANK LARA LARD LARK LARS LASE LASH LASS LAST LATE LATH LATS LEAD LEAH LEAK LEAL LEAN LEAS LECH LEEK LEES LEET LEIA LEIS LELA LENA LEND LENS LENT LEON LESS LEST LETS LIAN LICE LICH LICK LIED LIEN LIES LILA LILT LILY LINA LINE LINK LINN LINS LINT LINY LION LIRA LIRE LISA LIST LITE LITS LOAD LOAN LOCA LOCH LOCK LOID LOIN LOLA LOLL LONE LOOK LOON LOOS LOOT LORD LORE LORN LORY LOSE LOSS LOST LOTA LOTH LOTS LUCA LUCE LUCK LUCY LUES LUIS LULL LUNA LUNE LUNK LUNT LUNY LURE LURK LUSH LUST LUTE LYCH LYES LYLA LYLE LYRA LYRE LYSE MACE MACH MACK MACS MACY MAES MAIA MAID MAIL MAIN MALE MALL MALT MANA MANE MANS MANY MARA MARE MARK MARL MARS MART MARY MASA MASH MASK MASS MAST MATE MATH MATS MATT MEAD MEAL MEAN MEAT MEED MEEK MEET MELD MELL MELS MELT MEND MERE MERK MERL MESA MESH MESS META METE METH MHOS MIAH MICA MICE MICK MICS MIEN MILA MILD MILE MILK MILL MILS MILT MINA MIND MINE MINK MINT MIRA MIRE MIRK MIRS MIRY MISE MISS MIST MITE MITT MITY MOAN MOAS MOAT MOCK MOCS MOIL MOLA MOLD MOLE MOLL MOLS MOLT MOLY MONA MONK MONS MONY MOOD MOOL MOON MOOS MOOT MORA MORE MORN MORS MORT MOSH MOSK MOSS MOST MOTE MOTH MOTS MOTT MUCH MUCK MULE MULL MUNS MUON MURA MURE MURK MUSA MUSE MUSH MUSK MUSS MUST MUTE MUTS MUTT MYAH MYCS MYLA MYNA MYRA MYTH PACA PACE PACK PACS PACT PACY PAID PAIK PAIL PAIN PALE PALL PALS PALY PANE PANS PANT PARA PARD PARE PARK PARS PART PASE PASH PASS PAST PATE PATH PATS PATY PEAK PEAL PEAN PEAS PEAT PECH PECK PECS PEED PEEK PEEL PEEN PEES PEIN PELE PELT PEND PENS PENT PEON PERE PERK PERT PEST PETS PHAT PHIS PHON PHOT PIAL PIAN PIAS PICA PICE PICK PICS PIED PIES PILE PILL PILY PINA PINE PINK PINS PINT PINY PION PIRN PISH PISS PITA PITH PITS PITY PLAN PLAT PLAY PLEA PLED PLIE PLOD PLOT PLOY POCK POET POIS POLE POLL POLS POLY POND PONE PONS PONY POOD POOH POOL POON POOS PORE PORK PORN PORT POSE POSH POST POSY POTS PRAT PRAY PREE PREY PROA PROD PROS PUCE PUCK PULA PULE PULL PULS PUNA PUNK PUNS PUNT PUNY PURE PURL PURS PUSH PUSS PUTS PUTT PYAS PYES PYIN PYRE SAAD SACK SACS SAID SAIL SAIN SALE SALL SALS SALT SANA SAND SANE SANK SANS SARA SARD SARK SASH SASS SATE SEAL SEAN SEAS SEAT SECS SECT SEED SEEK SEEL SEEN SEES SEIS SELL SELS SEND SENE SENT SERA SERE SERS SETA SETH SETS SETT SHAD SHAE SHAH SHAT SHAY SHEA SHED SHES SHIN SHIT SHOD SHOE SHOT SIAL SICE SICK SICS SILD SILK SILL SILT SINE SINH SINK SINS SIRE SIRS SITE SITH SITS SLAT SLAY SLED SLID SLIT SLOE SLOT SOAK SOCA SOCK SOIL SOLA SOLD SOLE SOLS SONE SONS SOOK SOON SOOT SORA SORD SORE SORN SORT SOTH SOTS SPAE SPAN SPAS SPAT SPAY SPED SPIK SPIN SPIT SPOT SPRY SRIS SUCH SUCK SUED SUES SUET SUIT SULK SUNK SUNN SUNS SURA SURD SURE SUSS SYCE SYED SYNE TACE TACH TACK TACT TAEL TAIL TAIN TALA TALE TALK TALL TANK TANS TAOS TARA TARE TARN TARS TART TASK TASS TATE TATS TEAK TEAL TEAS TEAT TECH TEED TEEL TEEN TEES TELA TELE TELL TELS TEND TENS TENT TERN TESS TEST TETH TETS THAE THAN THAT THEA THEE THEN THEY THIN THIS TICK TICS TIED TIES TILE TILL TILS TILT TINA TINE TINS TINT TINY TIRE TIRL TITS TOAD TOEA TOED TOES TOIL TOIT TOLA TOLD TOLE TOLL TONE TONS TONY TOOK TOOL TOON TOOT TORA TORE TORN TORS TORT TORY TOSH TOSS TOST TOTE TOTS TRAD TRAE TRAY TREE TREK TRES TRET TREY TROD TROT TROY TUCK TUIS TULE TUNA TUNE TUNS TURD TURK TURN TUSH TUSK TUTS TYCE TYEE TYES TYIN TYNE TYRE WACK WAES WAIL WAIN WAIT WALE WALK WALL WALY WAND WANE WANK WANS WANT WANY WARD WARE WARK WARN WARS WART WARY WASH WAST WATS WATT WEAK WEAL WEAN WEED WEEK WEEL WEEN WEES WEET WELD WELL WELT WEND WENS WENT WERE WERT WEST WETS WHAT WHEE WHEN WHET WHEY WHID WHIN WHIT WHOA WICH WICK WILD WILE WILL WILT WILY WIND WINE WINK WINS WINY WIRE WIRY WISE WISH WISS WIST WITE WITH WITS WOAD WOES WOLD WONK WONS WONT WOOD WOOL WOOS WORD WORE WORK WORN WORT WOST WOTS WREN WRIT WUSS WYCH WYES WYLE WYND WYNN WYNS WYTE\n" - ] + "data": { + "text/plain": [ + "[(1240, 21), (1235, 14), (1232, 4), (1236, 1)]" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "show(improved_lock(greedy_lock()))" + "Counter(_.values()).most_common()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "We got up to 1240 words, an improvement of 5%, but can we go beyond that? I'll improve 50 random locks (this will take around 5 minutes):" + "The fact that there are 21 different locks at 1240 (and others that are close) suggested to me that there might be a lock with 1241 or more, and I should search longer. \n", + "\n", + "But a discussion with [Matt Chisholm](https://blog.glyphobet.net/faq) changed my thinking. Matt pointed out that some locks that look different are actually the same; they just have the letters within a disc in a different order. I'll define the function `canonical` to put each disc in canonical alphabetical order, and update `lock_table` to do three new things: (1) use the canonical form; (2) sort the locks by word count; and (3) display along with the word count the [Hamming distance](https://en.wikipedia.org/wiki/Hamming_distance) to a 1240 lock:" ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 4min 34s, sys: 1.03 s, total: 4min 35s\n", - "Wall time: 4min 36s\n" - ] - } - ], + "outputs": [], "source": [ - "%time improved_locks = [improved_lock(random_lock(seed=i)) for i in range(50)]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's see what we got:" + "def canonical(lock: Lock) -> Lock:\n", + " \"Canonicalize a lock by alphabetizing the letters in each disc.\"\n", + " return Lock(cat(sorted(disc)) for disc in lock)\n", + " \n", + "def distance(lock1, lock2) -> int:\n", + " return sum(len(set(lock1[i]) - set(lock2[i])) for i in range(DISCS))\n", + " \n", + "lock1240 = canonical(('SPTBDCLMWF', 'OAIEURLHYW', 'RNALEOTISC', 'SETADNLKYH'))\n", + "assert word_count(lock1240) == 1240\n", + "\n", + "def lock_table(locks: Collection[Lock], target=lock1240) -> dict: \n", + " \"\"\"A table of {canonical_lock: (word_count, distance_to_target} in sorted order.\"\"\"\n", + " locks = sorted(locks, key=word_count, reverse=True)\n", + " return {canonical(lock): (word_count(lock), distance(lock, target)) \n", + " for lock in locks}" ] }, { @@ -738,7 +970,12 @@ { "data": { "text/plain": [ - "Counter({1232: 6, 1235: 5, 1237: 8, 1240: 31})" + "{('BCDFLMPSTW', 'AEHILORUWY', 'ACEILNORST', 'ADEHKLNSTY'): (1240, 0),\n", + " ('BCDFLMPSTW', 'AEHILOPRUY', 'ACEILNORST', 'ADEHKLNSTY'): (1240, 1),\n", + " ('BCDFLMPSTW', 'AEHILORUWY', 'ACEILNORST', 'ADEKLMNSTY'): (1236, 1),\n", + " ('BCDGLMPSTW', 'AEHILORUWY', 'AEILMNORST', 'ADEKLNPSTY'): (1235, 3),\n", + " ('BCDHLMPSTW', 'AEHILOPRUY', 'ACEILNPRST', 'ADEHKLNSTY'): (1232, 3),\n", + " ('BCDFHLMPST', 'AEHILORUWY', 'ACELMNORST', 'ADEKLNPSTY'): (1232, 3)}" ] }, "execution_count": 27, @@ -747,33 +984,62 @@ } ], "source": [ - "Counter(map(word_count, improved_locks))" + "lock_table(hillclimbs)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "So 31/50 locks got a score of 1240.\n", - "My first reaction to this was that if I discovered 31 different locks with score 1240, then probably there is at least one undiscovered lock with a score above 1240. But a discussion with [Matt Chisholm](https://blog.glyphobet.net/faq) changed my thinking. The key is to realize that some locks that look different are actually the same; they just have the letters in a different order. I define the function `alock` to put each tumbler into alphabetical order (and make the lock be a tuple rather than a list, so that it can be an entry in a `dict` or `set`):" + "There are far fewer locks than it seemed at first! \n", + "\n", + "There are just two locks (not 21) that score 1240, and they differ in just one letter (a `P` or a `W` in the second disc). \n", + "\n", + "This discovery changes my whole thinking about the geometry of the lock/score space. Previously I imagined a spiky \"porcupine-shaped\" landscape, with many different peaks hitting a height of 1240. But now I picture a \"space needle\" landscape: a single peak containing the two locks (one with a `P` and one with a `W`), surrounded by other lesser towers. Now I think it is less likely that there is a lock that scores over 1240.\n", + "\n", + "# Searching More\n", + "\n", + "Despie the revelation, I'm not quite ready to give up on finding a higher-scoring lock. An easy thing to try is to search for 8,000 steps rather than just 4,000:" ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 31.9 s, sys: 61.3 ms, total: 32 s\n", + "Wall time: 32.1 s\n" + ] + }, + { + "data": { + "text/plain": [ + "{('BCDFLMPSTW', 'AEHILOPRUY', 'ACEILNORST', 'ADEHKLNSTY'): (1240, 1),\n", + " ('BCDFLMPSTW', 'AEHILORUWY', 'ACEILNORST', 'ADEHKLNSTY'): (1240, 0),\n", + " ('BCDGLMPSTW', 'AEHILORUWY', 'AEILMNORST', 'ADEKLNPSTY'): (1235, 3),\n", + " ('BCDFHLMPST', 'AEHILORUWY', 'ACELMNORST', 'ADEKLNPSTY'): (1232, 3)}" + ] + }, + "execution_count": 28, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "def alock(lock):\n", - " \"Canonicalize lock by alphabetizing the letters in each tumbler.\"\n", - " return tuple(Tumbler(sorted(tumbler)) for tumbler in lock)" + "%time lock_table(hillclimb(lock, repeat=8000) for lock in locks)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Then we can find the unique locks:" + "That didn't help: still the same two 1240 locks. \n", + "\n", + "Maybe part of the problem is that once we've improved a locks a bunch, no single-letter change can improve it further. What if we allowed each change to change either one or two or three letters at a time? That would give us a better chance of escaping from a local maximum." ] }, { @@ -781,18 +1047,22 @@ "execution_count": 29, "metadata": {}, "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 1min 51s, sys: 131 ms, total: 1min 51s\n", + "Wall time: 1min 51s\n" + ] + }, { "data": { "text/plain": [ - "{('BCDFHLMPST', 'AEHILORUWY', 'ACELMNORST', 'ADEKLNPSTY'): 1232,\n", - " ('BCDFLMPSTW', 'AEHILOPRUY', 'ACEILNORST', 'ADEHKLNSTY'): 1240,\n", - " ('BCDFLMPSTW', 'AEHILORUWY', 'ACEILNORST', 'ADEHKLNSTY'): 1240,\n", - " ('BCDGLMPSTW', 'AEHILORUWY', 'AEILMNORST', 'ADEKLNPSTY'): 1235,\n", - " ('BCDHLMPSTW', 'AEHILOPRUY', 'ACEILNPRST', 'ADEHKLNSTY'): 1232,\n", - " ('BCDHLMPSTW', 'AEHILORUWY', 'ACEILNORST', 'ADEHKLNSTY'): 1237,\n", - " ('BCDHLMPSTW', 'AEHILORUWY', 'ACEILNPRST', 'ADEHKLNSTY'): 1232,\n", - " ('BCDLMPRSTW', 'AEHILOPRUY', 'ACEILNPRST', 'ADEHKLNSTY'): 1232,\n", - " ('BCDLMPRSTW', 'AEHILORUWY', 'ACEILNORST', 'ADEHKLNSTY'): 1237}" + "{('BCDFLMPSTW', 'AEHILOPRUY', 'ACEILNORST', 'ADEHKLNSTY'): (1240, 1),\n", + " ('BCDFLMPSTW', 'AEHILORUWY', 'ACEILNORST', 'ADEHKLNSTY'): (1240, 0),\n", + " ('BCDLMPRSTW', 'AEHILORUWY', 'ACEILNORST', 'ADEHKLNSTY'): (1237, 1),\n", + " ('BCDGLMPSTW', 'AEHILORUWY', 'AEILMNORST', 'ADEKLNPSTY'): (1235, 3),\n", + " ('BCDHLMPSTW', 'AEHILOPRUY', 'ACEILNPRST', 'ADEHKLNSTY'): (1232, 3)}" ] }, "execution_count": 29, @@ -801,19 +1071,23 @@ } ], "source": [ - "def unique_locks(locks):\n", - " \"Return a dict of {lock: word_count} for the distinct locks.\"\n", - " return {alock(lock): word_count(lock) \n", - " for lock in locks}\n", + "def lock_changes(lock, n=3) -> Lock: \n", + " \"\"\"Make up to `n` random changes to lock, returning whichever lock is best.\"\"\"\n", + " locks = [lock]\n", + " for _ in range(n):\n", + " locks.append(changed_lock(locks[-1]))\n", + " return max(locks, key=word_count)\n", "\n", - "unique_locks(improved_locks)" + "%time lock_table(hillclimb(lock, lock_changes, repeat=3000) for lock in locks)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "So out of the 50 `improved_locks` there are actually only 9 distinct ones. And only two have a score of 1240:" + "Again, the same two 1240 locks; it just took longer to run.\n", + "\n", + "Maybe we should keep more options open. A hillclimbing search always tracks the single best scoring lock, but a [beam search](https://en.wikipedia.org/wiki/Beam_search) tracks multiple possibilities at each step. The number to track is called the beam width. We can treack 40 different locks, and try up to 3 changes for each one on each iteration:" ] }, { @@ -821,11 +1095,57 @@ "execution_count": 30, "metadata": {}, "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 1min 50s, sys: 118 ms, total: 1min 50s\n", + "Wall time: 1min 50s\n" + ] + }, { "data": { "text/plain": [ - "{('BCDFLMPSTW', 'AEHILOPRUY', 'ACEILNORST', 'ADEHKLNSTY'),\n", - " ('BCDFLMPSTW', 'AEHILORUWY', 'ACEILNORST', 'ADEHKLNSTY')}" + "{('BCDFLMPSTW', 'AEHILORUWY', 'ACEILNORST', 'ADEHKLNSTY'): (1240, 0),\n", + " ('BCDFLMPSTW', 'AEHILOPRUY', 'ACEILNORST', 'ADEHKLNSTY'): (1240, 1),\n", + " ('BCDHLMPSTW', 'AEHILORUWY', 'ACEILNORST', 'ADEHKLNSTY'): (1237, 1),\n", + " ('BCDLMPRSTW', 'AEHILOPRUY', 'ACEILNORST', 'ADEHKLNSTY'): (1237, 2),\n", + " ('BCDLMPRSTW', 'AEHILORUWY', 'ACEILNORST', 'ADEHKLNSTY'): (1237, 1),\n", + " ('BCDFLMPSTW', 'AEHILORUWY', 'ACEILNORST', 'ADEKLMNSTY'): (1236, 1),\n", + " ('BCDHLMPSTW', 'AEHILOPRUY', 'ACEILNORST', 'ADEHKLNSTY'): (1236, 2),\n", + " ('BCDGLMPSTW', 'AEHILORUWY', 'ACEILNORST', 'ADEHKLNSTY'): (1236, 1),\n", + " ('BCDFLMPSTW', 'AEHIKLORUY', 'ACEILNORST', 'ADEHKLNSTY'): (1236, 1),\n", + " ('BCDFLMPSTW', 'AEHILOPRUY', 'ACEILNORST', 'ADEKLNRSTY'): (1236, 2),\n", + " ('BCDGLMPSTW', 'AEHILORUWY', 'AEILMNORST', 'ADEKLNPSTY'): (1235, 3),\n", + " ('BCDGLMPSTW', 'AEHILOPRUY', 'ACEILNORST', 'ADEHKLNSTY'): (1235, 2),\n", + " ('BCDFLMPRST', 'AEHILORUWY', 'ACEILNORST', 'ADEHKLNSTY'): (1235, 1),\n", + " ('BCDFLMPSTW', 'AEHILORTUY', 'ACEILNORST', 'ADEHKLNSTY'): (1235, 1),\n", + " ('BCDFLMPSTW', 'AEHILOPRUY', 'ACEILNORST', 'ADEKLMNSTY'): (1235, 2),\n", + " ('BCDFLMPSTW', 'AEHILORUWY', 'ACEILNORST', 'ADEKLNRSTY'): (1235, 1),\n", + " ('BCDFLMPRST', 'AEHILOPRUY', 'ACEILNORST', 'ADEHKLNSTY'): (1235, 2),\n", + " ('BCDFHLMPST', 'AEHILORUWY', 'ACEILNORST', 'ADEHKLNSTY'): (1235, 1),\n", + " ('BCDFHLMPST', 'AEHILOPRUY', 'ACEILNORST', 'ADEHKLNSTY'): (1234, 2),\n", + " ('BCDHLMPSTW', 'AEHILORUWY', 'AEILMNORST', 'ADEKLNPSTY'): (1234, 3),\n", + " ('BCDFLMPSTW', 'AEHILORUWY', 'ACEILNORST', 'ADEKLNPSTY'): (1234, 1),\n", + " ('BCDGHLMPST', 'AEHILORUWY', 'AEILMNORST', 'ADEKLNPSTY'): (1234, 4),\n", + " ('BCDFGLMPST', 'AEHILORUWY', 'ACEILNORST', 'ADEHKLNSTY'): (1234, 1),\n", + " ('BCDFGLMPST', 'AEHILOPRUY', 'ACEILNORST', 'ADEHKLNSTY'): (1233, 2),\n", + " ('BCDLMPRSTW', 'AEHIKLORUY', 'ACEILNORST', 'ADEHKLNSTY'): (1233, 2),\n", + " ('BCDFLMPSTW', 'AEHILORUWY', 'ACEILNORST', 'ADEGKLNSTY'): (1233, 1),\n", + " ('BCDFGLMPST', 'AEHILORUWY', 'ACEILNORST', 'ADEKLMNSTY'): (1233, 2),\n", + " ('BCDLMPRSTW', 'AEHILOPRUY', 'ACEILNPRST', 'ADEHKLNSTY'): (1232, 3),\n", + " ('BCDFLMPSTW', 'AEHILOPRUY', 'ACEILNORST', 'ADEKLNPSTY'): (1232, 2),\n", + " ('BCDLMPRSTW', 'AEHILORTUY', 'ACEILNORST', 'ADEHKLNSTY'): (1232, 2),\n", + " ('BCDHLMPSTW', 'AEHILOPRUY', 'ACEILNPRST', 'ADEHKLNSTY'): (1232, 3),\n", + " ('BCDHLMPSTW', 'AEHILORUWY', 'ACEILNPRST', 'ADEHKLNSTY'): (1232, 2),\n", + " ('BCDFHLMPST', 'AEHILORUWY', 'ACELMNORST', 'ADEKLNPSTY'): (1232, 3),\n", + " ('BCDHLMPSTW', 'AEHIKLORUY', 'ACEILNORST', 'ADEHKLNSTY'): (1232, 2),\n", + " ('BCDFHLMPST', 'AEHILORUWY', 'ACEILNORST', 'ADEKLMNSTY'): (1232, 2),\n", + " ('BCDFGLMPST', 'AEHILORUWY', 'AEILMNORST', 'ADEKLNPSTY'): (1231, 3),\n", + " ('BCDLMPRSTW', 'AEHILORUWY', 'ACEILNPRST', 'ADEHKLNSTY'): (1231, 2),\n", + " ('BCDGLMPSTW', 'AEHIKLORUY', 'ACEILNORST', 'ADEHKLNSTY'): (1231, 2),\n", + " ('BCDFLMPRST', 'AEHIKLORUY', 'ACEILNORST', 'ADEHKLNSTY'): (1231, 2),\n", + " ('BCDFLMPSTW', 'AEHILORUWY', 'AEILMNORST', 'ADEKLNPSTY'): (1231, 2)}" ] }, "execution_count": 30, @@ -834,16 +1154,25 @@ } ], "source": [ - "{L for L in _ if word_count(L) == 1240}" + "def beam_search(locks, changer=lock_changes, scorer=word_count, beam_width=40, repeat=3000) -> List[Lock]:\n", + " \"\"\"Keep up to `beam_width` locks, changing each one with `changer`, and keeping the best scoring ones\n", + " according to `scorer`. Repeat.\"\"\"\n", + " locks = set(map(canonical, locks)) # Make a copy\n", + " for _ in range(repeat):\n", + " locks |= set(map(canonical, map(changer, locks)))\n", + " locks = set(sorted(locks, key=scorer, reverse=True)[:beam_width])\n", + " return locks\n", + "\n", + "%time lock_table(beam_search(locks))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "These two differ in just one letter (a `P` or a `W` in the second tumbler). \n", + "I'm getting more convinced that 1240 is the top-scoring lock. \n", "\n", - "This discovery changes my whole thinking about the space of scores for locks. Previously I imagined a spiky \"porcupine-shaped\" landscape, with 31 different peaks hitting a height of 1240 (and probably other peaks that we haven't discovered yet). But now I have a different picture of the landscape: a single peak containing the two locks (one with a `P` and one with a `W`), surrounded by rolling hills." + "On to the next question." ] }, { @@ -859,9 +1188,11 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Can we make a lock that spells 10 words simultaneously? One possible approach would be to start with any lock and randomly change it (just as we did with `improved_lock`), but measuring improvements by the number of horizontal words formed. My intuition is that this approach would work, eventually, but that progress would be slow, because most random changes to a letter would not make a word.\n", + "Can we make a lock that spells 10 words simultaneously? And could a lock with more than 10 letters on a disc spell more than 10 words simultaneously? A disc cannot have duplicates of any letter, so the upper bound is 26.\n", "\n", - "An alternative approach is to think of the lock not as a list of 4 vertical tumblers (each with 10 letters), but rather as a list of 10 horizontal words (each with 4 letters). I'll call this the *word list* representation, and note that a lock and a word list are *[matrix transposes](http://en.wikipedia.org/wiki/Transpose)* of each other—they swap rows for columns. There is an [old trick](https://books.google.com/books?id=eH6jBQAAQBAJ&pg=PA574&lpg=PA574&dq=lisp+transpose+matrix&source=bl&ots=Yixwj8m3k4&sig=KoeuJnFhRnJsiD06_Cx56rUOetQ&hl=en&sa=X&ved=0CB4Q6AEwAGoVChMIyM-WiriLxgIVD6OICh2QcwBK#v=onepage&q=transpose%20matrix&f=false) to compute the transpose of a matrix `M` with the expression `zip(*M)`. But `zip` returns tuples; we want strings, so we can define `transpose` as:" + "We could use hillclimbing with a `scorer` that counts simultaneous words. My intuition is that this approach would work, eventually, but that progress would be very slow, because most random changes to a single letter would not increase the number of simultaneous words.\n", + "\n", + "An alternative approach is to think of the lock not as a tuple of 4 discs (each with 10 letters), but rather as a list of 10 rows (each with 4 letters), where we want the rows to be words. Then we can just go through the list of valid words and greedily include words that don't duplicate a letter in any column:" ] }, { @@ -870,14 +1201,24 @@ "metadata": {}, "outputs": [], "source": [ - "def transpose(strings): return [Word(letters) for letters in zip(*strings)]" + "def greedy_simultaneous_words(words=WORDS) -> List[Word]:\n", + " \"\"\"Greedily add words that have no duplicate letters with previous rows.\"\"\"\n", + " rows = []\n", + " for word in words:\n", + " if not has_duplicate_letters(word, rows):\n", + " rows.append(word)\n", + " return rows\n", + "\n", + "def has_duplicate_letters(word: Word, rows: List[Word]) -> bool:\n", + " \"\"\"Is any letter in this `word` a duplicate of the corresponding letter in any of the `rows`?\"\"\"\n", + " return any(word[i] == row[i] for i in range(DISCS) for row in rows)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "And we can see the transpose of the `wordlock` is a list of words:" + "We can run `greedy_simultaneous_words()` (with the help of a `report` function) and see if it gives us 10 (or more) words" ] }, { @@ -886,274 +1227,57 @@ "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "['SLED',\n", - " 'PENS',\n", - " 'HYMN',\n", - " 'MHLM',\n", - " 'TNRP',\n", - " 'WRTY',\n", - " 'DUAL',\n", - " 'LOOK',\n", - " 'FAST',\n", - " 'BIKE']" - ] - }, - "execution_count": 32, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "13 words: AADI BEAD CHEF DIBS EBON FLIC GOGO HUCK ICKY KNUR LYLA ODYL PFFT\n" + ] } ], "source": [ - "transpose(['SPHMTWDLFB', \n", - " 'LEYHNRUOAI', \n", - " 'ENMLRTAOSK', \n", - " 'DSNMPYLKTE'])" + "def report(items) -> None: print(len(items), 'words:', *items)\n", + " \n", + "report(greedy_simultaneous_words())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "The first row of the word list has the letters SLED, because those are the letters in the first column of the lock. You can see that the WordLock® is designed to spell out LOOK FAST BIKE, among other words.\n", + "It does give us more than 10 words!\n", "\n", - "Now we're ready to find a good word list with this strategy:\n", - "\n", - "1. Start with some word list (e.g., a random sample of 10 words from `WORDS`).\n", - "2. Pick, at random, one word and change it to a new word.\n", - "3. If the change is an improvement, keep the change; otherwise discard the change.\n", - "4. Repeat.\n", - "\n", - "But what counts as an improvement? We can't improve the number of words, because we start with 10 words, and every change also gives us 10 words. Rather, we will try to improve the number of duplicate letters on any tumbler (of the lock that corresponds to the word list). We improve by reducing the number of duplicate letters, and stop when there are no duplicates.\n", - "\n", - "The following code implements this approach:" + "The answer we get depends on the order in which we go through the words. So let's try a thousand different orders, shuffling the list of words before each try:" ] }, { "cell_type": "code", "execution_count": 33, "metadata": {}, - "outputs": [], - "source": [ - "def improved_wordlist(wordlist):\n", - " \"Find a wordlist that has no duplicate letters, via random changes to wordlist.\"\n", - " score = duplicates(wordlist)\n", - " while score > 0:\n", - " wordlist2 = changed_wordlist(wordlist)\n", - " score2 = duplicates(wordlist2)\n", - " if score2 < score: \n", - " wordlist, score = wordlist2, score2\n", - " return wordlist\n", - " \n", - "def duplicates(wordlist):\n", - " \"The number of duplicate letters across all the tumblers of this wordlist.\"\n", - " lock = transpose(wordlist) \n", - " def duplicates(tumbler): return len(tumbler) - len(set(tumbler))\n", - " return sum(duplicates(tumbler) for tumbler in lock)\n", - "\n", - "def changed_wordlist(wordlist, words=list(WORDS)):\n", - " \"Make a copy of wordlist and replace one of the words.\"\n", - " copy = list(wordlist)\n", - " i = random.randrange(len(wordlist))\n", - " copy[i] = random.choice(words)\n", - " return copy" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The structure of `improved_wordlist` is similar to `improved_lock`, with a few differences:\n", - "1. We are minimizing duplicates, not maximizing word count. \n", - "2. We stop when the score is 0, rather than continuing for a given number of iterations.\n", - "3. We want to make a `random.choice` from `WORDS`. But `random.choice` can't operate on a `set`, so we\n", - "have to introduce `words=list(WORDS)`.\n", - "\n", - "Now we can find some wordlists:" - ] - }, - { - "cell_type": "code", - "execution_count": 34, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "['SYCE',\n", - " 'KINO',\n", - " 'THAW',\n", - " 'YELD',\n", - " 'INBY',\n", - " 'HUGS',\n", - " 'OMEN',\n", - " 'ADIT',\n", - " 'COWL',\n", - " 'MARC']" - ] - }, - "execution_count": 34, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "improved_wordlist(random.sample(WORDS, 10))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "That was easy! Can we [go to 11](https://www.youtube.com/watch?v=4xgx4k83zzc)?" - ] - }, - { - "cell_type": "code", - "execution_count": 35, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "['JAPE',\n", - " 'GIGA',\n", - " 'FLIC',\n", - " 'TOON',\n", - " 'URUS',\n", - " 'EWER',\n", - " 'DENY',\n", - " 'KHAF',\n", - " 'SYLI',\n", - " 'RUST',\n", - " 'IDYL']" - ] - }, - "execution_count": 35, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "improved_wordlist(random.sample(WORDS, 11))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Improving Anything" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We now have two similar functions, `improved_lock` and `improved_wordlist`. Could (and should?) we replace them by a single function, say, `improved`, that could improve locks, wordlists, and anything else?\n", - "\n", - "The answer is: *yes* we could, and *maybe* we should.\n", - "\n", - "It is nice to form an abstraction for the idea of *improvement*. (Traditionally, the method we have used for improvement has been called *hill-climbing*, because of the analogy that the score is like the elevation on a topological map, and we are trying to find our way to a peak.)\n", - "\n", - "However, there are many variations on the theme of *improvement*: maximizing or minimizing? Repeat for a given number of iterations, or continue until we meet a goal? I don't want `improved` to have an argument list a mile long, and I felt that five arguments is right on the border of acceptable. The arguments are:\n", - "1. `item`: The object to start with; this is what we will try to improve.\n", - "2. `changed`: a function that generates a new item.\n", - "3. `scorer`: a function that evaluates the quality of an item.\n", - "4. `extremum`: should be `max` if we are maximizing scores, or `min` if we are minimizing scores.\n", - "5. `stop`: a predicate with args `(i, score, item)`, where `i` is the iteration number, and `score` is `scorer(item)`. Return `True` to stop.\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 36, - "metadata": {}, - "outputs": [], - "source": [ - "def improved(item, changed, scorer, extremum, stop):\n", - " \"\"\"Apply the function `changed` to `item` and evaluate with the function `scorer`;\n", - " When `stop(i, score)` is true, return `item`.\"\"\"\n", - " score = scorer(item)\n", - " for i in itertools.count(0):\n", - " if stop(i, score):\n", - " return item\n", - " item2 = changed(item)\n", - " score2 = scorer(item2)\n", - " if score2 == extremum(score, score2):\n", - " item, score = item2, score2" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now we can re-implement `improved_lock` and `improved_wordlist` using `improved`:" - ] - }, - { - "cell_type": "code", - "execution_count": 37, - "metadata": {}, - "outputs": [], - "source": [ - "def improved_lock(lock, steps=3000):\n", - " \"Randomly change letters in lock, keeping changes that improve the score.\"\n", - " return improved(lock, changed_lock, word_count, max,\n", - " lambda i, _: i == steps)\n", - "\n", - "def improved_wordlist(wordlist):\n", - " \"Find a wordlist that has no duplicate letters, via random changes to wordlist.\"\n", - " return improved(wordlist, changed_wordlist, duplicates, min,\n", - " lambda _, score: score == 0)" - ] - }, - { - "cell_type": "code", - "execution_count": 38, - "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Lock: TMSLBPFWCD AEHYLORIWU CNATELISRO LDSYANKETH\n", - "Count: 1240\n", - "Words: BAAL BAAS BACH BACK BAIL BAIT BALD BALE BALK BALL BALS BAND BANE BANK BANS BARD BARE BARK BARN BARS BASE BASH BASK BASS BAST BATE BATH BATS BATT BEAD BEAK BEAN BEAT BECK BEEN BEES BEET BELL BELS BELT BEND BENE BENS BENT BERK BEST BETA BETH BETS BIAS BICE BILE BILK BILL BIND BINE BINS BINT BIOS BIRD BIRK BIRL BISE BISK BITE BITS BITT BLAE BLAH BLAT BLED BLET BLIN BLOT BOAS BOAT BOCK BOIL BOLA BOLD BOLE BOLL BOLT BOND BONE BONK BONY BOOK BOON BOOS BOOT BORA BORE BORK BORN BORT BOSH BOSK BOSS BOTA BOTH BOTS BOTT BRAD BRAE BRAN BRAS BRAT BRAY BREA BRED BREE BREN BRIA BRIE BRIN BRIS BRIT BROS BUCK BULK BULL BUNA BUND BUNK BUNN BUNS BUNT BUOY BURA BURD BURL BURN BURS BURY BUSH BUSK BUSS BUST BUSY BUTE BUTS BUTT BYES BYRE BYRL BYTE CACA CAEL CAID CAIN CALE CALK CALL CANE CANS CANT CARA CARD CARE CARK CARL CARN CARS CART CASA CASE CASH CASK CAST CATE CATS CECA CEES CEIL CELL CELS CELT CENT CERE CESS CETE CHAD CHAT CHAY CHIA CHID CHIN CHIS CHIT CHON CIAN CINE CION CIRE CIST CITE CITY CLAD CLAN CLAY CLOD CLON CLOT CLOY COAL COAT COCA COCK COED COEN COIL COIN COLA COLD COLE COLS COLT COLY CONE CONK CONN CONS CONY COOK COOL COON COOS COOT CORA CORD CORE CORK CORN CORS CORY COSH COSS COST COSY COTE COTS CRED CRIS CRIT CUED CUES CULL CULT CUNT CURD CURE CURL CURN CURS CURT CUSK CUSS CUTE CUTS CYAN CYST DACE DAIS DALE DALS DANA DANE DANK DANS DARE DARK DARN DART DASH DATA DATE DEAD DEAL DEAN DECK DEED DEES DEET DEIL DELE DELL DELS DELT DENE DENS DENT DENY DEON DERE DESK DHAK DHAL DIAL DICE DICK DIED DIEL DIES DIET DILL DINA DINE DINK DINS DINT DIOL DION DIRE DIRK DIRL DIRT DISH DISK DISS DITA DITE DITS DOAT DOCK DOCS DOES DOIT DOLE DOLL DOLS DOLT DONA DONE DONS DORE DORK DORS DORY DOSE DOSS DOST DOTE DOTH DOTS DOTY DRAT DRAY DREE DREK DUAD DUAL DUCE DUCK DUCT DUEL DUES DUET DUIT DULL DULY DUNE DUNK DUNS DUNT DUOS DURA DURE DURN DUSK DUST DUTY DYAD DYED DYES DYNE FACE FACT FAIL FAIN FALL FANE FANS FARD FARE FARL FART FASH FAST FATE FATS FEAL FEAT FECK FEED FEEL FEES FEET FELL FELT FEND FENS FEOD FERE FERN FESS FEST FETA FETE FETS FIAT FICE FILA FILE FILL FILS FIND FINE FINK FINN FINS FIRE FIRN FIRS FISH FIST FITS FLAK FLAN FLAT FLAY FLEA FLED FLEE FLEY FLIT FLOE FOAL FOES FOIL FOIN FOLD FOLK FOND FONS FONT FOOD FOOL FOOT FORA FORD FORE FORK FORT FOSS FRAE FRAT FRAY FRED FREE FRET FRIT FROE FUCK FUEL FULL FUND FUNK FUNS FURL FURS FURY FUSE FUSS FYCE LACE LACK LACS LACY LAID LAIN LALL LANA LAND LANE LANK LARA LARD LARK LARS LASE LASH LASS LAST LATE LATH LATS LEAD LEAH LEAK LEAL LEAN LEAS LECH LEEK LEES LEET LEIA LEIS LELA LENA LEND LENS LENT LEON LESS LEST LETS LIAN LICE LICH LICK LIED LIEN LIES LILA LILT LILY LINA LINE LINK LINN LINS LINT LINY LION LIRA LIRE LISA LIST LITE LITS LOAD LOAN LOCA LOCH LOCK LOID LOIN LOLA LOLL LONE LOOK LOON LOOS LOOT LORD LORE LORN LORY LOSE LOSS LOST LOTA LOTH LOTS LUCA LUCE LUCK LUCY LUES LUIS LULL LUNA LUNE LUNK LUNT LUNY LURE LURK LUSH LUST LUTE LYCH LYES LYLA LYLE LYRA LYRE LYSE MACE MACH MACK MACS MACY MAES MAIA MAID MAIL MAIN MALE MALL MALT MANA MANE MANS MANY MARA MARE MARK MARL MARS MART MARY MASA MASH MASK MASS MAST MATE MATH MATS MATT MEAD MEAL MEAN MEAT MEED MEEK MEET MELD MELL MELS MELT MEND MERE MERK MERL MESA MESH MESS META METE METH MHOS MIAH MICA MICE MICK MICS MIEN MILA MILD MILE MILK MILL MILS MILT MINA MIND MINE MINK MINT MIRA MIRE MIRK MIRS MIRY MISE MISS MIST MITE MITT MITY MOAN MOAS MOAT MOCK MOCS MOIL MOLA MOLD MOLE MOLL MOLS MOLT MOLY MONA MONK MONS MONY MOOD MOOL MOON MOOS MOOT MORA MORE MORN MORS MORT MOSH MOSK MOSS MOST MOTE MOTH MOTS MOTT MUCH MUCK MULE MULL MUNS MUON MURA MURE MURK MUSA MUSE MUSH MUSK MUSS MUST MUTE MUTS MUTT MYAH MYCS MYLA MYNA MYRA MYTH PACA PACE PACK PACS PACT PACY PAID PAIK PAIL PAIN PALE PALL PALS PALY PANE PANS PANT PARA PARD PARE PARK PARS PART PASE PASH PASS PAST PATE PATH PATS PATY PEAK PEAL PEAN PEAS PEAT PECH PECK PECS PEED PEEK PEEL PEEN PEES PEIN PELE PELT PEND PENS PENT PEON PERE PERK PERT PEST PETS PHAT PHIS PHON PHOT PIAL PIAN PIAS PICA PICE PICK PICS PIED PIES PILE PILL PILY PINA PINE PINK PINS PINT PINY PION PIRN PISH PISS PITA PITH PITS PITY PLAN PLAT PLAY PLEA PLED PLIE PLOD PLOT PLOY POCK POET POIS POLE POLL POLS POLY POND PONE PONS PONY POOD POOH POOL POON POOS PORE PORK PORN PORT POSE POSH POST POSY POTS PRAT PRAY PREE PREY PROA PROD PROS PUCE PUCK PULA PULE PULL PULS PUNA PUNK PUNS PUNT PUNY PURE PURL PURS PUSH PUSS PUTS PUTT PYAS PYES PYIN PYRE SAAD SACK SACS SAID SAIL SAIN SALE SALL SALS SALT SANA SAND SANE SANK SANS SARA SARD SARK SASH SASS SATE SEAL SEAN SEAS SEAT SECS SECT SEED SEEK SEEL SEEN SEES SEIS SELL SELS SEND SENE SENT SERA SERE SERS SETA SETH SETS SETT SHAD SHAE SHAH SHAT SHAY SHEA SHED SHES SHIN SHIT SHOD SHOE SHOT SIAL SICE SICK SICS SILD SILK SILL SILT SINE SINH SINK SINS SIRE SIRS SITE SITH SITS SLAT SLAY SLED SLID SLIT SLOE SLOT SOAK SOCA SOCK SOIL SOLA SOLD SOLE SOLS SONE SONS SOOK SOON SOOT SORA SORD SORE SORN SORT SOTH SOTS SRIS SUCH SUCK SUED SUES SUET SUIT SULK SUNK SUNN SUNS SURA SURD SURE SUSS SWAN SWAT SWAY SWOT SYCE SYED SYNE TACE TACH TACK TACT TAEL TAIL TAIN TALA TALE TALK TALL TANK TANS TAOS TARA TARE TARN TARS TART TASK TASS TATE TATS TEAK TEAL TEAS TEAT TECH TEED TEEL TEEN TEES TELA TELE TELL TELS TEND TENS TENT TERN TESS TEST TETH TETS THAE THAN THAT THEA THEE THEN THEY THIN THIS TICK TICS TIED TIES TILE TILL TILS TILT TINA TINE TINS TINT TINY TIRE TIRL TITS TOAD TOEA TOED TOES TOIL TOIT TOLA TOLD TOLE TOLL TONE TONS TONY TOOK TOOL TOON TOOT TORA TORE TORN TORS TORT TORY TOSH TOSS TOST TOTE TOTS TRAD TRAE TRAY TREE TREK TRES TRET TREY TROD TROT TROY TUCK TUIS TULE TUNA TUNE TUNS TURD TURK TURN TUSH TUSK TUTS TWAE TWAS TWAT TWEE TWIN TWIT TWOS TYCE TYEE TYES TYIN TYNE TYRE WACK WAES WAIL WAIN WAIT WALE WALK WALL WALY WAND WANE WANK WANS WANT WANY WARD WARE WARK WARN WARS WART WARY WASH WAST WATS WATT WEAK WEAL WEAN WEED WEEK WEEL WEEN WEES WEET WELD WELL WELT WEND WENS WENT WERE WERT WEST WETS WHAT WHEE WHEN WHET WHEY WHID WHIN WHIT WHOA WICH WICK WILD WILE WILL WILT WILY WIND WINE WINK WINS WINY WIRE WIRY WISE WISH WISS WIST WITE WITH WITS WOAD WOES WOLD WONK WONS WONT WOOD WOOL WOOS WORD WORE WORK WORN WORT WOST WOTS WREN WRIT WUSS WYCH WYES WYLE WYND WYNN WYNS WYTE\n" + "16 words: BUNN KAPH JOWL ADAM FLUX EPOS GEED RISK MYLA UNCI CRIB ICKY SKYE OTTO THRU PFFT\n" ] } ], "source": [ - "show(improved_lock(random_lock()))" + "def shuffled(items: list) -> List: return random.sample(items, len(items))\n", + "\n", + "def greedier_simultaneous_words(words=WORDS, n=1000) -> List[Word]:\n", + " \"\"\"Try `n` times and return the try with the most simultaneous words.\"\"\"\n", + " tries = (greedy_simultaneous_words(shuffled(WORDS)) for _ in range(n))\n", + " return max(tries, key=len)\n", + "\n", + "report(greedier_simultaneous_words())" ] }, { - "cell_type": "code", - "execution_count": 39, + "cell_type": "markdown", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "['PISS',\n", - " 'HOWK',\n", - " 'FALX',\n", - " 'RETE',\n", - " 'ETCH',\n", - " 'CLUB',\n", - " 'SPIV',\n", - " 'AJAR',\n", - " 'THEO',\n", - " 'BURL']" - ] - }, - "execution_count": 39, - "metadata": {}, - "output_type": "execute_result" - } - ], "source": [ - "improved_wordlist(random.sample(WORDS, 10))" + "16 simultaneous words; I think that's pretty good." ] }, { @@ -1168,55 +1292,38 @@ "metadata": {}, "source": [ "There is still one unanswered question: did the designers of WordLock® deliberately put \"FRED BUNS\" in, or was it a coincidence? Hacker News reader [emhart](https://news.ycombinator.com/user?id=emhart) (aka the competitive lockpicker Schyler Towne) astutely commented that he had found the [patent](https://www.google.com/patents/US6621405) assigned to WordLock; it describes an algorithm similar to my `greedy_lock`.\n", - "After seeing that, I'm inclined to believe that \"FRED BUNS\" is the coincidental result of running the algorithm. On\n", - "the other hand, there is a [followup patent](https://www.google.com/patents/US20080053167) that discusses a refinement\n", - "\"wherein the letters on the wheels are configured to spell a first word displayed on a first row of letters and a second word displayed on a second row of letters.\" So the possibility of a two-word phrase was something that Wordlock LLc. was aware of.\n", + "After seeing that, I'm inclined to believe that \"FRED BUNS\" is the coincidental result of running the algorithm. On the other hand, there is a [followup patent](https://www.google.com/patents/US20080053167) that discusses a refinement\n", + "\"wherein the letters on the wheels are configured to spell a first word displayed on a first row of letters and a second word displayed on a second row of letters.\" So the possibility of a two-word phrase was something that Wordlock LLc. was aware of." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Patents\n", "\n", - "We see below that the procedure described in the [patent](https://www.google.com/patents/US6621405) is not quite as good as `greedy_lock`, because the patent states that at each tumbler position \"*the entire word list is scanned*\" to produce the letter frequencies, whereas `greedy_lock` scans only the words that are consistent with the previous tumblers. Because of that difference, `greedy_lock` produces more words, 1177 to 1161:" + "We see below that the procedure described in the [Wordlock Inc patent](https://www.google.com/patents/US6621405) is not quite as good as `greedy_lock`, because the patent states that at each disc position \"*the entire word list is scanned*\" to produce the letter frequencies, whereas `greedy_lock` scans only the *possible* words: the words that are consistent with the previously-filled discs. Because of that difference, the patented algorithm does worse than my `greedy_lock` (by 1,161 to 1,177) and my `greedier_lock` (by 1,161 to 1,235)." ] }, { "cell_type": "code", - "execution_count": 40, + "execution_count": 34, "metadata": {}, "outputs": [], "source": [ - "def greedy_lock_patented(t=4, c=10, words=WORDS):\n", - " \"Make a lock with t tumblers, each consisting of c letters covering the most words.\"\n", - " lock = Lock()\n", - " for i in range(t):\n", - " # Make a tumbler of c letters, such that the tumbler covers the most words.\n", - " # Then update words to only include the ones that can be made with this tumbler\n", - " counter = Counter(word[i] for word in words)\n", - " tumbler = Tumbler(L for (L, _) in counter.most_common(c))\n", - " # words = {w for w in words if w[i] in tumbler} # <<<< The patent does not update\n", - " lock.append(tumbler)\n", - " return lock" + "def greedy_lock_patented(words=WORDS, order=range(DISCS)) -> Lock:\n", + " \"\"\"Make a lock where we greedily choose the LETTERS best letters for each disc, in order.\"\"\"\n", + " lock = DISCS * [Disc()] # Initially a lock of 4 empty discs\n", + " for i in order: \n", + " # Make lock[i] be a disc that covers the most words, then update `words`\n", + " lock[i] = disc = most_common(words, i)\n", + " #### Don't update words #### words = [w for w in words if w[i] in disc]\n", + " return Lock(lock)" ] }, { "cell_type": "code", - "execution_count": 41, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "1177" - ] - }, - "execution_count": 41, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "word_count(greedy_lock())" - ] - }, - { - "cell_type": "code", - "execution_count": 42, + "execution_count": 35, "metadata": {}, "outputs": [ { @@ -1225,7 +1332,7 @@ "1161" ] }, - "execution_count": 42, + "execution_count": 35, "metadata": {}, "output_type": "execute_result" } @@ -1234,6 +1341,13 @@ "word_count(greedy_lock_patented())" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "What does it say about our patent system that Wordlock Inc actually got a patent for an algorithm that is worse than the initial idea I came up with just to use as a baseline against the *real* hillclimbing algorithm?" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -1246,51 +1360,72 @@ "metadata": {}, "source": [ "It is a \n", - "good idea to have some tests, in case you want to change some code and see if you have introduced an error. Also, tests serve as examples of usage of functions. The following tests have poor coverage, because it is hard to test non-deterministic functions, and I didn't attempt that here." + "good idea to have some tests. The following tests have poor coverage, because it is hard to test non-deterministic functions, and I didn't attempt that here." ] }, { "cell_type": "code", - "execution_count": 43, + "execution_count": 36, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "'pass'" + "True" ] }, - "execution_count": 43, + "execution_count": 36, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "def tests():\n", + "def tests() -> bool:\n", " assert 'WORD' in WORDS\n", " assert 'FRED' in WORDS\n", " assert 'BUNS' in WORDS\n", + " assert 'FIVE' in WORDS\n", " assert 'XYZZ' not in WORDS\n", " assert 'word' not in WORDS\n", - " assert 'FIVE' in WORDS\n", " assert 'FIVER' not in WORDS\n", " assert len(WORDS) == 4360\n", - " \n", - " assert fredbuns == ['FB', 'RU', 'EN', 'DS']\n", - " assert combinations(fredbuns) == {'FRED','FRES','FRND','FRNS','FUED','FUES','FUND','FUNS',\n", - " 'BRED','BRES','BRND','BRNS','BUED','BUES','BUND','BUNS'}\n", - " assert words_from(fredbuns) == {'FRED', 'FUND', 'FUNS', 'BRED', 'BUND', 'BUNS'}\n", - " assert \"FRED\" in words_from(wordlock) and \"BUNS\" in words_from(wordlock)\n", "\n", - " assert wordlock == ['SPHMTWDLFB', 'LEYHNRUOAI', 'ENMLRTAOSK', 'DSNMPYLKTE']\n", - " assert len(combinations(wordlock)) == 10000\n", + " fredbuns = Lock(['FB', 'RU', 'EN', 'DS'])\n", + " assert words_from(fredbuns) == ['BRED', 'BUND', 'BUNS', 'FRED', 'FUND', 'FUNS']\n", + " assert word_count(fredbuns) == 6\n", + " assert most_common(fredbuns, 0) == 'FRED'\n", + " assert most_common(['stink', 'stank', 'stunk'], 2) == 'iau'\n", + "\n", + " assert greedy_lock() == ('SPTBDCLMAR', 'OAIEURLHYN', 'RNALEOTISM', 'SETADNLKYP')\n", + " assert greedier_lock() == ('BPTCMSDLGW', 'OAEIURLYHW', 'EARNLIOTSM', 'SETNDALKYP')\n", + " assert greedy_lock_patented() == ('SPTBDCLMAR', 'AOIEURLHYN', 'EARNLIOTSM', 'SETANDYLKO')\n", + " assert word_count(greedier_lock()) >= word_count(greedy_lock())\n", + " \n", + " assert wordlock == Lock(('SPHMTWDLFB', 'LEYHNRUOAI', 'ENMLRTAOSK', 'DSNMPYLKTE'))\n", + " assert regex(wordlock) == '[SPHMTWDLFB][LEYHNRUOAI][ENMLRTAOSK][DSNMPYLKTE]'\n", " assert word_count(wordlock) == 1118\n", + " assert canonical(wordlock) == ('BDFHLMPSTW', 'AEHILNORUY', 'AEKLMNORST', 'DEKLMNPSTY')\n", + " assert \"FRED\" in words_from(wordlock) \n", + " assert \"BUNS\" in words_from(wordlock)\n", + " assert \"QUIT\" not in words_from(wordlock)\n", + "\n", + " assert distance(wordlock, lock1240) == 6\n", + " assert distance(greedy_lock(), lock1240) == 5\n", + " assert distance(greedy_lock(), greedy_lock_patented()) == 1\n", + "\n", + " assert has_duplicate_letters(\"WORD\", [\"WILD\", \"DOGS\"])\n", + " assert not has_duplicate_letters(\"WORD\", [\"FREE\", \"CATS\"])\n", + "\n", + " assert greedy_simultaneous_words() == [\n", + " 'AADI', 'BEAD', 'CHEF', 'DIBS', 'EBON', 'FLIC', 'GOGO', 'HUCK', 'ICKY', 'KNUR', 'LYLA', 'ODYL', 'PFFT']\n", " \n", - " assert transpose(['HIE', 'BYE']) == ['HB', 'IY', 'EE']\n", - " assert transpose(transpose(wordlock)) == wordlock\n", - " \n", - " return 'pass'\n", + " assert lock_table([greedy_lock(), greedier_lock(), wordlock]) == {\n", + " ('BCDGLMPSTW', 'AEHILORUWY', 'AEILMNORST', 'ADEKLNPSTY'): (1235, 3),\n", + " ('ABCDLMPRST', 'AEHILNORUY', 'AEILMNORST', 'ADEKLNPSTY'): (1177, 5),\n", + " ('BDFHLMPSTW', 'AEHILNORUY', 'AEKLMNORST', 'DEKLMNPSTY'): (1118, 6)}\n", " \n", + " return True\n", + "\n", "tests()" ] }, @@ -1300,7 +1435,7 @@ "source": [ "# Addendum: 2018\n", "\n", - "New [research](https://www.theverge.com/2018/10/7/17940352/turing-test-one-word-minimal-human-ai-machine-poop) suggests that the one word that humans are most likely to think was generated by another human rather than by a machine is *\"poop\"*. Is it a coincidence that the same week this research came out, I was in a bike shop and saw the following:\n", + "New [research](https://www.theverge.com/2018/10/7/17940352/turing-test-one-word-minimal-human-ai-machine-poop) suggests that the one word that humans are most likely to think was generated by another human rather than by a machine is \"*poop*\". Is it a coincidence that the same week this research came out, I was in a bike shop and saw the following:\n", "\n", "![](like.jpg)\n", "\n", @@ -1309,7 +1444,7 @@ }, { "cell_type": "code", - "execution_count": 44, + "execution_count": 37, "metadata": {}, "outputs": [ { @@ -1318,7 +1453,7 @@ "True" ] }, - "execution_count": 44, + "execution_count": 37, "metadata": {}, "output_type": "execute_result" } @@ -1343,7 +1478,7 @@ }, { "cell_type": "code", - "execution_count": 45, + "execution_count": 38, "metadata": {}, "outputs": [ { @@ -1352,22 +1487,21 @@ "{7, 15, 23, 28, 31, 39, 47, 55, 60, 63, 71, 79, 87, 92, 95}" ] }, - "execution_count": 45, + "execution_count": 38, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "# Numbers up to 100 that are not the sum of 3 squares\n", - "nums = range(11)\n", - "sums = {A**2 + B**2 + C**2 for A in nums for B in nums for C in nums}\n", - "set(range(101)) - sums" + "nums = range(11) \n", + "sums = {A**2 + B**2 + C**2 for A in nums for B in nums for C in nums} # Sums of 3 squares\n", + "set(range(101)) - sums # Numbers up to 100 that are not the sum of 3 squares" ] } ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -1381,9 +1515,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.5.3" + "version": "3.8.15" } }, "nbformat": 4, - "nbformat_minor": 1 + "nbformat_minor": 4 }