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,