diff --git a/ipynb/SpellingBee.ipynb b/ipynb/SpellingBee.ipynb index 9fca877..eefa54c 100644 --- a/ipynb/SpellingBee.ipynb +++ b/ipynb/SpellingBee.ipynb @@ -8,50 +8,40 @@ "\n", "# Spelling Bee Puzzle\n", "\n", - "The [Jan. 3 2020 Riddler](https://fivethirtyeight.com/features/can-you-solve-the-vexing-vexillology/) is about the popular NY Times [Spelling Bee](https://www.nytimes.com/puzzles/spelling-bee) puzzle:\n", + "The [3 Jan. 2020 edition of the 538 Riddler](https://fivethirtyeight.com/features/can-you-solve-the-vexing-vexillology/) concerns the popular NYTimes [Spelling Bee](https://www.nytimes.com/puzzles/spelling-bee) puzzle:\n", "\n", - "*In this game, seven letters are arranged in a honeycomb lattice, with one letter in the center. Here’s the lattice from Dec. 24, 2019:*\n", + "> In this game, seven letters are arranged in a **honeycomb lattice**, with one letter in the center. Here’s the lattice from December 24, 2019:\n", + "> \n", + "> \n", + "> \n", + "> The goal is to identify as many words that meet the following criteria:\n", + "> 1. The word must be at least four letters long.\n", + "> 2. The word must include the central letter.\n", + "> 3. The word cannot include any letter beyond the seven given letters.\n", + ">\n", + ">Note that letters can be repeated. For example, the words GAME and AMALGAM are both acceptable words. Four-letter words are worth 1 point each, while five-letter words are worth 5 points, six-letter words are worth 6 points, seven-letter words are worth 7 points, etc. Words that use all of the seven letters in the honeycomb are known as “pangrams” and earn 7 bonus points (in addition to the points for the length of the word). So in the above example, MEGAPLEX is worth 15 points.\n", + ">\n", + "> ***Which seven-letter honeycomb results in the highest possible game score?*** To be a valid choice of seven letters, no letter can be repeated, it must not contain the letter S (that would be too easy) and there must be at least one pangram.\n", + ">\n", + "> For consistency, please use [this word list](https://norvig.com/ngrams/enable1.txt) to check your game score.\n", "\n", - "\n", "\n", - "*The goal is to identify as many words that meet the following criteria:*\n", "\n", - " (1) *The word must be at least four letters long.*\n", - " \n", - " (2) *The word must include the central letter.*\n", - " \n", - " (3) *The word cannot include any letter beyond the seven given letters.*\n", + "Since the referenced [word list](https://norvig.com/ngrams/enable1.txt) came from *my* web site (I didn't make up the list; it is a standard Scrabble word list that I happen to host a copy of), I felt somewhat compelled to solve this one. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Step 1: Words, Word Scores, and Pangrams\n", "\n", - "*Note that letters can be repeated. For example, the words GAME and AMALGAM are both acceptable words. Four-letter words are worth 1 point each, while five-letter words are worth 5 points, six-letter words are worth 6 points, seven-letter words are worth 7 points, etc. Words that use all of the seven letters in the honeycomb are known as “pangrams” and earn 7 bonus points (in addition to the points for the length of the word). So in the above example, MEGAPLEX is worth 15 points.*\n", + "Let's start by defining some basics:\n", "\n", - "***Which seven-letter honeycomb results in the highest possible game score?*** *To be a valid choice of seven letters, no letter can be repeated, it must not contain the letter S (that would be too easy) and there must be at least one pangram.*\n", - "\n", - "*For consistency, please use [this word list](https://norvig.com/ngrams/enable1.txt) to check your game score.*\n", - "\n", - "# My Approach\n", - "\n", - "Since the referenced word list came from **my** web site (I didn't make up the list; it is a standard Scrabble word list that I happen to host a copy of), I felt somewhat compelled to solve this one. \n", - "\n", - "Other word puzzles are hard because there are so many possibilities to consider. \n", - "But fortunately the honeycomb puzzle (unlike [Boggle](https://github.com/aimacode/aima-python/blob/master/search.py) or [Scrabble](Scrabble.ipynb)) deals with *unordered sets* of letters, not *ordered permutations* of letters. So, once we exclude the \"S\", there are only (25 choose 7) = 480,700 *sets* of seven letters to consider. A brute force approach could evaluate all of them (probably over the course of multiple hours). \n", - "\n", - "Fortunately, I noticed a better trick. The rules say that every valid honeycomb must contain a pangram. Therefore, it must be the case that every valid honeycomb **is** a pangram. How many pangrams could there be in the word list—maybe 10,000? It must be a lot less than the number of sets of 7 letters.\n", - "\n", - "So here's a broad sketch of my approach:\n", - "\n", - "- The **best honeycomb** is the one with the highest game score among all candidate honeycombs.\n", - "- A **candidate honeycomb** is any set of 7 letters that constitute a pangram word in the word list, with any one of the 7 letters as the center.\n", - "- A **pangram word** is a word with exactly 7 distinct letters.\n", - "- The **game score** for a honeycomb is the sum of the word scores for all the words that the honeycomb can make.\n", - "- The **word score** of a word is 1 for four-letter words, or else $n$ for $n$-letters plus a 7-point bonus for pangrams.\n", - "- A honeycomb **can make** a word if all the letters in the word are in the honeycomb, and the word contains the center letter.\n", - "- The **set of letters** in a word (or honeycomb) can be represented as a sorted string of distinct letters (e.g., the set of letters in \"AMALGAM\" is \"AGLM\"). \n", - "- A **honeycomb** is defined by two things, the set of seven letters, and the distinguished single center letter.\n", - "- The **word list** can ignore words that: are less than 4 letters long; have an S; or have more than 7 distinct letters.\n", - "\n", - "(Note: I could have used a `frozenset` to represent a set of letters, but a sorted string seemed simpler, and for debugging purposes, I'd rather be looking at `'AEGLMPX'` than at `frozenset({'A', 'E', 'G', 'L', 'M', 'P', 'X'})`).\n", - "\n", - "Each of these concepts can be implemented in a couple lines of code:" + "- A **valid word** is a string of (uppercase) letters, at least 4 letters, with no 's', and not more than 7 distinct letters.\n", + "- A **word list** is, well, a list of words.\n", + "- The **word score** is 1 for a four letter word, or the length of the word plus a bonus of 7 for a pangram.\n", + "- A **pangram** is a word with exactly 7 distinct letters.\n" ] }, { @@ -60,158 +50,88 @@ "metadata": {}, "outputs": [], "source": [ - "def best_honeycomb(words) -> tuple: \n", - " \"\"\"Return (score, honeycomb) for the honeycomb with highest game score on these words.\"\"\"\n", - " return max((game_score(h, words), h) for h in candidate_honeycombs(words))\n", - "\n", - "def candidate_honeycombs(words):\n", - " \"\"\"The pangram lettersets, each with all 7 centers.\"\"\"\n", - " pangrams = {letterset(w) for w in words if is_pangram(w)}\n", - " return (Honeycomb(pangram, center) for pangram in pangrams for center in pangram)\n", - "\n", - "def is_pangram(word) -> bool: \n", - " \"\"\"Does a word have exactly 7 distinct letters?\"\"\"\n", - " return len(set(word)) == 7\n", - "\n", - "def game_score(honeycomb, words) -> int:\n", - " \"\"\"The total score for this honeycomb; the sum of the word scores.\"\"\"\n", - " return sum(word_score(word) for word in words if can_make(honeycomb, word))\n", - "\n", - "def word_score(word) -> int: \n", - " \"\"\"The points for this word, including bonus for pangram.\"\"\"\n", - " bonus = (7 if is_pangram(word) else 0)\n", - " return (1 if len(word) == 4 else len(word) + bonus)\n", - "\n", - "def can_make(honeycomb, word) -> bool:\n", - " \"\"\"Can the honeycomb make this word?\"\"\"\n", - " (letters, center) = honeycomb\n", - " return center in word and all(L in letters for L in word)\n", - "\n", - "def letterset(word) -> str:\n", - " \"\"\"The set of letters in a word, as a sorted string.\n", - " For example, letterset('GLAM') == letterset('AMALGAM') == 'AGLM'.\"\"\"\n", - " return ''.join(sorted(set(word)))\n", - "\n", - "def Honeycomb(letters, center) -> tuple: return (letters, center)\n", - "\n", - "def wordlist(text) -> list:\n", - " \"\"\"A list of all the valid whitespace-separated words in text.\"\"\"\n", - " return [w for w in text.upper().split() \n", - " if len(w) >= 4 and 'S' not in w and len(set(w)) <= 7]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Experimentation and Small Test\n", - "\n", - "I'll make a tiny word list and start experimenting with it:" + "from typing import List, Set, Tuple, Dict\n", + "from collections import Counter, defaultdict, namedtuple\n", + "from itertools import combinations" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "['AMALGAM', 'GAME', 'GLAM', 'MEGAPLEX', 'CACCIATORE', 'EROTICA']" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "words = wordlist('amalgam amalgamation game games gem glam megaplex cacciatore erotica I me')\n", - "words" + "Word = str # Type for a word\n", + "\n", + "def valid_words(text) -> List[Word]:\n", + " \"\"\"A list of valid space-separated words in a string. Valid words \n", + " have at least 4 letters, no 'S', and no more than 7 distinct letters.\"\"\"\n", + " return [w for w in text.upper().split() \n", + " if len(w) >= 4 and 'S' not in w and len(set(w)) <= 7]\n", + "\n", + "def word_score(word) -> int: \n", + " \"\"\"The points for this word, including bonus for pangram.\"\"\"\n", + " return 1 if (len(word) == 4) else len(word) + 7 * is_pangram(word)\n", + "\n", + "def is_pangram(word) -> bool: \n", + " \"\"\"Does a word use all 7 letters (some maybe more than once)?\"\"\"\n", + " return len(set(word)) == 7" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Note that `I`, `me` and `gem` are too short, `games` has an `S` which is not allowed, and `amalgamation` has too many distinct letters (8). We're left with six valid words out of the original eleven. Here are examples of the functions in action:" + "I'll make a mini word list to experiment with: " ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'AMALGAM': 7,\n", - " 'GAME': 1,\n", - " 'GLAM': 1,\n", - " 'MEGAPLEX': 15,\n", - " 'CACCIATORE': 17,\n", - " 'EROTICA': 14}" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "{w: word_score(w) for w in words}" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'CACCIATORE', 'EROTICA', 'MEGAPLEX'}" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "{w for w in words if is_pangram(w)}" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'AMALGAM': 'AGLM',\n", - " 'GAME': 'AEGM',\n", - " 'GLAM': 'AGLM',\n", - " 'MEGAPLEX': 'AEGLMPX',\n", - " 'CACCIATORE': 'ACEIORT',\n", - " 'EROTICA': 'ACEIORT'}" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "{w: letterset(w) for w in words}" + "mini = valid_words('game amalgam amalgamation glam gem gems em megaplex cacciatore erotica')\n", + "mini" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Note that AMALGAM and GLAM have the same letterset, as do CACCIATORE and EROTICA. " + "Note that `gem` and `em` are too short, `gems` has an `s` which is not allowed, and `amalgamation` has too many distinct letters (8). We're left with six valid words out of the original ten. Here are examples of the other two functions in action:" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "{w: word_score(w) for w in mini}" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "{w for w in mini if is_pangram(w)}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Step 2: Honeycombs, Lettersets, and Game Scores\n", + "\n", + "A honeycomb lattice can be considered as a **set** of letters. The order of the letters doesn't matter; all that matters is:\n", + " 1. The set of seven letters in the honeycomb.\n", + " 2. The one distinguished center letter.\n", + " \n", + "Thus, we can represent a honeycomb as follows:\n", + " " ] }, { @@ -220,47 +140,41 @@ "metadata": {}, "outputs": [], "source": [ - "honeycomb = Honeycomb('AEGLMPX', 'G')" + "Honeycomb = namedtuple('Honeycomb', 'letters, center')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "I will represent a set of letters (from either a honeycomb or a word) as a sorted string. Why not a Python `set` or `frozenset`? Because a string takes up less space in memory, and its printed representation is more succint and easier to read when debugging. Compare:\n", + "- `frozenset({'A', 'C', 'E', 'I', 'O', 'R', 'T'})`\n", + "- `'ACEIORT'`\n", + "\n", + "I'll use the name `Letterset` for the type, and `letterset` for the function that converts a word to a set of letters:" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "{'AMALGAM': 7, 'GAME': 1, 'GLAM': 1, 'MEGAPLEX': 15}" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "{w: word_score(w) for w in words if can_make(honeycomb, w)}" + "Letterset = str # Type for sets of letters, like \"AGLM\"\n", + "\n", + "def letterset(word) -> Letterset:\n", + " \"\"\"The set of letters in a word, represented as a sorted str.\"\"\"\n", + " return ''.join(sorted(set(word)))" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "24" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ - "game_score(honeycomb, words)" + "honeycomb = Honeycomb(letterset('AEGLMPX'), 'G')\n", + "honeycomb" ] }, { @@ -271,7 +185,12 @@ { "data": { "text/plain": [ - "(31, ('ACEIORT', 'T'))" + "{'GAME': 'AEGM',\n", + " 'AMALGAM': 'AGLM',\n", + " 'GLAM': 'AGLM',\n", + " 'MEGAPLEX': 'AEGLMPX',\n", + " 'CACCIATORE': 'ACEIORT',\n", + " 'EROTICA': 'ACEIORT'}" ] }, "execution_count": 9, @@ -280,22 +199,121 @@ } ], "source": [ - "best_honeycomb(words)" + "{w: letterset(w) for w in mini}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "**We're done!** We know how to find the best honeycomb. But so far, we've only done it for the tiny word list. Let's look at the real word list.\n", + "Note that 'AMALGAM' and 'GLAM' have the same letterset, as do 'CACCIATORE' and 'EROTICA'. \n", "\n", - "# The enable1 Word List\n" + "The game score for a honeycomb is the sum of the word scores for all the words that the honeycomb can make. How do we know if a honeycomb can make a word? It can if (1) the word contains the honeycomb's center and (2) every letter in the word is in the honeycomb. " ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, + "outputs": [], + "source": [ + "def game_score(honeycomb, words) -> int:\n", + " \"\"\"The total score for this honeycomb.\"\"\"\n", + " return sum(word_score(w) for w in words if can_make(honeycomb, w))\n", + "\n", + "def can_make(honeycomb, word) -> bool:\n", + " \"\"\"Can the honeycomb make this word?\"\"\"\n", + " return honeycomb.center in word and all(L in honeycomb.letters for L in word)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "24" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "game_score(honeycomb, mini)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Step 3: Best Honeycomb\n", + "\n", + "This puzzle is different from many other word puzzles because it deals with *unordered sets* of letters, not *ordered permutations* of letters. That makes things easier. When I searched for an optimal 5×5 [Boggle](Boggle.ipynb) board, I couldn't exhaustively try all $26^{(5×5)} \\approx 10^{35}$ possibilites; I could only do hillclimbing to find a local maximum. But for Spelling Bee, it *is* feasible to try every possibility and get a guaranteed highest-scoring honeycomb. \n", + "\n", + "A key constraint of the game is that **there must be at least one pangram** in the set of words that a valid honeycomb can make. That means that every valid honeycomb must ***be*** a pangram letterset of one of the words in the word list. So my approach to find the best (highest scoring) honeycomb is:\n", + "\n", + " * Go through all the words and find all the valid honeycombs: the 7-letter pangram lettersets, with any of the 7 letters as center.\n", + " * Compute the game score for each valid honeycomb and return a best one.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "def best_honeycomb(words) -> Tuple[int, Honeycomb]: \n", + " \"\"\"Return (score, honeycomb) for a honeycomb with highest score on these words.\"\"\"\n", + " honeycombs = valid_honeycombs(map(letterset, words))\n", + " return max((game_score(h, words), h) for h in honeycombs)\n", + "\n", + "def valid_honeycombs(lettersets) -> List[Honeycomb]:\n", + " \"\"\"All valid Honeycombs that can be made from these lettersets.\"\"\"\n", + " pangram_lettersets = {s for s in lettersets if len(s) == 7}\n", + " return [Honeycomb(letters, center) \n", + " for letters in pangram_lettersets \n", + " for center in letters]" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(31, Honeycomb(letters='ACEIORT', center='T'))" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "best_honeycomb(mini)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**We're done!** We know how to find the best honeycomb. But so far, we've only done it for the mini word list. \n", + "\n", + "# Step 4: The enable1 Word List\n", + "\n", + "Here's the real word list, `enable1.txt`, and some counts derived from it:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, "outputs": [ { "name": "stdout", @@ -312,7 +330,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": 15, "metadata": {}, "outputs": [ { @@ -321,19 +339,19 @@ "44585" ] }, - "execution_count": 11, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "enable1 = wordlist(open('enable1.txt').read())\n", + "enable1 = valid_words(open('enable1.txt').read())\n", "len(enable1)" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 16, "metadata": {}, "outputs": [ { @@ -342,7 +360,7 @@ "14741" ] }, - "execution_count": 12, + "execution_count": 16, "metadata": {}, "output_type": "execute_result" } @@ -354,7 +372,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 17, "metadata": {}, "outputs": [ { @@ -363,19 +381,19 @@ "7986" ] }, - "execution_count": 13, + "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "pangram_sets = {letterset(w) for w in pangrams}\n", - "len(pangram_sets)" + "lettersets = {letterset(w) for w in pangrams}\n", + "len(lettersets)" ] }, { "cell_type": "code", - "execution_count": 14, + "execution_count": 18, "metadata": {}, "outputs": [ { @@ -384,41 +402,41 @@ "55902" ] }, - "execution_count": 14, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "_ * 7" + "len(valid_honeycombs(lettersets))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "So to recap on the number of words of various types in enable1:\n", + "So we have the following counts:\n", "\n", - " 172,820 total words\n", - " 44,585 valid words (eliminating \"S\" words, short words, 8+ letter words)\n", - " 14,741 pangram words\n", - " 7,986 unique pangram lettersets\n", - " 55,902 candidate honeycombs\n", + "- 172,820 words in the `enable1` word list\n", + "- 44,585 valid Spelling Bee words\n", + "- 14,741 pangram words \n", + "- 7,986 distinct pangram lettersets\n", + "- 55,902 (7 × 7,986) valid honeycombs\n", "\n", - "How long will it take to run `best_honeycomb(enable1)`? Let's estimate by checking how long it takes to compute the game score of a single honeycomb:" + "How long will it take to run `best_honeycomb(enable1)`? Most of the computation time is in `game_score` (which has to look at all 44,585 words), so let's estimate the total time by first checking how long it takes to compute the game score of a single honeycomb:" ] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 19, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 10.5 ms, sys: 286 µs, total: 10.8 ms\n", - "Wall time: 10.8 ms\n" + "CPU times: user 12.6 ms, sys: 260 µs, total: 12.9 ms\n", + "Wall time: 13 ms\n" ] }, { @@ -427,7 +445,7 @@ "153" ] }, - "execution_count": 15, + "execution_count": 19, "metadata": {}, "output_type": "execute_result" } @@ -440,68 +458,93 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "That's to compute one `game_score`. Multiply by 55,902 candidate honeycombs and we get somewhere in the 10 minute range. I could run `best_honeycomb(enable1)` right now and take a coffee break until it completes, but I'm predisposed to think that a puzzle like this deserves a more elegant solution. I know that [Project Euler](https://projecteuler.net/) designs their puzzles so that a good solution runs in less than a minute, so I'll make that my goal here.\n", - "\n", - "# Making it Faster\n", - "\n", - "Here's how I think about making a more efficient program:\n", - "\n", - "- We're doing a `game_score` for each of the 55,902 `candidate_honeycombs`. \n", - "- `game_score` has to **look at each word in the wordlist, and test if it is a subset of the honeycomb.**\n", - "- We can speed things up by flipping the test around: **look at each letter subset of the honeycomb, and test if it is in the word list.**\n", - "- By **letter subset** I mean a letter set containing a subset of the letters in the honeycomb, and definitely containing the center. So, for `Honeycomb('ACEIORT', 'T')` the letter subsets are `['T', 'AT', 'CT', 'ET', 'IT', 'OT', 'RT', 'ACT', 'AET', ...]`\n", - "- Why will flipping the test be faster? Because there are 44,585 words in the word list and only 64 letter subsets of a honeycomb. (A subset must include the center letter, and it may or may not include each of the other 6 letters, so there are exactly $2^6 = 64$ letter subsets of each pangram.)\n", - "- We're left with the problem of deciding if a letter subset is a word. In fact, a letter subset might correspond to multiple words (e.g. `'AGLM'` corresponds to both `GLAM` and `AMALGAM`). \n", - "- Ultimately we're more interested in the total number of points that a letter subset corresponds to, not in the individual word(s).\n", - "- So I will create a table of `{letter_subset: total_points}` giving the total number of word score points for all the words that correspond to the letter subset. I call this a `points_table`.\n", - "- Since the points table is independent of any honeycomb, I can compute it once and for all; I don't need to recompute it for each honeycomb.\n", - "- To compute `game_score`, just take the sum of the 64 letter subset entries in the points table.\n", - "\n", - "Here's the code. Notice I didn't want to redefine the global function `game_score` with a different signature, so instead I made it be a local function that references the local `pts_table`," + "About 13 milliseconds. How many minutes would that be for all 55,902 valid honeycombs?" ] }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 20, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "12.1121" + ] + }, + "execution_count": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "from collections import Counter, defaultdict\n", - "from itertools import combinations\n", - "\n", - "def best_honeycomb(words) -> tuple: \n", - " \"\"\"Return (score, honeycomb) for the honeycomb with highest score on these words.\"\"\"\n", - " pts_table = points_table(words)\n", - " def game_score(honeycomb) -> int: \n", - " return sum(pts_table[s] for s in letter_subsets(honeycomb))\n", - " return max((game_score(h), h) for h in candidate_honeycombs(words))\n", - "\n", - "def points_table(words) -> dict:\n", - " \"\"\"Return a dict of {letterset: points} from words.\"\"\"\n", - " table = Counter()\n", - " for w in words:\n", - " table[letterset(w)] += word_score(w)\n", - " return table\n", - "\n", - "def letter_subsets(honeycomb) -> list:\n", - " \"\"\"The 64 subsets of the letters in the honeycomb (that must contain the center letter).\"\"\"\n", - " (letters, center) = honeycomb\n", - " return [''.join(subset) \n", - " for n in range(1, 8) \n", - " for subset in combinations(letters, n)\n", - " if center in subset]" + ".013 * 55902 / 60" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Let's get a feel for how this works. First the `letter_subsets`:" + "About 12 minutes. I could run `best_honeycomb(enable1)` right now and take a coffee break until it completes, but I think that a puzzle like this deserves a more elegant solution. I'd like to get the run time under a minute (as is suggested in [Project Euler](https://projecteuler.net/)), and I have an idea how to do it.\n", + "\n", + "# Step 5: Making it Faster\n", + "\n", + "Here's my plan for a more efficient program:\n", + "\n", + "1. Keep the same strategy of trying every pangram letterset, but do some precomputation that will make `game_score` much faster.\n", + "1. The precomputation is: compute the `letterset` and `word_score` for each word, and make a table of `{letterset: total_points}` giving the total number of points that can be made with each letterset. I call this a **points table**.\n", + "3. These calculations are independent of the honeycomb, so they need to be done only once, not 55,902 times. \n", + "4. For each valid honeycomb, pass it and the points table to `game_score2` (we changed the name because the interface has changed). In `game_score2`, generate every valid **subset** of the letters in the honeycomb. A valid subset must include the center letter, and it may or may not include each of the other 6 letters, so there are exactly $2^6 = 64$ valid subsets. The function `letter_subsets(honeycomb)` computes these. \n", + "5. To compute `game_score`, just take the sum of the 64 subset entries in the points table.\n", + "\n", + "\n", + "That means that in `game_score` we no longer need to iterate over 44,585 words and check if each word is a subset of the honeycomb. Instead we iterate over the 64 subsets of the honeycomb and for each one check—in one table lookup—whether it is a word (or more than word) and how many total points those word(s) score. Since 64 < 44,585, that's a nice optimization!\n", + "\n", + "\n", + "Here's the code." ] }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 21, + "metadata": {}, + "outputs": [], + "source": [ + "def best_honeycomb(words) -> Tuple[int, Honeycomb]: \n", + " \"\"\"Return (score, honeycomb) for the honeycomb with highest score on these words.\"\"\"\n", + " pts_table = points_table(words)\n", + " return max((game_score2(h, pts_table), h)\n", + " for h in valid_honeycombs(pts_table))\n", + "\n", + "def points_table(words) -> Dict[Letterset, int]:\n", + " \"\"\"Return a dict of {letterset: points} from words.\"\"\"\n", + " table = Counter()\n", + " for w in words:\n", + " table[letterset(w)] += word_score(w)\n", + " return table\n", + "\n", + "def letter_subsets(honeycomb) -> List[Letterset]:\n", + " \"\"\"The 64 subsets of the letters in the honeycomb, always including the center letter.\"\"\"\n", + " return [''.join(subset) \n", + " for n in range(1, 8) \n", + " for subset in combinations(honeycomb.letters, n)\n", + " if honeycomb.center in subset]\n", + "\n", + "def game_score2(honeycomb, pts_table) -> int:\n", + " \"\"\"The total score for this honeycomb, given a points_table.\"\"\"\n", + " return sum(pts_table[s] for s in letter_subsets(honeycomb))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's get a feel for how this works. First the `letter_subsets` (a 4-letter honeycomb makes $2^3 = 8$ subsets; 7-letter honeycombs make 64):" + ] + }, + { + "cell_type": "code", + "execution_count": 22, "metadata": {}, "outputs": [ { @@ -510,14 +553,33 @@ "['C', 'AC', 'BC', 'CD', 'ABC', 'ACD', 'BCD', 'ABCD']" ] }, - "execution_count": 17, + "execution_count": 22, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "# A 4-letter honeycomb makes 2**3 = 8 subsets; 7-letter honeycombs make 2**7 == 64\n", - "letter_subsets(('ABCD', 'C')) " + "letter_subsets(Honeycomb('ABCD', 'C')) " + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['GAME', 'AMALGAM', 'GLAM', 'MEGAPLEX', 'CACCIATORE', 'EROTICA']" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "mini # Remind me again what the mini word list is?" ] }, { @@ -529,73 +591,48 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 24, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "['AMALGAM', 'GAME', 'GLAM', 'MEGAPLEX', 'CACCIATORE', 'EROTICA']" + "Counter({'AEGM': 1, 'AGLM': 8, 'AEGLMPX': 15, 'ACEIORT': 31})" ] }, - "execution_count": 18, + "execution_count": 24, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "words # Remind me again what the words are?" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Counter({'AGLM': 8, 'AEGM': 1, 'AEGLMPX': 15, 'ACEIORT': 31})" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "points_table(words)" + "points_table(mini)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "The letterset `'ACEIORT'` gets 31 points, 17 for CACCIATORE and 14 for EROTICA, and the letterset `'AGLM'` gets 8 points, 7 for AMALGAM and 1 for GLAM. The other lettersets represent one word each. " - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Let's test that `best_honeycomb(words)` gets the same answer as before, and that the points table has the same set of pangrams as before." + "The letterset `'AGLM'` gets 8 points, 7 for AMALGAM and 1 for GLAM. `'ACEIORT'` gets 31 points, 17 for CACCIATORE and 14 for EROTICA. The other lettersets represent one word each. \n", + "\n", + "Let's make sure we haven't broken the `game_score` and `best_honeycomb` functions:" ] }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 25, "metadata": {}, "outputs": [], "source": [ - "assert best_honeycomb(words) == (31, ('ACEIORT', 'T'))\n", - "assert pangram_sets == {s for s in points_table(enable1) if len(s) == 7}" + "assert game_score2(honeycomb, points_table(mini)) == 24\n", + "assert best_honeycomb(mini) == (31, Honeycomb('ACEIORT', 'T'))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "# The Solution" + "# Step 6: The Solution" ] }, { @@ -607,24 +644,24 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 26, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 1.84 s, sys: 4.03 ms, total: 1.84 s\n", - "Wall time: 1.85 s\n" + "CPU times: user 2.2 s, sys: 6.33 ms, total: 2.21 s\n", + "Wall time: 2.21 s\n" ] }, { "data": { "text/plain": [ - "(3898, ('AEGINRT', 'R'))" + "(3898, Honeycomb(letters='AEGINRT', center='R'))" ] }, - "execution_count": 21, + "execution_count": 26, "metadata": {}, "output_type": "execute_result" } @@ -637,81 +674,10 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "**Wow! 3898 is a high score!** And it took only 2 seconds to find it!\n", + "**Wow! 3898 is a high score!** And it took only 2 seconds of computation to find it!\n", "\n" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Making it Even Fasterer\n", - "\n", - "OK, that was 30 times faster than my goal of one minute. It was a nice optimization to look at only 64 letter subsets rather than 44,585 words. But I'm still looking at 103,187 honeycombs, and I feel that some of them are a waste of time. Consider the pangram \"JUKEBOX\". With the uncommon letters J, K, and X, it does not look like a high-scoring honeycomb, no matter what center we choose. So why waste time trying all seven centers? Here's the outline of a faster `best_honeycomb`:\n", - "\n", - "- Go through the pangrams as before\n", - "- However, always keep track of the best score and the best honeycomb that we have found so far.\n", - "- For each new pangram, first see how many points it would score if we ignore the restrriction that a particular center letter must be used. (I compute that with `game_score('')`, where again `game_score` is a local function,\n", - "this time with access to both `pts_table` and `subsets`.)\n", - "- Only if `game_score('')` is better than the best score found so far, then evaluate `game_score(C)` for each of the seven possible centers `C`.\n", - "- In the end, return the best score and the best honeycomb." - ] - }, - { - "cell_type": "code", - "execution_count": 22, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 439 ms, sys: 1.93 ms, total: 441 ms\n", - "Wall time: 441 ms\n" - ] - }, - { - "data": { - "text/plain": [ - "(3898, ('AEGINRT', 'R'))" - ] - }, - "execution_count": 22, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "def best_honeycomb(words) -> tuple: \n", - " \"\"\"Return (score, honeycomb) for the honeycomb with highest score on these words.\"\"\"\n", - " best_score, best_honeycomb = 0, None\n", - " pts_table = points_table(words)\n", - " pangrams = (s for s in pts_table if len(s) == 7)\n", - " for pangram in pangrams:\n", - " subsets = string_subsets(pangram)\n", - " def game_score(center): return sum(pts_table[s] for s in subsets if center in s)\n", - " if game_score('') > best_score:\n", - " for C in pangram:\n", - " if game_score(C) > best_score:\n", - " best_score, best_honeycomb = game_score(C), Honeycomb(pangram, C)\n", - " return (best_score, best_honeycomb)\n", - "\n", - "def string_subsets(letters) -> list:\n", - " \"\"\"All subsets of a string.\"\"\"\n", - " return [''.join(s) \n", - " for n in range(len(letters) + 1) \n", - " for s in combinations(letters, n)]\n", - "\n", - "%time best_honeycomb(enable1)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Looking good! We get the same answer, and in about half a second, four times faster than before. " - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -720,12 +686,12 @@ "\n", "I'm curious about a bunch of things.\n", "\n", - "What's the highest-scoring individual word?" + "* What's the highest-scoring individual word?" ] }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 27, "metadata": {}, "outputs": [ { @@ -734,7 +700,7 @@ "'ANTITOTALITARIAN'" ] }, - "execution_count": 23, + "execution_count": 27, "metadata": {}, "output_type": "execute_result" } @@ -747,12 +713,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "What are some of the pangrams?" + "* What are some of the pangrams?" ] }, { "cell_type": "code", - "execution_count": 24, + "execution_count": 28, "metadata": {}, "outputs": [ { @@ -775,7 +741,7 @@ " 'UTOPIAN']" ] }, - "execution_count": 24, + "execution_count": 28, "metadata": {}, "output_type": "execute_result" } @@ -788,27 +754,33 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "What's the breakdown of reasons why words are invalid?\n" + "* What's the breakdown of reasons why words are invalid?" ] }, { "cell_type": "code", - "execution_count": 25, + "execution_count": 29, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[('S', 103913), ('valid', 44585), ('>7', 23400), ('<4', 922)]" + "[('has an S', 103913),\n", + " ('valid', 44585),\n", + " ('more than 7 distinct letters', 23400),\n", + " ('less than 4 letters', 922)]" ] }, - "execution_count": 25, + "execution_count": 29, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "Counter('S' if 'S' in w else '<4' if len(w) < 4 else '>7' if len(set(w)) > 7 else 'valid'\n", + "Counter('has an S' if 'S' in w else \n", + " 'less than 4 letters' if len(w) < 4 else \n", + " 'more than 7 distinct letters' if len(set(w)) > 7 else \n", + " 'valid'\n", " for w in open('enable1.txt').read().upper().split()).most_common()" ] }, @@ -818,12 +790,12 @@ "source": [ "There are more than twice as many words with an 'S' as there are valid words.\n", "\n", - "About the `points_table`: How many different letter subsets are there? " + "* About the `points_table`: How many different letter subsets are there? " ] }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 30, "metadata": {}, "outputs": [ { @@ -832,7 +804,7 @@ "21661" ] }, - "execution_count": 26, + "execution_count": 30, "metadata": {}, "output_type": "execute_result" } @@ -848,12 +820,12 @@ "source": [ "That means there's about two valid words for each letterset.\n", "\n", - "Which lettersets score the most? The least?" + "* Which letter subsets score the most?" ] }, { "cell_type": "code", - "execution_count": 27, + "execution_count": 31, "metadata": {}, "outputs": [ { @@ -868,47 +840,36 @@ " ('AGINORT', 380),\n", " ('ADEINRT', 318),\n", " ('CENORTU', 318),\n", - " ('ACDEIRT', 307),\n", - " ('AEGILNR', 304),\n", - " ('AEILNRT', 283),\n", - " ('AEGINR', 270),\n", - " ('ACINORT', 266),\n", - " ('ADENRTU', 265),\n", - " ('EGILNRT', 259),\n", - " ('AILNORT', 252),\n", - " ('DEGINR', 251),\n", - " ('AEIMNRT', 242),\n", - " ('ACELORT', 241)]" + " ('ACDEIRT', 307)]" ] }, - "execution_count": 27, + "execution_count": 31, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "pts.most_common(20)" + "pts.most_common(10)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The best honeycomb is also the highest scoring letter subset on its own (although it only gets 832 of the 3,898 total points from using all seven letters).\n", + "\n", + "* Which letter subsets score the least points?" ] }, { "cell_type": "code", - "execution_count": 28, + "execution_count": 32, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[('IRY', 1),\n", - " ('AGOY', 1),\n", - " ('GHOY', 1),\n", - " ('GIOY', 1),\n", - " ('EKOY', 1),\n", - " ('ORUY', 1),\n", - " ('EOWY', 1),\n", - " ('ANUY', 1),\n", - " ('AGUY', 1),\n", - " ('ELUY', 1),\n", - " ('ANYZ', 1),\n", + "[('ANYZ', 1),\n", " ('BEUZ', 1),\n", " ('EINZ', 1),\n", " ('EKRZ', 1),\n", @@ -920,13 +881,40 @@ " ('EMYZ', 1)]" ] }, - "execution_count": 28, + "execution_count": 32, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "pts.most_common()[-20:]" + "pts.most_common()[-10:]" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "824" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "sum(v == 1 for v in pts.values()) # How many letter subsets score 1 point?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There are 824 letter subsets that only appear in one four-letter word, for one point." ] }, { @@ -935,12 +923,12 @@ "source": [ "# Fancy Report\n", "\n", - "I'd like to see the actual words that each honeycomb can make, in addition to the total score, and I'm curious about how the words are divided up by letterset. Here's a function to provide such a report. I remembered that there is a `fill` function in Python (it is in the `textwrap` module) but this all turned out to be more complicated than I expected. I guess it is difficult to create a practical extraction and reporting tool. I feel you, Larry Wall." + "I'd like to see the actual words that each honeycomb can make, in addition to the total score, and I'm curious about how the words are divided up by letterset. Here's a function to provide such a report. I remembered that there is a `fill` function in Python (it is in the `textwrap` module) but this turned out to be a lot more complicated than I expected. I guess it is difficult to create a practical extraction and reporting tool. I feel you, Larry Wall." ] }, { "cell_type": "code", - "execution_count": 29, + "execution_count": 34, "metadata": { "scrolled": false }, @@ -951,28 +939,28 @@ "def report(words, honeycomb=None):\n", " \"\"\"Print stats, words, and word scores for the given honeycomb (or \n", " for the best honeycomb if no honeycomb is given) over the given word list.\"\"\"\n", - " optimal = (\"\" if honeycomb else \"optimal \")\n", - " if honeycomb is None:\n", - " _, honeycomb = best_honeycomb(words)\n", + " bins = group_by(words, letterset)\n", + " adj = (\"given\" if honeycomb else \"optimal\")\n", + " honeycomb = honeycomb or best_honeycomb(words)[1]\n", + " points = game_score(honeycomb, words)\n", " subsets = letter_subsets(honeycomb)\n", - " bins = group_by(words, letterset)\n", - " score = sum(word_score(w) for w in words if letterset(w) in subsets)\n", - " nwords = sum(len(bins[s]) for s in subsets)\n", + " nwords = sum(len(bins[s]) for s in subsets)\n", " print(f'For this list of {Ns(len(words), \"word\")}:')\n", - " print(f'The {optimal}honeycomb {honeycomb} forms '\n", - " f'{Ns(nwords, \"word\")} for {Ns(score, \"point\")}.')\n", - " print(f'Here are the words formed by each subset, with pangrams first:\\n')\n", + " print(f'The {adj} honeycomb {honeycomb.letters} with center {honeycomb.center}'\n", + " f' makes {Ns(nwords, \"word\")} for {Ns(points, \"point\")}:\\n')\n", + " indent = ' ' * 4\n", " for s in sorted(subsets, key=lambda s: (-len(s), s)):\n", " if bins[s]:\n", " pts = sum(word_score(w) for w in bins[s])\n", - " print(f'{s} forms {Ns(len(bins[s]), \"word\")} for {Ns(pts, \"point\")}:')\n", + " print(f'{s} makes {Ns(len(bins[s]), \"word\")} for {Ns(pts, \"point\")}:')\n", " words = [f'{w}({word_score(w)})' for w in sorted(bins[s])]\n", " print(fill(' '.join(words), width=80,\n", - " initial_indent=' ', subsequent_indent=' '))\n", + " initial_indent=indent, subsequent_indent=indent))\n", " \n", - "def Ns(n, thing, plural=None):\n", - " \"\"\"Ns(3, 'bear') => '3 bears'; Ns(1, 'world') => '1 world'\"\"\" \n", - " return f\"{n:,d} {thing if n == 1 else plurtal}\"\n", + "def Ns(n, noun):\n", + " \"\"\"A string with `n` followed by the plural or singular of noun:\n", + " Ns(3, 'bear') => '3 bears'; Ns(1, 'world') => '1 world'\"\"\" \n", + " return f\"{n:,d} {noun}{'' if n == 1 else 's'}\"\n", "\n", "def group_by(items, key):\n", " \"Group items into bins of a dict, each bin keyed by key(item).\"\n", @@ -984,7 +972,7 @@ }, { "cell_type": "code", - "execution_count": 30, + "execution_count": 35, "metadata": {}, "outputs": [ { @@ -992,25 +980,24 @@ "output_type": "stream", "text": [ "For this list of 6 words:\n", - "The honeycomb ('AEGLMPX', 'G') forms 4 words for 24 points.\n", - "Here are the words formed by each subset, with pangrams first:\n", + "The given honeycomb AEGLMPX with center G makes 4 words for 24 points:\n", "\n", - "AEGLMPX forms 1 word for 15 points:\n", + "AEGLMPX makes 1 word for 15 points:\n", " MEGAPLEX(15)\n", - "AEGM forms 1 word for 1 point:\n", + "AEGM makes 1 word for 1 point:\n", " GAME(1)\n", - "AGLM forms 2 words for 8 points:\n", + "AGLM makes 2 words for 8 points:\n", " AMALGAM(7) GLAM(1)\n" ] } ], "source": [ - "report(words, honeycomb)" + "report(mini, honeycomb)" ] }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 36, "metadata": { "scrolled": false }, @@ -1020,10 +1007,9 @@ "output_type": "stream", "text": [ "For this list of 44,585 words:\n", - "The optimal honeycomb ('AEGINRT', 'R') forms 537 words for 3,898 points.\n", - "Here are the words formed by each subset, with pangrams first:\n", + "The optimal honeycomb AEGINRT with center R makes 537 words for 3,898 points:\n", "\n", - "AEGINRT forms 50 words for 832 points:\n", + "AEGINRT makes 50 words for 832 points:\n", " AERATING(15) AGGREGATING(18) ARGENTINE(16) ARGENTITE(16) ENTERTAINING(19)\n", " ENTRAINING(17) ENTREATING(17) GARNIERITE(17) GARTERING(16) GENERATING(17)\n", " GNATTIER(15) GRANITE(14) GRATINE(14) GRATINEE(15) GRATINEEING(18)\n", @@ -1035,147 +1021,147 @@ " RETAGGING(16) RETAINING(16) RETARGETING(18) RETEARING(16) RETRAINING(17)\n", " RETREATING(17) TANGERINE(16) TANGIER(14) TARGETING(16) TATTERING(16)\n", " TEARING(14) TREATING(15)\n", - "AEGINR forms 35 words for 270 points:\n", + "AEGINR makes 35 words for 270 points:\n", " AGINNER(7) AGREEING(8) ANEARING(8) ANERGIA(7) ANGERING(8) ANGRIER(7)\n", " ARGININE(8) EARING(6) EARNING(7) EARRING(7) ENGRAIN(7) ENGRAINING(10)\n", " ENRAGING(8) GAINER(6) GANGRENING(10) GARNERING(9) GEARING(7) GRAINER(7)\n", " GRAINIER(8) GRANNIE(7) GREGARINE(9) NAGGIER(7) NEARING(7) RANGIER(7)\n", " REAGIN(6) REARING(7) REARRANGING(11) REEARNING(9) REENGAGING(10) REGAIN(6)\n", " REGAINER(8) REGAINING(9) REGEARING(9) REGINA(6) REGINAE(7)\n", - "AEGIRT forms 5 words for 34 points:\n", + "AEGIRT makes 5 words for 34 points:\n", " AIGRET(6) AIGRETTE(8) GAITER(6) IRRIGATE(8) TRIAGE(6)\n", - "AEGNRT forms 13 words for 94 points:\n", + "AEGNRT makes 13 words for 94 points:\n", " ARGENT(6) GARNET(6) GENERATE(8) GRANTEE(7) GRANTER(7) GREATEN(7) NEGATER(7)\n", " REAGENT(7) REGENERATE(10) REGNANT(7) REGRANT(7) TANAGER(7) TEENAGER(8)\n", - "AEINRT forms 30 words for 232 points:\n", + "AEINRT makes 30 words for 232 points:\n", " ARENITE(7) ATTAINER(8) ENTERTAIN(9) ENTERTAINER(11) ENTRAIN(7) ENTRAINER(9)\n", " INERRANT(8) INERTIA(7) INERTIAE(8) INTENERATE(10) INTREAT(7) ITERANT(7)\n", " ITINERANT(9) ITINERATE(9) NATTIER(7) NITRATE(7) RATINE(6) REATTAIN(8)\n", " REINITIATE(10) RETAIN(6) RETAINER(8) RETINA(6) RETINAE(7) RETIRANT(8)\n", " RETRAIN(7) TERRAIN(7) TERTIAN(7) TRAINEE(7) TRAINER(7) TRIENNIA(8)\n", - "AGINRT forms 21 words for 167 points:\n", + "AGINRT makes 21 words for 167 points:\n", " AIRTING(7) ATTIRING(8) GRANITA(7) GRANTING(8) GRATIN(6) GRATING(7)\n", " INGRATIATING(12) INTRIGANT(9) IRRIGATING(10) IRRITATING(10) NARRATING(9)\n", " NITRATING(9) RANTING(7) RATING(6) RATTING(7) TARING(6) TARRING(7) TARTING(7)\n", " TITRATING(9) TRAINING(8) TRIAGING(8)\n", - "EGINRT forms 26 words for 218 points:\n", + "EGINRT makes 26 words for 218 points:\n", " ENGIRT(6) ENTERING(8) GETTERING(9) GITTERN(7) GREETING(8) IGNITER(7)\n", " INTEGER(7) INTERNING(9) INTERRING(9) REENTERING(10) REGREETING(10)\n", " REGRETTING(10) REIGNITE(8) REIGNITING(10) REINTERRING(11) RENTING(7)\n", " RETINTING(9) RETIRING(8) RETTING(7) RINGENT(7) TEETERING(9) TENTERING(9)\n", " TIERING(7) TITTERING(9) TREEING(7) TRIGGERING(10)\n", - "AEGNR forms 18 words for 120 points:\n", + "AEGNR makes 18 words for 120 points:\n", " ANGER(5) ARRANGE(7) ARRANGER(8) ENGAGER(7) ENRAGE(6) GANGER(6) GANGRENE(8)\n", " GARNER(6) GENERA(6) GRANGE(6) GRANGER(7) GREENGAGE(9) NAGGER(6) RANGE(5)\n", " RANGER(6) REARRANGE(9) REENGAGE(8) REGNA(5)\n", - "AEGRT forms 19 words for 123 points:\n", + "AEGRT makes 19 words for 123 points:\n", " AGGREGATE(9) ERGATE(6) ETAGERE(7) GARGET(6) GARRET(6) GARTER(6) GRATE(5)\n", " GRATER(6) GREAT(5) GREATER(7) REAGGREGATE(11) REGATTA(7) REGRATE(7) RETAG(5)\n", " RETARGET(8) TAGGER(6) TARGE(5) TARGET(6) TERGA(5)\n", - "AEINR forms 3 words for 19 points:\n", + "AEINR makes 3 words for 19 points:\n", " INANER(6) NARINE(6) RAINIER(7)\n", - "AEIRT forms 20 words for 135 points:\n", + "AEIRT makes 20 words for 135 points:\n", " ARIETTA(7) ARIETTE(7) ARTIER(6) ATTIRE(6) ATTRITE(7) IRATE(5) IRATER(6)\n", " IRRITATE(8) ITERATE(7) RATITE(6) RATTIER(7) REITERATE(9) RETIA(5)\n", " RETIARII(8) TARRIER(7) TATTIER(7) TEARIER(7) TERAI(5) TERRARIA(8) TITRATE(7)\n", - "AENRT forms 19 words for 132 points:\n", + "AENRT makes 19 words for 132 points:\n", " ANTEATER(8) ANTRE(5) ENTERA(6) ENTRANT(7) ENTREAT(7) ERRANT(6) NARRATE(7)\n", " NARRATER(8) NATTER(6) NEATER(6) RANTER(6) RATTEEN(7) RATTEN(6) RATTENER(8)\n", " REENTRANT(9) RETREATANT(10) TANNER(6) TERNATE(7) TERRANE(7)\n", - "AGINR forms 19 words for 138 points:\n", + "AGINR makes 19 words for 138 points:\n", " AGRARIAN(8) AIRING(6) ANGARIA(7) ARRAIGN(7) ARRAIGNING(10) ARRANGING(9)\n", " GARAGING(8) GARNI(5) GARRING(7) GNARRING(8) GRAIN(5) GRAINING(8) INGRAIN(7)\n", " INGRAINING(10) RAGGING(7) RAGING(6) RAINING(7) RANGING(7) RARING(6)\n", - "AGIRT forms 1 word for 5 points:\n", + "AGIRT makes 1 word for 5 points:\n", " TRAGI(5)\n", - "AGNRT forms 1 word for 5 points:\n", + "AGNRT makes 1 word for 5 points:\n", " GRANT(5)\n", - "AINRT forms 9 words for 64 points:\n", + "AINRT makes 9 words for 64 points:\n", " ANTIAIR(7) ANTIAR(6) ANTIARIN(8) INTRANT(7) IRRITANT(8) RIANT(5) TITRANT(7)\n", " TRAIN(5) TRINITARIAN(11)\n", - "EGINR forms 24 words for 186 points:\n", + "EGINR makes 24 words for 186 points:\n", " ENGINEER(8) ENGINEERING(11) ERRING(6) GINGER(6) GINGERING(9) GINNER(6)\n", " GINNIER(7) GREEING(7) GREENIE(7) GREENIER(8) GREENING(8) GRINNER(7)\n", " NIGGER(6) REENGINEER(10) REENGINEERING(13) REGREENING(10) REIGN(5)\n", " REIGNING(8) REINING(7) RENEGING(8) RENIG(5) RENIGGING(9) RERIGGING(9)\n", " RINGER(6)\n", - "EGIRT forms 4 words for 27 points:\n", + "EGIRT makes 4 words for 27 points:\n", " GRITTIER(8) TERGITE(7) TIGER(5) TRIGGER(7)\n", - "EGNRT forms 2 words for 12 points:\n", + "EGNRT makes 2 words for 12 points:\n", " GERENT(6) REGENT(6)\n", - "EINRT forms 29 words for 190 points:\n", + "EINRT makes 29 words for 190 points:\n", " ENTIRE(6) INERT(5) INTER(5) INTERN(6) INTERNE(7) INTERNEE(8) INTERTIE(8)\n", " NETTIER(7) NITER(5) NITERIE(7) NITRE(5) NITRITE(7) NITTIER(7) REINTER(7)\n", " RENITENT(8) RENTIER(7) RETINE(6) RETINENE(8) RETINITE(8) RETINT(6)\n", " TEENIER(7) TENTIER(7) TERRINE(7) TINIER(6) TINNER(6) TINNIER(7) TINTER(6)\n", " TRIENE(6) TRINE(5)\n", - "GINRT forms 6 words for 43 points:\n", + "GINRT makes 6 words for 43 points:\n", " GIRTING(7) GRITTING(8) RINGGIT(7) TIRING(6) TRIGGING(8) TRINING(7)\n", - "AEGR forms 17 words for 84 points:\n", + "AEGR makes 17 words for 84 points:\n", " AGER(1) AGGER(5) AGREE(5) ARREARAGE(9) EAGER(5) EAGERER(7) EAGRE(5) EGGAR(5)\n", " GAGER(5) GAGGER(6) GARAGE(6) GEAR(1) RAGE(1) RAGEE(5) RAGGEE(6) REGEAR(6)\n", " REGGAE(6)\n", - "AEIR forms 4 words for 22 points:\n", + "AEIR makes 4 words for 22 points:\n", " AERIE(5) AERIER(6) AIRER(5) AIRIER(6)\n", - "AENR forms 9 words for 40 points:\n", + "AENR makes 9 words for 40 points:\n", " ANEAR(5) ARENA(5) EARN(1) EARNER(6) NEAR(1) NEARER(6) RANEE(5) REEARN(6)\n", " RERAN(5)\n", - "AERT forms 24 words for 127 points:\n", + "AERT makes 24 words for 127 points:\n", " AERATE(6) ARETE(5) EATER(5) ERRATA(6) RATE(1) RATER(5) RATTER(6) REATA(5)\n", " RETEAR(6) RETREAT(7) RETREATER(9) TARE(1) TARRE(5) TARTER(6) TARTRATE(8)\n", " TATER(5) TATTER(6) TEAR(1) TEARER(6) TERRA(5) TERRAE(6) TETRA(5) TREAT(5)\n", " TREATER(7)\n", - "AGIR forms 2 words for 6 points:\n", + "AGIR makes 2 words for 6 points:\n", " AGRIA(5) RAGI(1)\n", - "AGNR forms 5 words for 13 points:\n", + "AGNR makes 5 words for 13 points:\n", " GNAR(1) GNARR(5) GRAN(1) GRANA(5) RANG(1)\n", - "AGRT forms 3 words for 13 points:\n", + "AGRT makes 3 words for 13 points:\n", " GRAT(1) RAGTAG(6) TAGRAG(6)\n", - "AINR forms 4 words for 8 points:\n", + "AINR makes 4 words for 8 points:\n", " AIRN(1) NAIRA(5) RAIN(1) RANI(1)\n", - "AIRT forms 5 words for 21 points:\n", + "AIRT makes 5 words for 21 points:\n", " AIRT(1) ATRIA(5) RIATA(5) TIARA(5) TRAIT(5)\n", - "ANRT forms 10 words for 50 points:\n", + "ANRT makes 10 words for 50 points:\n", " ANTRA(5) ARRANT(6) RANT(1) RATAN(5) RATTAN(6) TANTARA(7) TANTRA(6) TARN(1)\n", " TARTAN(6) TARTANA(7)\n", - "EGIR forms 3 words for 17 points:\n", + "EGIR makes 3 words for 17 points:\n", " GREIGE(6) RERIG(5) RIGGER(6)\n", - "EGNR forms 6 words for 37 points:\n", + "EGNR makes 6 words for 37 points:\n", " GENRE(5) GREEN(5) GREENER(7) REGREEN(7) RENEGE(6) RENEGER(7)\n", - "EGRT forms 7 words for 45 points:\n", + "EGRT makes 7 words for 45 points:\n", " EGRET(5) GETTER(6) GREET(5) GREETER(7) REGREET(7) REGRET(6) REGRETTER(9)\n", - "EINR forms 4 words for 17 points:\n", + "EINR makes 4 words for 17 points:\n", " INNER(5) REIN(1) RENIN(5) RENNIN(6)\n", - "EIRT forms 17 words for 87 points:\n", + "EIRT makes 17 words for 87 points:\n", " RETIE(5) RETIRE(6) RETIREE(7) RETIRER(7) RITE(1) RITTER(6) TERRIER(7)\n", " TERRIT(6) TIER(1) TIRE(1) TITER(5) TITRE(5) TITTER(6) TITTERER(8) TRIER(5)\n", " TRITE(5) TRITER(6)\n", - "ENRT forms 19 words for 104 points:\n", + "ENRT makes 19 words for 104 points:\n", " ENTER(5) ENTERER(7) ENTREE(6) ETERNE(6) NETTER(6) REENTER(7) RENNET(6)\n", " RENT(1) RENTE(5) RENTER(6) RETENE(6) TEENER(6) TENNER(6) TENTER(6) TERN(1)\n", " TERNE(5) TERREEN(7) TERRENE(7) TREEN(5)\n", - "GINR forms 9 words for 44 points:\n", + "GINR makes 9 words for 44 points:\n", " GIRN(1) GIRNING(7) GRIN(1) GRINNING(8) IRING(5) RIGGING(7) RING(1)\n", " RINGING(7) RINNING(7)\n", - "GIRT forms 3 words for 3 points:\n", + "GIRT makes 3 words for 3 points:\n", " GIRT(1) GRIT(1) TRIG(1)\n", - "AER forms 7 words for 25 points:\n", + "AER makes 7 words for 25 points:\n", " AREA(1) AREAE(5) ARREAR(6) RARE(1) RARER(5) REAR(1) REARER(6)\n", - "AGR forms 2 words for 2 points:\n", + "AGR makes 2 words for 2 points:\n", " AGAR(1) RAGA(1)\n", - "AIR forms 2 words for 2 points:\n", + "AIR makes 2 words for 2 points:\n", " ARIA(1) RAIA(1)\n", - "ART forms 5 words for 24 points:\n", + "ART makes 5 words for 24 points:\n", " ATTAR(5) RATATAT(7) TART(1) TARTAR(6) TATAR(5)\n", - "EGR forms 4 words for 15 points:\n", + "EGR makes 4 words for 15 points:\n", " EGER(1) EGGER(5) GREE(1) GREEGREE(8)\n", - "EIR forms 2 words for 11 points:\n", + "EIR makes 2 words for 11 points:\n", " EERIE(5) EERIER(6)\n", - "ENR forms 1 word for 1 point:\n", + "ENR makes 1 word for 1 point:\n", " ERNE(1)\n", - "ERT forms 7 words for 27 points:\n", + "ERT makes 7 words for 27 points:\n", " RETE(1) TEETER(6) TERETE(6) TERRET(6) TETTER(6) TREE(1) TRET(1)\n", - "GIR forms 2 words for 7 points:\n", + "GIR makes 2 words for 7 points:\n", " GRIG(1) GRIGRI(6)\n" ] } @@ -1190,24 +1176,42 @@ "source": [ "# S Words\n", "\n", - "What if we allowed honeycombs (and words) to have an S?" + "What if we allowed honeycombs and words to have an 'S' in them?" ] }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 37, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "(98141, 44585)" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "def S_words(text) -> list:\n", - " \"\"\"A list of all the valid space-separated words, including words with an S.\"\"\"\n", - " return [w for w in text.upper().split() \n", - " if len(w) >= 4 and len(set(w)) <= 7]" + "enable1s = [w for w in open('enable1.txt').read().upper().split() \n", + " if len(w) >= 4 and len(set(w)) <= 7]\n", + "\n", + "len(enable1s), len(enable1)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "That more than doubles the number of words. Will it double the score?" ] }, { "cell_type": "code", - "execution_count": 33, + "execution_count": 38, "metadata": { "scrolled": false }, @@ -1217,10 +1221,9 @@ "output_type": "stream", "text": [ "For this list of 98,141 words:\n", - "The optimal honeycomb ('AEINRST', 'E') forms 1,179 words for 8,681 points.\n", - "Here are the words formed by each subset, with pangrams first:\n", + "The optimal honeycomb AEINRST with center E makes 1,179 words for 8,681 points:\n", "\n", - "AEINRST forms 86 words for 1,381 points:\n", + "AEINRST makes 86 words for 1,381 points:\n", " ANESTRI(14) ANTISERA(15) ANTISTRESS(17) ANTSIER(14) ARENITES(15)\n", " ARSENITE(15) ARSENITES(16) ARTINESS(15) ARTINESSES(17) ATTAINERS(16)\n", " ENTERTAINERS(19) ENTERTAINS(17) ENTRAINERS(17) ENTRAINS(15) ENTREATIES(17)\n", @@ -1238,17 +1241,17 @@ " STRAITNESSES(19) TANISTRIES(17) TANNERIES(16) TEARSTAIN(16) TEARSTAINS(17)\n", " TENANTRIES(17) TERNARIES(16) TERRAINS(15) TERTIANS(15) TRAINEES(15)\n", " TRAINERS(15) TRANSIENT(16) TRANSIENTS(17) TRISTEARIN(17) TRISTEARINS(18)\n", - "AEINRS forms 16 words for 124 points:\n", + "AEINRS makes 16 words for 124 points:\n", " AIRINESS(8) AIRINESSES(10) ANSERINE(8) ANSERINES(9) ARISEN(6) ARSINE(6)\n", " ARSINES(7) INSANER(7) INSNARE(7) INSNARER(8) INSNARERS(9) INSNARES(8)\n", " SENARII(7) SIERRAN(7) SIRENIAN(8) SIRENIANS(9)\n", - "AEINRT forms 30 words for 232 points:\n", + "AEINRT makes 30 words for 232 points:\n", " ARENITE(7) ATTAINER(8) ENTERTAIN(9) ENTERTAINER(11) ENTRAIN(7) ENTRAINER(9)\n", " INERRANT(8) INERTIA(7) INERTIAE(8) INTENERATE(10) INTREAT(7) ITERANT(7)\n", " ITINERANT(9) ITINERATE(9) NATTIER(7) NITRATE(7) RATINE(6) REATTAIN(8)\n", " REINITIATE(10) RETAIN(6) RETAINER(8) RETINA(6) RETINAE(7) RETIRANT(8)\n", " RETRAIN(7) TERRAIN(7) TERTIAN(7) TRAINEE(7) TRAINER(7) TRIENNIA(8)\n", - "AEINST forms 80 words for 713 points:\n", + "AEINST makes 80 words for 713 points:\n", " ANISETTE(8) ANISETTES(9) ANTISENSE(9) ANTISTATE(9) ANTSIEST(8)\n", " ASININITIES(11) ASSASSINATE(11) ASSASSINATES(12) ASTATINE(8) ASTATINES(9)\n", " ENTASIA(7) ENTASIAS(8) ENTASIS(7) ETESIAN(7) ETESIANS(8) INANEST(7)\n", @@ -1264,7 +1267,7 @@ " TASTINESS(9) TASTINESSES(11) TATTINESS(9) TATTINESSES(11) TENIAS(6)\n", " TENIASES(8) TENIASIS(8) TETANIES(8) TETANISE(8) TETANISES(9) TINEAS(6)\n", " TISANE(6) TISANES(7) TITANATES(9) TITANESS(8) TITANESSES(10) TITANITES(9)\n", - "AEIRST forms 60 words for 473 points:\n", + "AEIRST makes 60 words for 473 points:\n", " AERIEST(7) AIREST(6) AIRIEST(7) ARIETTAS(8) ARIETTES(8) ARISTAE(7)\n", " ARISTATE(8) ARTERIES(8) ARTERITIS(9) ARTIEST(7) ARTISTE(7) ARTISTES(8)\n", " ARTISTRIES(10) ARTSIER(7) ARTSIEST(8) ASSISTER(8) ASSISTERS(9) ASTERIA(7)\n", @@ -1276,7 +1279,7 @@ " TARSIERS(8) TASTIER(7) TEARIEST(8) TERAIS(6) TERTIARIES(10) TITRATES(8)\n", " TRAITRESS(9) TRAITRESSES(11) TREATIES(8) TREATISE(8) TREATISES(9)\n", " TRISTATE(8)\n", - "AENRST forms 40 words for 336 points:\n", + "AENRST makes 40 words for 336 points:\n", " ANTEATERS(9) ANTRES(6) ARRESTANT(9) ARRESTANTS(10) ARSENATE(8) ARSENATES(9)\n", " ASSENTER(8) ASSENTERS(9) ASTERN(6) EARNEST(7) EARNESTNESS(11)\n", " EARNESTNESSES(13) EARNESTS(8) EASTERN(7) EASTERNER(9) EASTERNERS(10)\n", @@ -1285,7 +1288,7 @@ " RETREATANTS(11) SARSENET(8) SARSENETS(9) SERENATA(8) SERENATAS(9)\n", " SERENATE(8) STERNA(6) TANNERS(7) TARANTASES(10) TARTNESS(8) TARTNESSES(10)\n", " TERRANES(8)\n", - "EINRST forms 70 words for 582 points:\n", + "EINRST makes 70 words for 582 points:\n", " ENTERITIS(9) ENTERITISES(11) ENTIRENESS(10) ENTIRENESSES(12) ENTIRES(7)\n", " ENTIRETIES(10) ENTRIES(7) ESTRIN(6) ESTRINS(7) ETERNISE(8) ETERNISES(9)\n", " ETERNITIES(10) INERTNESS(9) INERTNESSES(11) INERTS(6) INSERT(6) INSERTER(8)\n", @@ -1298,43 +1301,43 @@ " SINTER(6) SINTERS(7) STERNITE(8) STERNITES(9) STINTER(7) STINTERS(8)\n", " TEENSIER(8) TEENTSIER(9) TERRINES(8) TINNERS(7) TINTERS(7) TRIENES(7)\n", " TRIENS(6) TRIENTES(8) TRINES(6) TRINITIES(9) TRITENESS(9) TRITENESSES(11)\n", - "AEINR forms 3 words for 19 points:\n", + "AEINR makes 3 words for 19 points:\n", " INANER(6) NARINE(6) RAINIER(7)\n", - "AEINS forms 17 words for 129 points:\n", + "AEINS makes 17 words for 129 points:\n", " ANISE(5) ANISES(6) ASININE(7) EASINESS(8) EASINESSES(10) INANENESS(9)\n", " INANENESSES(11) INANES(6) INSANE(6) INSANENESS(10) INSANENESSES(12)\n", " NANNIES(7) SANIES(6) SANSEI(6) SANSEIS(7) SIENNA(6) SIENNAS(7)\n", - "AEINT forms 10 words for 64 points:\n", + "AEINT makes 10 words for 64 points:\n", " ENTIA(5) INITIATE(8) INNATE(6) TAENIA(6) TAENIAE(7) TENIA(5) TENIAE(6)\n", " TINEA(5) TITANATE(8) TITANITE(8)\n", - "AEIRS forms 17 words for 106 points:\n", + "AEIRS makes 17 words for 106 points:\n", " AERIES(6) AIRERS(6) ARISE(5) ARISES(6) ARRISES(7) EASIER(6) RAISE(5)\n", " RAISER(6) RAISERS(7) RAISES(6) RERAISE(7) RERAISES(8) SASSIER(7) SERAI(5)\n", " SERAIS(6) SIERRA(6) SIERRAS(7)\n", - "AEIRT forms 20 words for 135 points:\n", + "AEIRT makes 20 words for 135 points:\n", " ARIETTA(7) ARIETTE(7) ARTIER(6) ATTIRE(6) ATTRITE(7) IRATE(5) IRATER(6)\n", " IRRITATE(8) ITERATE(7) RATITE(6) RATTIER(7) REITERATE(9) RETIA(5)\n", " RETIARII(8) TARRIER(7) TATTIER(7) TEARIER(7) TERAI(5) TERRARIA(8) TITRATE(7)\n", - "AEIST forms 15 words for 112 points:\n", + "AEIST makes 15 words for 112 points:\n", " EASIEST(7) ETATIST(7) SASSIEST(8) SATIATE(7) SATIATES(8) SATIETIES(9)\n", " SIESTA(6) SIESTAS(7) STEATITE(8) STEATITES(9) TASSIE(6) TASSIES(7)\n", " TASTIEST(8) TATTIES(7) TATTIEST(8)\n", - "AENRS forms 25 words for 172 points:\n", + "AENRS makes 25 words for 172 points:\n", " ANEARS(6) ARENAS(6) EARNERS(7) EARNS(5) ENSNARE(7) ENSNARER(8) ENSNARERS(9)\n", " ENSNARES(8) NARES(5) NEARNESS(8) NEARNESSES(10) NEARS(5) RANEES(6)\n", " RARENESS(8) RARENESSES(10) REEARNS(7) RENNASE(7) RENNASES(8) SANER(5)\n", " SARSEN(6) SARSENS(7) SNARE(5) SNARER(6) SNARERS(7) SNARES(6)\n", - "AENRT forms 19 words for 132 points:\n", + "AENRT makes 19 words for 132 points:\n", " ANTEATER(8) ANTRE(5) ENTERA(6) ENTRANT(7) ENTREAT(7) ERRANT(6) NARRATE(7)\n", " NARRATER(8) NATTER(6) NEATER(6) RANTER(6) RATTEEN(7) RATTEN(6) RATTENER(8)\n", " REENTRANT(9) RETREATANT(10) TANNER(6) TERNATE(7) TERRANE(7)\n", - "AENST forms 32 words for 217 points:\n", + "AENST makes 32 words for 217 points:\n", " ANATASE(7) ANATASES(8) ANENST(6) ANNATES(7) ANSATE(6) ANTENNAS(8) ANTES(5)\n", " ASSENT(6) ASSENTS(7) ENATES(6) ENTASES(7) ETNAS(5) NATES(5) NEATENS(7)\n", " NEATEST(7) NEATNESS(8) NEATNESSES(10) NEATS(5) SANEST(6) SATEEN(6)\n", " SATEENS(7) SENATE(6) SENATES(7) SENSATE(7) SENSATES(8) SETENANT(8)\n", " SETENANTS(9) STANE(5) STANES(6) TANNATES(8) TANNEST(7) TENANTS(7)\n", - "AERST forms 85 words for 604 points:\n", + "AERST makes 85 words for 604 points:\n", " AERATES(7) ARETES(6) ARREST(6) ARRESTEE(8) ARRESTEES(9) ARRESTER(8)\n", " ARRESTERS(9) ARRESTS(7) ASSERT(6) ASSERTER(8) ASSERTERS(9) ASSERTS(7)\n", " ASTER(5) ASTERS(6) ATTESTER(8) ATTESTERS(9) EASTER(6) EASTERS(7) EATERS(6)\n", @@ -1349,19 +1352,19 @@ " TASTERS(7) TATERS(6) TATTERS(7) TEARERS(7) TEARS(5) TEASER(6) TEASERS(7)\n", " TERRAS(6) TERRASES(8) TESSERA(7) TESSERAE(8) TETRAS(6) TRASSES(7)\n", " TREATERS(8) TREATS(6)\n", - "EINRS forms 29 words for 184 points:\n", + "EINRS makes 29 words for 184 points:\n", " EERINESS(8) EERINESSES(10) ESERINE(7) ESERINES(8) INNERS(6) NEREIS(6)\n", " REINS(5) RENINS(6) RENNINS(7) RERISEN(7) RESIN(5) RESINS(6) RINSE(5)\n", " RINSER(6) RINSERS(7) RINSES(6) RISEN(5) SEINER(6) SEINERS(7) SEREIN(6)\n", " SEREINS(7) SERIN(5) SERINE(6) SERINES(7) SERINS(6) SINNER(6) SINNERS(7)\n", " SIREN(5) SIRENS(6)\n", - "EINRT forms 29 words for 190 points:\n", + "EINRT makes 29 words for 190 points:\n", " ENTIRE(6) INERT(5) INTER(5) INTERN(6) INTERNE(7) INTERNEE(8) INTERTIE(8)\n", " NETTIER(7) NITER(5) NITERIE(7) NITRE(5) NITRITE(7) NITTIER(7) REINTER(7)\n", " RENITENT(8) RENTIER(7) RETINE(6) RETINENE(8) RETINITE(8) RETINT(6)\n", " TEENIER(7) TENTIER(7) TERRINE(7) TINIER(6) TINNER(6) TINNIER(7) TINTER(6)\n", " TRIENE(6) TRINE(5)\n", - "EINST forms 58 words for 469 points:\n", + "EINST makes 58 words for 469 points:\n", " EINSTEIN(8) EINSTEINS(9) ENTITIES(8) INSENTIENT(10) INSET(5) INSETS(6)\n", " INSISTENT(9) INTENSE(7) INTENSENESS(11) INTENSENESSES(13) INTENSEST(9)\n", " INTENSITIES(11) INTENTNESS(10) INTENTNESSES(12) INTENTS(7) INTESTINE(9)\n", @@ -1372,83 +1375,83 @@ " TEENTSIEST(10) TENNIES(7) TENNIS(6) TENNISES(8) TENNIST(7) TENNISTS(8)\n", " TENSITIES(9) TENTIEST(8) TESTINESS(9) TESTINESSES(11) TINES(5) TINIEST(7)\n", " TININESS(8) TININESSES(10) TINNIEST(8) TINNINESS(9) TINNINESSES(11)\n", - "EIRST forms 38 words for 262 points:\n", + "EIRST makes 38 words for 262 points:\n", " EERIEST(7) IRITISES(8) RESIST(6) RESISTER(8) RESISTERS(9) RESISTS(7)\n", " RESITE(6) RESITES(7) RETIES(6) RETIREES(8) RETIRERS(8) RETIRES(7) RETRIES(7)\n", " RITES(5) RITTERS(7) SISTER(6) SISTERS(7) SITTER(6) SITTERS(7) STIRRER(7)\n", " STIRRERS(8) STRETTI(7) TERRIERS(8) TERRIES(7) TERRITS(7) TESTIER(7) TIERS(5)\n", " TIRES(5) TITERS(6) TITRES(6) TITTERERS(9) TITTERS(7) TRESSIER(8)\n", " TRESSIEST(9) TRIERS(6) TRIES(5) TRISTE(6) TRITEST(7)\n", - "ENRST forms 35 words for 246 points:\n", + "ENRST makes 35 words for 246 points:\n", " ENTERERS(8) ENTERS(6) ENTREES(7) NERTS(5) NESTER(6) NESTERS(7) NETTERS(7)\n", " REENTERS(8) RENEST(6) RENESTS(7) RENNETS(7) RENTERS(7) RENTES(6) RENTS(5)\n", " RESENT(6) RESENTS(7) RETENES(7) SERENEST(8) STERN(5) STERNER(7) STERNEST(8)\n", " STERNNESS(9) STERNNESSES(11) STERNS(6) TEENERS(7) TENNERS(7) TENSER(6)\n", " TENTERS(7) TERNES(6) TERNS(5) TERREENS(8) TERRENES(8) TERSENESS(9)\n", " TERSENESSES(11) TREENS(6)\n", - "AEIN forms 2 words for 11 points:\n", + "AEIN makes 2 words for 11 points:\n", " INANE(5) NANNIE(6)\n", - "AEIR forms 4 words for 22 points:\n", + "AEIR makes 4 words for 22 points:\n", " AERIE(5) AERIER(6) AIRER(5) AIRIER(6)\n", - "AEIS forms 2 words for 13 points:\n", + "AEIS makes 2 words for 13 points:\n", " EASIES(6) SASSIES(7)\n", - "AEIT forms 1 word for 6 points:\n", + "AEIT makes 1 word for 6 points:\n", " TATTIE(6)\n", - "AENR forms 9 words for 40 points:\n", + "AENR makes 9 words for 40 points:\n", " ANEAR(5) ARENA(5) EARN(1) EARNER(6) NEAR(1) NEARER(6) RANEE(5) REEARN(6)\n", " RERAN(5)\n", - "AENS forms 9 words for 46 points:\n", + "AENS makes 9 words for 46 points:\n", " ANES(1) ANSAE(5) SANE(1) SANENESS(8) SANENESSES(10) SANES(5) SENNA(5)\n", " SENNAS(6) SENSA(5)\n", - "AENT forms 13 words for 63 points:\n", + "AENT makes 13 words for 63 points:\n", " ANENT(5) ANTAE(5) ANTE(1) ANTENNA(7) ANTENNAE(8) ATTENT(6) EATEN(5) ENATE(5)\n", " ETNA(1) NEAT(1) NEATEN(6) TANNATE(7) TENANT(6)\n", - "AERS forms 26 words for 121 points:\n", + "AERS makes 26 words for 121 points:\n", " AREAS(5) ARES(1) ARREARS(7) ARSE(1) ARSES(5) EARS(1) ERAS(1) ERASE(5)\n", " ERASER(6) ERASERS(7) ERASES(6) RARES(5) RASE(1) RASER(5) RASERS(6) RASES(5)\n", " REARERS(7) REARS(5) REASSESS(8) REASSESSES(10) SAREE(5) SAREES(6) SEAR(1)\n", " SEARER(6) SEARS(5) SERA(1)\n", - "AERT forms 24 words for 127 points:\n", + "AERT makes 24 words for 127 points:\n", " AERATE(6) ARETE(5) EATER(5) ERRATA(6) RATE(1) RATER(5) RATTER(6) REATA(5)\n", " RETEAR(6) RETREAT(7) RETREATER(9) TARE(1) TARRE(5) TARTER(6) TARTRATE(8)\n", " TATER(5) TATTER(6) TEAR(1) TEARER(6) TERRA(5) TERRAE(6) TETRA(5) TREAT(5)\n", " TREATER(7)\n", - "AEST forms 35 words for 164 points:\n", + "AEST makes 35 words for 164 points:\n", " ASSET(5) ASSETS(6) ATES(1) ATTEST(6) ATTESTS(7) EAST(1) EASTS(5) EATS(1)\n", " ESTATE(6) ESTATES(7) ETAS(1) SATE(1) SATES(5) SEAT(1) SEATS(5) SETA(1)\n", " SETAE(5) STASES(6) STATE(5) STATES(6) TASSE(5) TASSES(6) TASSET(6)\n", " TASSETS(7) TASTE(5) TASTES(6) TATES(5) TEAS(1) TEASE(5) TEASES(6) TEATS(5)\n", " TESTA(5) TESTAE(6) TESTATE(7) TESTATES(8)\n", - "EINR forms 4 words for 17 points:\n", + "EINR makes 4 words for 17 points:\n", " INNER(5) REIN(1) RENIN(5) RENNIN(6)\n", - "EINS forms 10 words for 53 points:\n", + "EINS makes 10 words for 53 points:\n", " NINES(5) NINNIES(7) NISEI(5) NISEIS(6) SEINE(5) SEINES(6) SEISIN(6)\n", " SEISINS(7) SINE(1) SINES(5)\n", - "EINT forms 6 words for 28 points:\n", + "EINT makes 6 words for 28 points:\n", " INTENT(6) INTINE(6) NINETEEN(8) NITE(1) TENTIE(6) TINE(1)\n", - "EIRS forms 20 words for 101 points:\n", + "EIRS makes 20 words for 101 points:\n", " IRES(1) IRISES(6) REIS(1) RERISE(6) RERISES(7) RISE(1) RISER(5) RISERS(6)\n", " RISES(5) SEISER(6) SEISERS(7) SERIES(6) SERRIES(7) SIRE(1) SIREE(5)\n", " SIREES(6) SIRES(5) SIRREE(6) SIRREES(7) SISSIER(7)\n", - "EIRT forms 17 words for 87 points:\n", + "EIRT makes 17 words for 87 points:\n", " RETIE(5) RETIRE(6) RETIREE(7) RETIRER(7) RITE(1) RITTER(6) TERRIER(7)\n", " TERRIT(6) TIER(1) TIRE(1) TITER(5) TITRE(5) TITTER(6) TITTERER(8) TRIER(5)\n", " TRITE(5) TRITER(6)\n", - "EIST forms 8 words for 41 points:\n", + "EIST makes 8 words for 41 points:\n", " SISSIEST(8) SITE(1) SITES(5) STIES(5) TESTIEST(8) TESTIS(6) TIES(1)\n", " TITTIES(7)\n", - "ENRS forms 12 words for 80 points:\n", + "ENRS makes 12 words for 80 points:\n", " ERNES(5) ERNS(1) RESEEN(6) SERENE(6) SERENENESS(10) SERENENESSES(12)\n", " SERENER(7) SERENES(7) SNEER(5) SNEERER(7) SNEERERS(8) SNEERS(6)\n", - "ENRT forms 19 words for 104 points:\n", + "ENRT makes 19 words for 104 points:\n", " ENTER(5) ENTERER(7) ENTREE(6) ETERNE(6) NETTER(6) REENTER(7) RENNET(6)\n", " RENT(1) RENTE(5) RENTER(6) RETENE(6) TEENER(6) TENNER(6) TENTER(6) TERN(1)\n", " TERNE(5) TERREEN(7) TERRENE(7) TREEN(5)\n", - "ENST forms 18 words for 94 points:\n", + "ENST makes 18 words for 94 points:\n", " ENTENTES(8) NEST(1) NESTS(5) NETS(1) NETTS(5) SENNET(6) SENNETS(7) SENT(1)\n", " SENTE(5) TEENS(5) TENETS(6) TENS(1) TENSE(5) TENSENESS(9) TENSENESSES(11)\n", " TENSES(6) TENSEST(7) TENTS(5)\n", - "ERST forms 44 words for 266 points:\n", + "ERST makes 44 words for 266 points:\n", " ERST(1) ESTER(5) ESTERS(6) REEST(5) REESTS(6) RESET(5) RESETS(6) RESETTER(8)\n", " RESETTERS(9) REST(1) RESTER(6) RESTERS(7) RESTRESS(8) RESTRESSES(10)\n", " RESTS(5) RETEST(6) RETESTS(7) RETS(1) SEREST(6) SETTER(6) SETTERS(7)\n", @@ -1456,50 +1459,52 @@ " STREETS(7) STRESS(6) STRESSES(8) STRETTE(7) TEETERS(7) TERRETS(7) TERSE(5)\n", " TERSER(6) TERSEST(7) TESTER(6) TESTERS(7) TETTERS(7) TREES(5) TRESS(5)\n", " TRESSES(7) TRETS(5)\n", - "AER forms 7 words for 25 points:\n", + "AER makes 7 words for 25 points:\n", " AREA(1) AREAE(5) ARREAR(6) RARE(1) RARER(5) REAR(1) REARER(6)\n", - "AES forms 8 words for 33 points:\n", + "AES makes 8 words for 33 points:\n", " ASEA(1) ASSES(5) ASSESS(6) ASSESSES(8) EASE(1) EASES(5) SASSES(6) SEAS(1)\n", - "AET forms 2 words for 2 points:\n", + "AET makes 2 words for 2 points:\n", " TATE(1) TEAT(1)\n", - "EIN forms 1 word for 1 point:\n", + "EIN makes 1 word for 1 point:\n", " NINE(1)\n", - "EIR forms 2 words for 11 points:\n", + "EIR makes 2 words for 11 points:\n", " EERIE(5) EERIER(6)\n", - "EIS forms 7 words for 35 points:\n", + "EIS makes 7 words for 35 points:\n", " ISSEI(5) ISSEIS(6) SEIS(1) SEISE(5) SEISES(6) SISES(5) SISSIES(7)\n", - "EIT forms 1 word for 6 points:\n", + "EIT makes 1 word for 6 points:\n", " TITTIE(6)\n", - "ENR forms 1 word for 1 point:\n", + "ENR makes 1 word for 1 point:\n", " ERNE(1)\n", - "ENS forms 6 words for 20 points:\n", + "ENS makes 6 words for 20 points:\n", " NESS(1) NESSES(6) SEEN(1) SENE(1) SENSE(5) SENSES(6)\n", - "ENT forms 5 words for 15 points:\n", + "ENT makes 5 words for 15 points:\n", " ENTENTE(7) NETT(1) TEEN(1) TENET(5) TENT(1)\n", - "ERS forms 13 words for 52 points:\n", + "ERS makes 13 words for 52 points:\n", " ERRS(1) ERSES(5) REES(1) RESEE(5) RESEES(6) SEER(1) SEERESS(7) SEERESSES(9)\n", " SEERS(5) SERE(1) SERER(5) SERES(5) SERS(1)\n", - "ERT forms 7 words for 27 points:\n", + "ERT makes 7 words for 27 points:\n", " RETE(1) TEETER(6) TERETE(6) TERRET(6) TETTER(6) TREE(1) TRET(1)\n", - "EST forms 18 words for 79 points:\n", + "EST makes 18 words for 79 points:\n", " SESTET(6) SESTETS(7) SETS(1) SETT(1) SETTEE(6) SETTEES(7) SETTS(5) STET(1)\n", " STETS(5) TEES(1) TEST(1) TESTEE(6) TESTEES(7) TESTES(6) TESTS(5) TETS(1)\n", " TSETSE(6) TSETSES(7)\n", - "EN forms 1 word for 1 point:\n", + "EN makes 1 word for 1 point:\n", " NENE(1)\n", - "ES forms 3 words for 7 points:\n", + "ES makes 3 words for 7 points:\n", " ESES(1) ESSES(5) SEES(1)\n" ] } ], "source": [ - "report(S_words(open('enable1.txt').read()))" + "report(enable1s)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ + "Yes it does!\n", + "\n", "# Pictures\n", "\n", "Here are pictures for the highest-scoring honeycombs, with and without an S:\n", @@ -1524,7 +1529,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.0" + "version": "3.7.6" } }, "nbformat": 4,