4 "cell_type": "markdown",
9 "\"Word chain\" puzzles are where you transform one word into another, by changing one letter at a time, with all the intermediate steps being valid words. \n",
11 "For instance, you can transform 'rash' to 'jags' like this:\n",
21 "(the capital letter is the one changed in each step).\n",
25 "Given this [list of words](words4.txt), what is the minimum number of steps to go from `vice` to `wars`?"
55 "output_type": "execute_result"
59 "words = [w.strip() for w in open('08-words.txt').readlines()]\n",
71 "def adjacents(word):\n",
72 " return [word[0:i] + l + word[i+1:]\n",
73 " for i in range(len(word))\n",
74 " for l in string.ascii_lowercase\n",
86 "neighbours = {w: [n for n in adjacents(w) if n in words]\n",
98 "def distance(w1, w2):\n",
99 " return sum(1 for i in range(len(w1))\n",
100 " if w1[i] != w2[i])"
105 "execution_count": 6,
111 "# def extend(chain):\n",
112 "# return [chain + [s] for s in neighbours[chain[-1]]\n",
113 "# if s not in chain]"
118 "execution_count": 7,
124 "def extend(chain, closed=None):\n",
126 " nbrs = set(neighbours[chain[-1]]) - closed\n",
128 " nbrs = neighbours[chain[-1]]\n",
129 " return [chain + [s] for s in nbrs\n",
130 " if s not in chain]"
135 "execution_count": 8,
141 "def extend_raw(chain):\n",
142 " nbrs = [w for w in adjacents(chain[-1]) if w in words]\n",
143 " return [chain + [s] for s in nbrs\n",
144 " if s not in chain]"
149 "execution_count": 9,
155 "def bfs_search(start, goal, debug=False):\n",
156 " agenda = [[start]]\n",
157 " finished = False\n",
158 " while not finished and agenda:\n",
159 " current = agenda[0]\n",
162 " if current[-1] == goal:\n",
163 " finished = True\n",
165 " successors = extend(current)\n",
166 " agenda = agenda[1:] + successors\n",
175 "execution_count": 10,
181 "def bfs_search_closed(start, goal, debug=False):\n",
182 " agenda = [[start]]\n",
184 " finished = False\n",
185 " while not finished and agenda:\n",
186 " current = agenda[0]\n",
189 " if current[-1] == goal:\n",
190 " finished = True\n",
192 " closed.add(current[-1])\n",
193 " successors = extend(current, closed)\n",
194 " agenda = agenda[1:] + successors\n",
203 "execution_count": 11,
209 "def dfs_search(start, goal, debug=False):\n",
210 " agenda = [[start]]\n",
211 " finished = False\n",
212 " while not finished and agenda:\n",
213 " current = agenda[0]\n",
216 " if current[-1] == goal:\n",
217 " finished = True\n",
219 " successors = extend(current)\n",
220 " agenda = successors + agenda[1:]\n",
229 "execution_count": 12,
235 "def astar_search(start, goal, debug=False):\n",
236 " agenda = [(distance(start, goal), [start])]\n",
237 " heapq.heapify(agenda)\n",
238 " finished = False\n",
239 " while not finished and agenda:\n",
240 " _, current = heapq.heappop(agenda)\n",
243 " if current[-1] == goal:\n",
244 " finished = True\n",
246 " successors = extend(current)\n",
247 " for s in successors:\n",
248 " heapq.heappush(agenda, (len(current) + distance(s[-1], goal) - 1, s))\n",
257 "execution_count": 13,
263 "# Uses direct lookup of successors, rather than using cached neighbours in the dict\n",
264 "def astar_search_raw(start, goal, debug=False):\n",
265 " agenda = [(distance(start, goal), [start])]\n",
266 " heapq.heapify(agenda)\n",
267 " finished = False\n",
268 " while not finished and agenda:\n",
269 " _, current = heapq.heappop(agenda)\n",
272 " if current[-1] == goal:\n",
273 " finished = True\n",
275 " successors = extend_raw(current) # Difference here\n",
276 " for s in successors:\n",
277 " heapq.heappush(agenda, (len(current) + distance(s[-1], goal) - 1, s))\n",
286 "execution_count": 14,
292 "def astar_search_closed(start, goal, debug=False):\n",
293 " agenda = [(distance(start, goal), [start])]\n",
294 " heapq.heapify(agenda)\n",
296 " finished = False\n",
297 " while not finished and agenda:\n",
298 " _, current = heapq.heappop(agenda)\n",
301 " if current[-1] == goal:\n",
302 " finished = True\n",
304 " closed.add(current[-1])\n",
305 " successors = extend(current, closed)\n",
306 " for s in successors:\n",
307 " heapq.heappush(agenda, (len(current) + distance(s[-1], goal) - 1, s))\n",
316 "execution_count": 15,
324 "['vice', 'dice', 'dire', 'dare', 'ware', 'wars']"
327 "execution_count": 15,
329 "output_type": "execute_result"
333 "astar_search('vice', 'wars')"
338 "execution_count": 16,
346 "['vice', 'dice', 'dire', 'dare', 'ware', 'wars']"
349 "execution_count": 16,
351 "output_type": "execute_result"
355 "astar_search_raw('vice', 'wars')"
360 "execution_count": 17,
371 "execution_count": 17,
373 "output_type": "execute_result"
377 "len(astar_search('vice', 'wars'))"
382 "execution_count": 18,
388 "ename": "KeyboardInterrupt",
390 "output_type": "error",
392 "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
393 "\u001b[0;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)",
394 "\u001b[0;32m<ipython-input-18-77e0f8036978>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mlen\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mbfs_search\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'vice'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m'wars'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
395 "\u001b[0;32m<ipython-input-9-0c5cf399e032>\u001b[0m in \u001b[0;36mbfs_search\u001b[0;34m(start, goal, debug)\u001b[0m\n\u001b[1;32m 10\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 11\u001b[0m \u001b[0msuccessors\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mextend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcurrent\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 12\u001b[0;31m \u001b[0magenda\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0magenda\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0msuccessors\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 13\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0magenda\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 14\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mcurrent\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
396 "\u001b[0;31mKeyboardInterrupt\u001b[0m: "
401 "len(bfs_search('vice', 'wars'))"
406 "execution_count": 19,
417 "execution_count": 19,
419 "output_type": "execute_result"
423 "len(bfs_search_closed('vice', 'wars'))"
428 "execution_count": 20,
439 "execution_count": 20,
441 "output_type": "execute_result"
445 "len(dfs_search('vice', 'wars'))"
450 "execution_count": 21,
457 "output_type": "stream",
459 "1000 loops, best of 3: 225 µs per loop\n"
465 "astar_search('vice', 'wars')"
470 "execution_count": 22,
477 "output_type": "stream",
479 "10 loops, best of 3: 22.1 ms per loop\n"
485 "astar_search_raw('vice', 'wars')"
490 "execution_count": 23,
497 "output_type": "stream",
499 "1000 loops, best of 3: 280 µs per loop\n"
505 "astar_search_closed('vice', 'wars')"
510 "execution_count": 24,
516 "ename": "KeyboardInterrupt",
518 "output_type": "error",
520 "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
521 "\u001b[0;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)",
522 "\u001b[0;32m<ipython-input-24-ec1214959874>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mget_ipython\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrun_cell_magic\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'timeit'\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m''\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m\"bfs_search('vice', 'wars')\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
523 "\u001b[0;32m/usr/local/lib/python3.5/dist-packages/IPython/core/interactiveshell.py\u001b[0m in \u001b[0;36mrun_cell_magic\u001b[0;34m(self, magic_name, line, cell)\u001b[0m\n\u001b[1;32m 2113\u001b[0m \u001b[0mmagic_arg_s\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mvar_expand\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mline\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mstack_depth\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2114\u001b[0m \u001b[0;32mwith\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mbuiltin_trap\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 2115\u001b[0;31m \u001b[0mresult\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mfn\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mmagic_arg_s\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mcell\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 2116\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mresult\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 2117\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n",
524 "\u001b[0;32m<decorator-gen-58>\u001b[0m in \u001b[0;36mtimeit\u001b[0;34m(self, line, cell)\u001b[0m\n",
525 "\u001b[0;32m/usr/local/lib/python3.5/dist-packages/IPython/core/magic.py\u001b[0m in \u001b[0;36m<lambda>\u001b[0;34m(f, *a, **k)\u001b[0m\n\u001b[1;32m 186\u001b[0m \u001b[0;31m# but it's overkill for just that one bit of state.\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 187\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mmagic_deco\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0marg\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 188\u001b[0;31m \u001b[0mcall\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mlambda\u001b[0m \u001b[0mf\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m*\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mk\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mf\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0ma\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;34m**\u001b[0m\u001b[0mk\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 189\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 190\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mcallable\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0marg\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
526 "\u001b[0;32m/usr/local/lib/python3.5/dist-packages/IPython/core/magics/execution.py\u001b[0m in \u001b[0;36mtimeit\u001b[0;34m(self, line, cell)\u001b[0m\n\u001b[1;32m 1042\u001b[0m \u001b[0mnumber\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;36m1\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1043\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0m_\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mrange\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m10\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m-> 1044\u001b[0;31m \u001b[0mtime_number\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mtimer\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtimeit\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnumber\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 1045\u001b[0m \u001b[0mworst_tuning\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mmax\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mworst_tuning\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mtime_number\u001b[0m \u001b[0;34m/\u001b[0m \u001b[0mnumber\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 1046\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mtime_number\u001b[0m \u001b[0;34m>=\u001b[0m \u001b[0;36m0.2\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
527 "\u001b[0;32m/usr/local/lib/python3.5/dist-packages/IPython/core/magics/execution.py\u001b[0m in \u001b[0;36mtimeit\u001b[0;34m(self, number)\u001b[0m\n\u001b[1;32m 137\u001b[0m \u001b[0mgc\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mdisable\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 138\u001b[0m \u001b[0;32mtry\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 139\u001b[0;31m \u001b[0mtiming\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0minner\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mit\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mtimer\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 140\u001b[0m \u001b[0;32mfinally\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 141\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mgcold\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
528 "\u001b[0;32m<magic-timeit>\u001b[0m in \u001b[0;36minner\u001b[0;34m(_it, _timer)\u001b[0m\n",
529 "\u001b[0;32m<ipython-input-9-0c5cf399e032>\u001b[0m in \u001b[0;36mbfs_search\u001b[0;34m(start, goal, debug)\u001b[0m\n\u001b[1;32m 10\u001b[0m \u001b[0;32melse\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 11\u001b[0m \u001b[0msuccessors\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mextend\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcurrent\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 12\u001b[0;31m \u001b[0magenda\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0magenda\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m+\u001b[0m \u001b[0msuccessors\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 13\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0magenda\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 14\u001b[0m \u001b[0;32mreturn\u001b[0m \u001b[0mcurrent\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
530 "\u001b[0;31mKeyboardInterrupt\u001b[0m: "
536 "bfs_search('vice', 'wars')"
541 "execution_count": 25,
548 "output_type": "stream",
550 "1 loop, best of 3: 1.12 s per loop\n"
556 "bfs_search_closed('vice', 'wars')"
561 "execution_count": 26,
568 "output_type": "stream",
570 "10 loops, best of 3: 127 ms per loop\n"
576 "dfs_search('vice', 'wars')"
580 "cell_type": "markdown",
585 "The example shows that `jags` is reachable in four steps from `rash`. There are 11 words one step away from `rash`: \n",
586 "`bash`, `cash`, `dash`, `gash`, `hash`, `lash`, `mash`, `rasp`, `rush`, `sash`, and `wash`. \n",
588 "There are 47 words reachable in one or two steps from `rash`. They are `base`, `bash`, `bask`, `bass`, `bast`, `bath`, `bosh`, `bush`, `case`, `cash`, `cask`, `cast`, `dash`, `dish`, `gash`, `gasp`, `gosh`, `gush`, `hash`, `hasp`, `hath`, `hush`, `lash`, `lass`, `last`, `lath`, `lush`, `mash`, `mask`, `mass`, `mast`, `math`, `mesh`, `mush`, `push`, `ramp`, `rasp`, `ruse`, `rush`, `rusk`, `rust`, `sash`, `sass`, `tush`, `wash`, `wasp`, and `wish`.\n",
590 "There are 180 words reachable in up to three steps from `rash`.\n",
592 "How many words are reachable in up to ten steps from `vice`?"
597 "execution_count": 27,
603 "def reachable_in(word, n, trim_extras=False):\n",
604 " reachable = set()\n",
605 " boundary = set([word])\n",
606 " for i in range(n):\n",
608 " for w in boundary:\n",
609 " extras.update(neighbours[w])\n",
610 " if trim_extras:\n",
611 " extras.difference_update(reachable)\n",
612 " reachable.update(boundary)\n",
613 " boundary = extras.copy()\n",
614 " return reachable.union(extras).difference(set([word]))"
619 "execution_count": 28,
629 " '`bash`, `cash`, `dash`, `gash`, `hash`, `lash`, `mash`, `rasp`, `rush`, `sash`, `wash`')"
632 "execution_count": 28,
634 "output_type": "execute_result"
638 "len(reachable_in('rash', 1)), ', '.join(sorted('`{}`'.format(r) for r in reachable_in('rash', 1)))"
643 "execution_count": 29,
653 " '`base`, `bash`, `bask`, `bass`, `bast`, `bath`, `bosh`, `bush`, `case`, `cash`, `cask`, `cast`, `dash`, `dish`, `gash`, `gasp`, `gosh`, `gush`, `hash`, `hasp`, `hath`, `hush`, `lash`, `lass`, `last`, `lath`, `lush`, `mash`, `mask`, `mass`, `mast`, `math`, `mesh`, `mush`, `push`, `ramp`, `rasp`, `ruse`, `rush`, `rusk`, `rust`, `sash`, `sass`, `tush`, `wash`, `wasp`, `wish`')"
656 "execution_count": 29,
658 "output_type": "execute_result"
662 "len(reachable_in('rash', 2)), ', '.join(sorted('`{}`'.format(r) for r in reachable_in('rash', 2)))"
667 "execution_count": 30,
679 "execution_count": 30,
681 "output_type": "execute_result"
685 "len(reachable_in('rash', 3))"
690 "execution_count": 31,
702 "execution_count": 31,
704 "output_type": "execute_result"
708 "len(reachable_in('rash', 10))"
713 "execution_count": 32,
725 "execution_count": 32,
727 "output_type": "execute_result"
731 "len(reachable_in('vice', 10))"
736 "execution_count": 33,
744 "output_type": "stream",
746 "100 loops, best of 3: 9.26 ms per loop\n"
752 "len(reachable_in('rash', 10))"
757 "execution_count": 34,
765 "output_type": "stream",
767 "100 loops, best of 3: 3.64 ms per loop\n"
773 "len(reachable_in('rash', 10, trim_extras=True))"
778 "execution_count": null,
788 "display_name": "Python 3",
789 "language": "python",
797 "file_extension": ".py",
798 "mimetype": "text/x-python",
800 "nbconvert_exporter": "python",
801 "pygments_lexer": "ipython3",