{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# The Devil and the Coin Flip Game\n",
    "\n",
    ">You're playing a game with the devil, with your soul at stake. You're sitting at a circular table which has 4 coins, arranged in a diamond, at the 12, 3, 6, and 9 o'clock positions. You are blindfolded, and can never see the coins or the table.\n",
    "\n",
    ">Your goal is to get all 4 coins showing heads, by telling the devil the position(s) of some coins to flip. We call this a \"move\" on your part. The devil must faithfully perform the requested flips, but may first sneakily rotate the table any number of quarter-turns, so that the coins are in different positions. You keep making moves, and the devil keeps rotating and flipping, until all 4 coins show heads.\n",
    "\n",
    "> Example: You tell the devil tthe 12 o'clock and 6 o'clock positions. The devil could leave the table unrotated (or could rotate it a half-turn), and then flip the two coins that you specified. Or the devil could rotate the table a quarter turn in either direction, and then flip the coins that are now in the 12 o'clock and 6 o'clock positions (which were formerly at 3 o'clock and 9 o'clock).  You won't know which of these actions the devil took.\n",
    "\n",
    "> What is a shortest sequence of moves that is *guaranteed* to win, no matter what the initial state of the coins, and no matter what rotations the devil applies?\n",
    "\n",
    "# Analysis\n",
    "\n",
    "The player, being blindfolded, does not know the true state of the coins. So the player should represent what is known: the *set of possible states* of the coins. We call this a *belief state*. At the start of the game, each of the four coins could be either heads or tails, so that's 2<sup>4</sup> = 16 possibilities in the belief state. \n",
    "\n",
    "A move updates the belief state as follows: for every coin sequence in the current belief state, rotate it in every possible way, and then flip the coins specified by the position(s) in the move. Collect all these results together to form the new belief state. Solving the game means coming up with a sequence of moves that ends in a belief state consisting of just `{'HHHH'}`. It must be a shortest possible sequence, so a breadth-first search seems right. The search space will be tiny, so compute time will be trivial; the only issue is specifying the domain correctly. To increase the chance of getting it correct, I won't try to do anything fancy, such as noticing that some coin sequences are rotational variants of other sequences.\n",
    "\n",
    "\n",
    "# Implementation Choices\n",
    "\n",
    "Here are the main concepts, and my implementation choices:\n",
    "\n",
    "- `Coins`: A *coin sequence* is represented as a `str` of four characters, such as `'HTTT'`. \n",
    "- `Belief`: A *belief state* is represented as a `frozenset` of `Coins` (frozen so that it can be hashed in a `set`).\n",
    "- `Move`: A *move* is a set of positions to flip. A position will be an integer index into the coin sequence, so a move is a set of these such as `{0, 2}`, which we can interpret as \"flip the 12 o'clock and 6 o'clock positions.\" \n",
    "- `all_coins`: Set of all possible coin sequences: `{'HHHH', 'HHHT', ...}`.\n",
    "- `rotations`: The function `rotations(coins)` returns the set of all 4 rotations of the coin sequence.\n",
    "- `update`: The function `update(belief, move)` retuns an updated belief state, representing all the possible coin sequences that could result from any devil rotation followed by the specified flip(s). (But don't flip `'HHHH'`, because the game would have already ended.)\n",
    "- `flip`: The function `flip(coins, move)` flips the specified positions within the coin sequence."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "from collections import deque, Counter\n",
    "from itertools   import product, combinations\n",
    "import random\n",
    "\n",
    "Coins     = ''.join   # A coin sequence, such as 'HHHT'.\n",
    "Belief    = frozenset # A belief state is a set of possible coin sequences.\n",
    "all_coins = Belief(map(Coins, product('HT', repeat=4)))\n",
    "\n",
    "def rotations(coins) -> [Coins]: \n",
    "    \"A list of all possible rotations of a coin sequence.\"\n",
    "    return [coins[r:] + coins[:r] for r in range(4)]\n",
    "\n",
    "def update(belief, move) -> Belief:\n",
    "    \"Update belief: consider all possible rotations, then flip.\"\n",
    "    return Belief((c if c == 'HHHH' else flip(c, move))\n",
    "                  for coins in belief\n",
    "                  for c in rotations(coins))\n",
    "\n",
    "def flip(coins, move) -> Coins:\n",
    "    \"Flip the coins in the positions specified by the move.\"\n",
    "    coins = list(coins) # Need a mutable sequence\n",
    "    for i in move:\n",
    "        coins[i] = ('H' if coins[i] == 'T' else 'T')\n",
    "    return Coins(coins)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's try out these functions:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "'THTT'"
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "flip('HHHT', {0, 2})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['HHHT', 'HHTH', 'HTHH', 'THHH']"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "rotations('HHHT')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "frozenset({'HHHH',\n",
       "           'HHHT',\n",
       "           'HHTH',\n",
       "           'HHTT',\n",
       "           'HTHH',\n",
       "           'HTHT',\n",
       "           'HTTH',\n",
       "           'HTTT',\n",
       "           'THHH',\n",
       "           'THHT',\n",
       "           'THTH',\n",
       "           'THTT',\n",
       "           'TTHH',\n",
       "           'TTHT',\n",
       "           'TTTH',\n",
       "           'TTTT'})"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "all_coins"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We see there are 16 coin sequences in the `all_coins` belief state. Now if we update this belief state by flipping all 4 positions, we should get a new belief state where we have eliminated the possibility of 4 tails, leaving 15 possible coin sequences:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "frozenset({'HHHH',\n",
       "           'HHHT',\n",
       "           'HHTH',\n",
       "           'HHTT',\n",
       "           'HTHH',\n",
       "           'HTHT',\n",
       "           'HTTH',\n",
       "           'HTTT',\n",
       "           'THHH',\n",
       "           'THHT',\n",
       "           'THTH',\n",
       "           'THTT',\n",
       "           'TTHH',\n",
       "           'TTHT',\n",
       "           'TTTH'})"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "update(all_coins, {0, 1, 2, 3})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Everything looks good so far. One more thing: we need to find all subsets of the 4 positions; these are the possible moves:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "def powerset(sequence): \n",
    "    \"All subsets of a sequence.\"\n",
    "    return [set(c) \n",
    "            for r in range(len(sequence) + 1)\n",
    "            for c in combinations(sequence, r)]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[set(),\n",
       " {0},\n",
       " {1},\n",
       " {2},\n",
       " {3},\n",
       " {0, 1},\n",
       " {0, 2},\n",
       " {0, 3},\n",
       " {1, 2},\n",
       " {1, 3},\n",
       " {2, 3},\n",
       " {0, 1, 2},\n",
       " {0, 1, 3},\n",
       " {0, 2, 3},\n",
       " {1, 2, 3},\n",
       " {0, 1, 2, 3}]"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "powerset(range(4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Search for a Solution\n",
    "\n",
    "The function `search` does a breadth-first search starting\n",
    "from a `belief` state consisting of `all_coins`, and applying a sequences of moves, trying to\n",
    "find a sequence that leads to the goal belief state in which the only possibility is 4 heads: `{'HHHH'}`.\n",
    "As is typical for search algorithms, we build a search tree, keeping a queue of tree `nodes` to consider, where each \n",
    "node consists of a path (a sequence of moves) and a resulting belief state. We also keep track, in `explored`, of\n",
    "the states we have already explored, so that we don't have to revisit them."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "def search():\n",
    "    \"Breadth-first search from all_coins to all heads.\"\n",
    "    explored = set()\n",
    "    queue = deque([Node([], all_coins)])\n",
    "    while queue:\n",
    "        (path, belief) = queue.popleft()\n",
    "        if belief == {'HHHH'}:\n",
    "            return path\n",
    "        for move in powerset(range(4)):\n",
    "            belief2 = update(belief, move)\n",
    "            if belief2 not in explored:\n",
    "                explored.add(belief2)\n",
    "                queue.append(Node(path + [move], belief2))\n",
    "                \n",
    "def Node(path, belief) -> tuple: return (path, belief)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# A Solution"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[{0, 1, 2, 3},\n",
       " {0, 2},\n",
       " {0, 1, 2, 3},\n",
       " {0, 1},\n",
       " {0, 1, 2, 3},\n",
       " {0, 2},\n",
       " {0, 1, 2, 3},\n",
       " {0},\n",
       " {0, 1, 2, 3},\n",
       " {0, 2},\n",
       " {0, 1, 2, 3},\n",
       " {0, 1},\n",
       " {0, 1, 2, 3},\n",
       " {0, 2},\n",
       " {0, 1, 2, 3}]"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "search()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "That's a 15-move sequence that is guaranteed to lead to a win. You can stop reading here if all you want is the answer to the puzzle. Or you can continue on ...\n",
    "\n",
    "# Verifying the Solution\n",
    "\n",
    "Can I verify that the solution is correct?  Exploring with paper and pencil, it does appear to work. A colleague did the puzzle and got the same answer. And here's further validation: The function `random_devil` takes an initial coin sequence and a sequence of moves, and plays those moves with a devil that chooses rotations randomly, returning the number of moves it takes until the player wins:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "def random_devil(coins, moves) -> int or None:\n",
    "    \"\"\"A random devil responds to moves starting from coins; \n",
    "    return the number of moves until win, or None.\"\"\"\n",
    "    if coins == 'HHHH': return 0\n",
    "    for (i, move) in enumerate(moves, 1):\n",
    "        coins = flip(random.choice(list(rotations(coins))), move)\n",
    "        if coins == 'HHHH': \n",
    "            return i"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "I will let the `random_devil` play 1000 times from each possible starting coin sequence:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "Counter({0: 1000,\n",
       "         1: 1000,\n",
       "         2: 986,\n",
       "         3: 1014,\n",
       "         4: 1052,\n",
       "         5: 1004,\n",
       "         6: 976,\n",
       "         7: 968,\n",
       "         8: 964,\n",
       "         9: 1014,\n",
       "         10: 985,\n",
       "         11: 1003,\n",
       "         12: 1056,\n",
       "         13: 983,\n",
       "         14: 1021,\n",
       "         15: 974})"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "moves = search()\n",
    "\n",
    "Counter(random_devil(coins, moves) \n",
    "        for coins in all_coins\n",
    "        for _ in range(1000))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This says that the player won all 16,000 times. (If the player had ever lost, there would have been an entry for `None` in the Counter.)\n",
    "The remarkable thing, which I can't explain, is that there are very nearly exactly 1,000 results for each of the move counts from 0 to 15. Can you explain that?\n",
    "\n",
    "# Canonical Coin Sequences and Moves\n",
    "\n",
    "Consider the four coin sequences `{'HHHT', 'HHTH', 'HTHH', 'THHH'}`. In a sense, these are all the same: they all denote the same sequence of coins with the table rotated to different degrees. Since the devil is free to rotate the table any amount at any time, we could be justified in treating all four of these as equivalent, and collapsing them into one representative member (we could arbitrarily choose the one that comes first in alphabetical order, `'HHHT'`). I will write a definition for `Belief` that returns a `frozenset`, just like before, but makes it a set of canonical coin sequences.\n",
    "\n",
    "Similarly, the four moves `[{3}, {2}, {1}, {0}]` are equivalent, in that they all say \"flip one of the coins, but since you're going to rotate the table first, it doesn't matter which one I specify.\" I will write a function `canonical_moves` that lists all the canonical rotationally invariant moves corresponding to a belief state."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "def Belief(coin_collection): \n",
    "    \"A set of all the coin sequences in this collection, canonicalized.\"\n",
    "    return frozenset(min(rotations(c)) for c in coin_collection)\n",
    "\n",
    "def canonical_moves(coin_collection):\n",
    "    \"All rotationally invariant moves for a sequence of N coins.\"\n",
    "    return [set(i for (i, coin) in enumerate(coins) if coin == 'H' and 'H')\n",
    "            for coins in sorted(coin_collection) if 'H' in coins]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "frozenset({'HHHH', 'HHHT', 'HHTT', 'HTHT', 'HTTT', 'TTTT'})"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "Belief(all_coins)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[{0, 1, 2, 3}, {0, 1, 2}, {0, 1}, {0, 2}, {0}]"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "canonical_moves(_)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The starting belief set is down from 16 sequences to 6. Call these  4 heads, 3 heads, 2 adjacent heads, 2 opposite heads, 1 head, and no heads, respectively. Similarly, there are only 5 distinct moves, which we can call flip 4, flip 3, flip 2 adjacent, flip 2 opposite, and flip 1.\n",
    "\n",
    "I have to confess to a form of cheating here: I threw in the `sorted` in `canonical_moves` even though it was not necessary. By including it, I made sure that `{0, 1, 2, 3}` is the first of the possible moves, which makes `search` faster, because half the time `{0, 1, 2, 3}` is the right move to make.\n",
    "\n",
    "# Visualizing the Solution\n",
    "\n",
    "How does the solution work? One answer is \"it takes care of all possibilities.\" But it would be nice to gain more insight. I'll print a table showing the belief state after each move, using the newly-defined canonicalized `Belief` form, and lining them up neatly in columns."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "def show(moves, belief=all_coins):\n",
    "    \"For each move, print the move number, move, and belief state.\"\n",
    "    show_line(0, 'start', belief)\n",
    "    for (i, move) in enumerate(moves, 1):\n",
    "        belief = update(belief, move)\n",
    "        show_line(i, move, belief)\n",
    "\n",
    "def show_line(i, move, belief, order=sorted(Belief(all_coins))):\n",
    "    \"Print the move number, move, and belief state.\"\n",
    "    ordered_belief = [(coins if coins in belief else '    ')\n",
    "                      for coins in order]\n",
    "    print('{:2} | {:5} | {} | {}'\n",
    "          .format(i, join(move), join(ordered_belief, ' '), i))\n",
    "    \n",
    "def join(items, sep='') -> str: return sep.join(map(str, items))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      " 0 | start | HHHH HHHT HHTT HTHT HTTT TTTT | 0\n",
      " 1 | 0123  | HHHH HHHT HHTT HTHT HTTT      | 1\n",
      " 2 | 02    | HHHH HHHT HHTT      HTTT TTTT | 2\n",
      " 3 | 0123  | HHHH HHHT HHTT      HTTT      | 3\n",
      " 4 | 01    | HHHH HHHT      HTHT HTTT TTTT | 4\n",
      " 5 | 0123  | HHHH HHHT      HTHT HTTT      | 5\n",
      " 6 | 02    | HHHH HHHT           HTTT TTTT | 6\n",
      " 7 | 0123  | HHHH HHHT           HTTT      | 7\n",
      " 8 | 0     | HHHH      HHTT HTHT      TTTT | 8\n",
      " 9 | 0123  | HHHH      HHTT HTHT           | 9\n",
      "10 | 02    | HHHH      HHTT           TTTT | 10\n",
      "11 | 0123  | HHHH      HHTT                | 11\n",
      "12 | 01    | HHHH           HTHT      TTTT | 12\n",
      "13 | 0123  | HHHH           HTHT           | 13\n",
      "14 | 02    | HHHH                     TTTT | 14\n",
      "15 | 0123  | HHHH                          | 15\n"
     ]
    }
   ],
   "source": [
    "show(search())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can see that every odd-numbered move flips all four coins to eliminate the possibility of `TTTT`, flipping it to `HHHH`. We can also see that moves 2, 4, and 6 flip two coins and have the effect of eventually eliminating the two \"two heads\" sequences from the belief state, and then move 8 flips a single coin, thus eliminating the \"three heads\" and \"one heads\" sequences, and bringing back the \"two heads\" possibilities. Repeating moves 2, 4, and 6 in moves 10, 12, and 14 then re-eliminates the \"two heads\", and move 15 gets the belief state down to `{'HHHH'}`.\n",
    "\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Different Numbers of Coins\n",
    "\n",
    "What if there are 3 coins on the table arranged in a triangle? Or 6 coins in a hexagon? To answer that, I'll generalize the two functions that have a \"4\" in them, `search`, and `rotations`.  I'll also generalize `update`, which had `'HHHH'` in it. And I'll introduce the function `canonical_coins` to return a belief state of all canonical coin sequences of length `N`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {
    "collapsed": false
   },
   "outputs": [],
   "source": [
    "def search(N=4):\n",
    "    \"Breadth-first search from all_coins to all heads.\"\n",
    "    start     = canonical_coins(N)\n",
    "    all_moves = canonical_moves(start)\n",
    "    goal      = {'H' * N}\n",
    "    explored  = set()\n",
    "    queue     = deque([Node([], start)])\n",
    "    while queue:\n",
    "        (path, belief) = queue.popleft()\n",
    "        if belief == goal:\n",
    "            return path\n",
    "        for move in all_moves:\n",
    "            belief2 = update(belief, move)\n",
    "            if belief2 not in explored:\n",
    "                explored.add(belief2)\n",
    "                queue.append(Node(path + [move], belief2))\n",
    "\n",
    "def rotations(coins) -> [Coins]: \n",
    "    \"A list of all possible rotations of a coin sequence.\"\n",
    "    return [coins[r:] + coins[:r] for r in range(len(coins))]\n",
    "                       \n",
    "def update(belief, move) -> Belief:\n",
    "    \"Update belief: consider all possible rotations, then flip.\"\n",
    "    return Belief((flip(c, move) if 'T' in c else c)\n",
    "                  for coins in belief\n",
    "                  for c in rotations(coins))\n",
    "\n",
    "def canonical_coins(N) -> Belief: return Belief(map(Coins, product('HT', repeat=N)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "First verify that we didn't break `search`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[{0, 1, 2, 3},\n",
       " {0, 2},\n",
       " {0, 1, 2, 3},\n",
       " {0, 1},\n",
       " {0, 1, 2, 3},\n",
       " {0, 2},\n",
       " {0, 1, 2, 3},\n",
       " {0, 1, 2},\n",
       " {0, 1, 2, 3},\n",
       " {0, 2},\n",
       " {0, 1, 2, 3},\n",
       " {0, 1},\n",
       " {0, 1, 2, 3},\n",
       " {0, 2},\n",
       " {0, 1, 2, 3}]"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "search(4)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now test the new definitions:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "['HHHHHT', 'HHHHTH', 'HHHTHH', 'HHTHHH', 'HTHHHH', 'THHHHH']"
      ]
     },
     "execution_count": 19,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "rotations('HHHHHT')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "frozenset({'HHHH', 'HHHT', 'HHTT', 'HTHT', 'HTTT', 'TTTT'})"
      ]
     },
     "execution_count": 20,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "canonical_coins(4)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "frozenset({'HHHHH',\n",
       "           'HHHHT',\n",
       "           'HHHTT',\n",
       "           'HHTHT',\n",
       "           'HHTTT',\n",
       "           'HTHTT',\n",
       "           'HTTTT',\n",
       "           'TTTTT'})"
      ]
     },
     "execution_count": 21,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "canonical_coins(5)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[{0, 1, 2, 3, 4}, {0, 1, 2, 3}, {0, 1, 2}, {0, 1, 3}, {0, 1}, {0, 2}, {0}]"
      ]
     },
     "execution_count": 22,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "canonical_moves(_)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{1: 2, 2: 3, 3: 4, 4: 6, 5: 8, 6: 14, 7: 20, 8: 36, 9: 60, 10: 108}"
      ]
     },
     "execution_count": 23,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "{N: len(canonical_coins(N))\n",
    " for N in range(1, 11)}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "On the one hand this is encouraging; there are only 108 canonical coin sequences of length 10, far less than the 1024 non-canonical squences. On the other hand, it is discouraging; since we are searching over the belief states, that would be 2<sup>108</sup> ≌ 10<sup>32</sup> belief states, which is infeasible. However, we should be able to easily handle up to N=7, because 2<sup>20</sup> is only a million.\n",
    "\n",
    "# Solutions for 1 to 7 Coins"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{1: [{0}],\n",
       " 2: [{0, 1}, {0}, {0, 1}],\n",
       " 3: None,\n",
       " 4: [{0, 1, 2, 3},\n",
       "  {0, 2},\n",
       "  {0, 1, 2, 3},\n",
       "  {0, 1},\n",
       "  {0, 1, 2, 3},\n",
       "  {0, 2},\n",
       "  {0, 1, 2, 3},\n",
       "  {0, 1, 2},\n",
       "  {0, 1, 2, 3},\n",
       "  {0, 2},\n",
       "  {0, 1, 2, 3},\n",
       "  {0, 1},\n",
       "  {0, 1, 2, 3},\n",
       "  {0, 2},\n",
       "  {0, 1, 2, 3}],\n",
       " 5: None,\n",
       " 6: None,\n",
       " 7: None}"
      ]
     },
     "execution_count": 24,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "{N: search(N) for N in range(1, 8)}"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Too bad; there are no solutions for N = 3, 5, 6, or 7.  \n",
    "\n",
    "There are solutions for N = 1, 2, 4; of lengths 1, 3, 15. That suggests the conjecture: for every *N* that is a power of 2, there will be a solution of length 2<sup>*N*</sup> - 1. \n",
    "\n",
    "# Solution for 8 Coins\n",
    "\n",
    "Even though there are 2<sup>36</sup> = 68 billion belief states fo N = 8, I hope we can find a solution early in the search process. Let's try:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "CPU times: user 1min 47s, sys: 731 ms, total: 1min 48s\n",
      "Wall time: 1min 50s\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "255"
      ]
     },
     "execution_count": 25,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "%time solution8 = search(8)\n",
    "len(solution8)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Eureka! That's evidence in favor of my conjecture, and it took less time than I thought it would. It is still not proof, and I'll need a more efficient/clever approach to test even for N=16. Can you come up with that approach? Can you prove the conjecture for all powers of 2? Here's the solution for N = 8 to help you discover a pattern:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {
    "collapsed": false
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[{0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 4, 5},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 2, 4, 5, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 4, 5},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 2, 3},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 4, 5},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 2, 4, 5, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 4, 5},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 2, 3, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 4, 5},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 2, 4, 5, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 4, 5},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 2, 3},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 4, 5},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 2, 4, 5, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 4, 5},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 2, 3, 4, 5},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 4, 5},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 2, 4, 5, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 4, 5},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 2, 3},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 4, 5},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 2, 4, 5, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 4, 5},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 2, 3, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 4, 5},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 2, 4, 5, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 4, 5},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 2, 3},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 4, 5},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 2, 4, 5, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 4, 5},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 2, 3, 4, 5, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 4, 5},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 2, 4, 5, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 4, 5},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 2, 3},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 4, 5},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 2, 4, 5, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 4, 5},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 2, 3, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 4, 5},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 2, 4, 5, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 4, 5},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 2, 3},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 4, 5},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 2, 4, 5, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 4, 5},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 2, 3, 4, 5},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 4, 5},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 2, 4, 5, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 4, 5},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 2, 3},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 4, 5},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 2, 4, 5, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 4, 5},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 2, 3, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 4, 5},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 2, 4, 5, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 4, 5},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 2, 3},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 4, 5},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 2, 4, 5, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 1, 4, 5},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7},\n",
       " {0, 2, 4, 6},\n",
       " {0, 1, 2, 3, 4, 5, 6, 7}]"
      ]
     },
     "execution_count": 26,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "solution8"
   ]
  }
 ],
 "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.6.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
