X-Git-Url: https://git.njae.me.uk/?a=blobdiff_plain;f=word-chains%2Fword-chain-solution.ipynb;fp=word-chains%2Fword-chain-solution.ipynb;h=0052abce5b05c22c7669b120bca349d24c2d1937;hb=a0fc23654a764a8fdec3fa9858b0453c58bc5f34;hp=0000000000000000000000000000000000000000;hpb=26e4d67c40509502e77c6935e7ded629c661f8e1;p=ou-summer-of-code-2017.git diff --git a/word-chains/word-chain-solution.ipynb b/word-chains/word-chain-solution.ipynb new file mode 100644 index 0000000..0052abc --- /dev/null +++ b/word-chains/word-chain-solution.ipynb @@ -0,0 +1,583 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Word chains\n", + "\n", + "\"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", + "\n", + "For instance, you can transform 'rash' to 'jags' like this:\n", + "\n", + "```\n", + "rash\n", + "Bash\n", + "basS\n", + "baGs\n", + "Jags\n", + "```\n", + "\n", + "(the capital letter is the one changed in each step).\n", + "\n", + "## Part 1\n", + "\n", + "Given this [list of words](words4.txt), what is the minimum number of steps to go from `vice` to `wars`?" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "import string\n", + "import heapq" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "2336" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "words = [w.strip() for w in open('words4.txt').readlines()]\n", + "len(words)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def adjacents(word):\n", + " return [word[0:i] + l + word[i+1:]\n", + " for i in range(len(word))\n", + " for l in string.ascii_lowercase\n", + " if l != word[i]]" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "neighbours = {w: [n for n in adjacents(w) if n in words]\n", + " for w in words}" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def distance(w1, w2):\n", + " return sum(1 for i in range(len(w1))\n", + " if w1[i] != w2[i])" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def extend(chain):\n", + " return [chain + [s] for s in neighbours[chain[-1]]\n", + " if s not in chain]" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def bfs_search(start, target, debug=False):\n", + " return bfs([[start]], target, debug=debug)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def bfs(agenda, goal, debug=False):\n", + " finished = False\n", + " while not finished and agenda:\n", + " current = agenda[0]\n", + " if debug:\n", + " print(current)\n", + " if current[-1] == goal:\n", + " finished = True\n", + " else:\n", + " successors = extend(current)\n", + " agenda = agenda[1:] + successors\n", + " if agenda:\n", + " return current\n", + " else:\n", + " return None " + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def dfs_search(start, target, debug=False):\n", + " return dfs([[start]], target, debug=debug)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def dfs(agenda, goal, debug=False):\n", + " finished = False\n", + " while not finished and agenda:\n", + " current = agenda[0]\n", + " if debug:\n", + " print(current)\n", + " if current[-1] == goal:\n", + " finished = True\n", + " else:\n", + " successors = extend(current)\n", + " agenda = successors + agenda[1:]\n", + " if agenda:\n", + " return current\n", + " else:\n", + " return None " + ] + }, + { + "cell_type": "code", + "execution_count": 57, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def astar_search(start, target, debug=False):\n", + " agenda = [(distance(start, target), [start])]\n", + " heapq.heapify(agenda)\n", + " return astar(agenda, target, debug=debug)" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def astar(agenda, goal, debug=False):\n", + " finished = False\n", + " while not finished and agenda:\n", + " _, current = heapq.heappop(agenda)\n", + " if debug:\n", + " print(current)\n", + " if current[-1] == goal:\n", + " finished = True\n", + " else:\n", + " successors = extend(current)\n", + " for s in successors:\n", + " heapq.heappush(agenda, (len(current) + distance(s[-1], goal) - 1, s))\n", + " if agenda:\n", + " return current\n", + " else:\n", + " return None " + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['vice', 'dice', 'dire', 'dare', 'ware', 'wars']" + ] + }, + "execution_count": 58, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "astar_search('vice', 'wars')" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "6" + ] + }, + "execution_count": 60, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(astar_search('vice', 'wars'))" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "6" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(bfs_search('vice', 'wars'))" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "793" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(dfs_search('vice', 'wars'))" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "10000 loops, best of 3: 154 µs per loop\n" + ] + } + ], + "source": [ + "%%timeit\n", + "astar_search('vice', 'wars')" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1 loop, best of 3: 1min 40s per loop\n" + ] + } + ], + "source": [ + "%%timeit\n", + "bfs_search('vice', 'wars')" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "10 loops, best of 3: 86.3 ms per loop\n" + ] + } + ], + "source": [ + "%%timeit\n", + "dfs_search('vice', 'wars')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Part 2\n", + "\n", + "The example shows that `jags` is reachable in four steps from `rash`. There are 11 words one step away from `rash`: \n", + "`bash`, `cash`, `dash`, `gash`, `hash`, `lash`, `mash`, `rasp`, `rush`, `sash`, and `wash`. \n", + "\n", + "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", + "\n", + "There are 180 words reachable in up to three steps from `rash`.\n", + "\n", + "How many words are reachable in up to ten steps from `vice`?" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def reachable_in(word, n, trim_extras=False):\n", + " reachable = set()\n", + " boundary = set([word])\n", + " for i in range(n):\n", + " extras = set()\n", + " for w in boundary:\n", + " extras.update(neighbours[w])\n", + " if trim_extras:\n", + " extras.difference_update(reachable)\n", + " reachable.update(boundary)\n", + " boundary = extras.copy()\n", + " return reachable.union(extras).difference(set([word]))" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(11,\n", + " '`bash`, `cash`, `dash`, `gash`, `hash`, `lash`, `mash`, `rasp`, `rush`, `sash`, `wash`')" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(reachable_in('rash', 1)), ', '.join(sorted('`{}`'.format(r) for r in reachable_in('rash', 1)))" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(47,\n", + " '`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`')" + ] + }, + "execution_count": 39, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(reachable_in('rash', 2)), ', '.join(sorted('`{}`'.format(r) for r in reachable_in('rash', 2)))" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "180" + ] + }, + "execution_count": 40, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(reachable_in('rash', 3))" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "2195" + ] + }, + "execution_count": 48, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(reachable_in('rash', 10))" + ] + }, + { + "cell_type": "code", + "execution_count": 47, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/plain": [ + "2192" + ] + }, + "execution_count": 47, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(reachable_in('vice', 10))" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "100 loops, best of 3: 5.97 ms per loop\n" + ] + } + ], + "source": [ + "%%timeit\n", + "len(reachable_in('rash', 10))" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "100 loops, best of 3: 3.1 ms per loop\n" + ] + } + ], + "source": [ + "%%timeit\n", + "len(reachable_in('rash', 10, trim_extras=True))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.5.2+" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +}