From 39c9b52ea2c835ddd7b5836c945a25af8be5ebbf Mon Sep 17 00:00:00 2001 From: Peter Norvig Date: Fri, 5 Jan 2018 20:50:31 -0800 Subject: [PATCH] Add files via upload --- Coin Flip.ipynb | 1172 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1172 insertions(+) create mode 100644 Coin Flip.ipynb diff --git a/Coin Flip.ipynb b/Coin Flip.ipynb new file mode 100644 index 0000000..c52082e --- /dev/null +++ b/Coin Flip.ipynb @@ -0,0 +1,1172 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# The Devil and the Coin Flip Game\n", + "\n", + "If the Devil ever challenges me to a [fiddle contest](https://en.wikipedia.org/wiki/The_Devil_Went_Down_to_Georgia), I'm going to lose. I'd have a better chance at this contest:\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 the 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 24 = 16 possibilities in the belief state:\n", + "\n", + " {HHHH, HHHT, HHTH, HHTT, HTHH, HTHT, HTTH, HTTT, THHH, THHT, THTH, THTT, TTHH, TTHT, TTTH, TTTT}\n", + "\n", + "The idea is that even though the player doesn't know for sure what the actual state of the coins is, nor what rotations the devil performs, the player can still manipulate the belief state towards the goal belief state:\n", + "\n", + " {HHHH}\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 list of moves that ends in the belief state `{'HHHH'}`. It must be a shortest possible sequence of moves, so a [breadth-first search](https://en.wikipedia.org/wiki/Breadth-first_search) is appropriate. The search space is small (just 216 possible belief states), so run time will be fast; the only issue is specifying the domain correctly. To increase the chance of getting it right, 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* (on the table) 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).\n", + "- `Position`: A position is an integer index into the coin sequence; position `0` selects the `H` in `'HTTT'`\n", + "and corresponds to the 12 o'clock position; position 1 corresponds to 3 o'clock, and so on.\n", + "- `Move`: A *move* is a set of positions to flip, such as `{0, 1}`. \n", + "- `Strategy`: A strategy for playing the game is just a list of moves. Since there is no feedback while playing\n", + "(the player is blindfolded) there is no need for decision points in the strategy.\n", + "- `all_coins()`: A belief state consisting of the set of all possible coin sequences: `{'HHHH', 'HHHT', ...}`.\n", + "- `rotations(coins)`: returns a set of all 4 rotations of the coin sequence.\n", + "- `update(belief, move)`: an updated belief state: all the coin sequences that could result from any rotation followed by the specified flip(s).\n", + "- `flip(coins, move)`: flips the specified positions within the coin sequence.\n", + " (But don't flip `'HHHH'`, because the game would have already ended if this were the coin sequence.)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "from collections import deque, Counter\n", + "from itertools import product, combinations\n", + "import random\n", + "\n", + "Coins = ''.join # A coin sequence; a str: 'HHHT'.\n", + "Belief = frozenset # A set of possible coin sequences: {'HHHT', 'TTTH'}\n", + "Move = set # A set of positions to flip: {0, 1}\n", + "Strategy = list # A list of Moves: [{0, 1}, {0, 1, 2, 3}, ...]\n", + "\n", + "def all_coins() -> Belief:\n", + " \"Return the belief set consisting of all possible coin sequences.\"\n", + " return 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(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 (but leave 'HHHH' alone).\"\n", + " if coins == 'HHHH': return coins\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": {}, + "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": {}, + "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": {}, + "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": {}, + "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": true + }, + "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": {}, + "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 generic function `search` does a breadth-first search starting\n", + "from a `start` state, looking for a `goal` state, considering possible `actions` (a collection of moves) at each turn,\n", + "and computing the `result` of each action (`result` is a function such that `result(state, action)` returns the new state that results from executing the action in the current state). It works by keeping a queue of unexplored possibilities, where each entry in the queue is a pair consisting of a *strategy* (sequence of moves) and a *state* that that strategy leads to. We also keep a set of `explored` states, so that we don't repeat ourselves.\n", + "\n", + "Amazingly, \n", + "even though we want to search in the space of *belief states*, we can still use the generic search function that is designed for regular-old-states. The search for belief states just works, as long as we properly specify the start state, the goal state, and the means of moving between states." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def search(start, goal, actions, result) -> Strategy:\n", + " \"Breadth-first search from start state to goal; return strategy to get to goal.\"\n", + " explored = set()\n", + " queue = deque([([], start)])\n", + " while queue:\n", + " (strategy, state) = queue.popleft()\n", + " if state == goal:\n", + " return strategy\n", + " for action in actions:\n", + " state2 = result(state, action)\n", + " if state2 not in explored:\n", + " explored.add(state2)\n", + " queue.append((strategy + [action], state2))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The `coin_search` function calls the generic `search` function to solve our specific problem:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def coin_search() -> Strategy: \n", + " \"Use `search` to solve the Coin Flip problem.\"\n", + " return search(start=all_coins(), goal={'HHHH'}, \n", + " actions=powerset(range(4)), result=update)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# A Solution" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "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": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "coin_search()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "That's a 15-move strategy that is guaranteed to lead to a win. Stop here if all you want is the answer to the puzzle. Or you can continue on ...\n", + "\n", + "----\n", + "\n", + "# Verifying the Solution\n", + "\n", + "I don't have a proof, but I have some evidence that the solution is correct:\n", + "- Exploring with paper and pencil, it does appear to work. \n", + "- A colleague did the puzzle and got the same answer. \n", + "- Running the function `random_devil` below is consistent with it working.\n", + "\n", + "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. Note this is dealing with concrete, individual states of the world, like `HTHH`, not belief states." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def random_devil(coins, strategy) -> 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(strategy, 1):\n", + " coins = flip(random.choice(list(rotations(coins))), move)\n", + " if coins == 'HHHH': \n", + " return i\n", + " return None" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "I will let the `random_devil` play 10,000 times from each possible starting coin sequence:" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Counter({0: 10000,\n", + " 1: 10000,\n", + " 2: 10033,\n", + " 3: 9967,\n", + " 4: 9996,\n", + " 5: 9962,\n", + " 6: 10027,\n", + " 7: 10015,\n", + " 8: 9895,\n", + " 9: 10011,\n", + " 10: 9995,\n", + " 11: 10127,\n", + " 12: 10019,\n", + " 13: 9902,\n", + " 14: 10074,\n", + " 15: 9977})" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "strategy = coin_search()\n", + "\n", + "Counter(random_devil(coins, strategy) \n", + " for coins in all_coins()\n", + " for _ in range(10000))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This says that the player won all 160,000 times. (If the player had ever lost, there would have been an entry for `None` in the Counter.) This suggests the strategy is likely to win, but doesn't prove it will always win, and doesn't say anything about the possibility of a shorter solution.\n", + "The remarkable thing, which I can't explain, is that there are very nearly exactly 10,000 results for each of the move counts from 0 to 15. Can you explain that?\n", + "\n", + "# Canonical Coin Sequences\n", + "\n", + "Consider these 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 redefine `Belief` as a function that returns a `frozenset`, just like before, but makes it a set of `canonical` coin sequences." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def canonical(coins): return min(rotations(coins))\n", + "\n", + "def Belief(coin_collection): \n", + " \"A set of all the coin sequences in this collection, canonicalized.\"\n", + " return frozenset(map(canonical, coin_collection))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "With `Belief` redefined, the result of calling `all_coins` will be different:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "frozenset({'HHHH', 'HHHT', 'HHTT', 'HTHT', 'HTTT', 'TTTT'})" + ] + }, + "execution_count": 14, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "all_coins()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The starting belief set is down from 16 to 6, namely 4 heads, 3 heads, 2 adjacent heads, 2 opposite heads, 1 head, and no heads, respectively. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Solutions for *N* 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 three functions that have a \"4\" in them, `all_coins`, `rotations` and `coin_search`. I'll also generalize `flip`, which had `'HHHH'` in it. " + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def all_coins(N=4) -> Belief:\n", + " \"Return the belief set consisting of all possible coin sequences.\"\n", + " return Belief(map(Coins, product('HT', repeat=N)))\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 coin_search(N=4) -> Strategy: \n", + " \"Use the generic `search` function to solve the Coin Flip problem.\"\n", + " return search(start=all_coins(N), goal={'H' * N}, \n", + " actions=all_moves(N), result=update)\n", + "\n", + "def flip(coins, move) -> Coins:\n", + " \"Flip the coins in the positions specified by the move (but leave all 'H' alone).\"\n", + " if 'T' not in coins: return coins\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": [ + "I also need to generalize `all_moves`. To compute the set of possible moves for 4 coins, I used `powerset`, and got 16 possible moves. Now I want to know the set od canonicalized moves for any *N*. To get that, I'll look at the canonicalized set of `all_coins(N)`, and for each one pull out the positions that have an `H` in them, and flip those (the positions with a `T` should be symmetric, so we don't need them as well)." + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def all_moves(N) -> [Move]:\n", + " \"All rotationally invariant moves for a sequence of N coins.\"\n", + " return [set(i for (i, coin) in enumerate(coins) if coin == 'H')\n", + " for coins in sorted(all_coins(N))]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's test the new definitions:" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'ok'" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "assert all_moves(4) == [{0, 1, 2, 3}, {0, 1, 2}, {0, 1}, {0, 2}, {0}, set()]\n", + "assert all_coins(4) == {'HHHH', 'HHHT', 'HHTT', 'HTHT', 'HTTT', 'TTTT'}\n", + "assert all_coins(5) == {'HHHHH','HHHHT', 'HHHTT','HHTHT','HHTTT', 'HTHTT', 'HTTTT', 'TTTTT'}\n", + "assert rotations('HHHHHT') == {'HHHHHT', 'HHHHTH', 'HHHTHH', 'HHTHHH', 'HTHHHH', 'THHHHH'}\n", + "assert update({'TTTTTTT'}, {3}) == {'HTTTTTT'}\n", + "assert (update(rotations('HHHHHT'), {0}) == update({'HHHHHT'}, {1 })== update({'HHHHHT'}, {2})\n", + " == {'HHHHHH', 'HHHHTT', 'HHHTHT', 'HHTHHT'})\n", + "'ok'" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{0, 1, 2, 3}, {0, 1, 2}, {0, 1}, {0, 2}, {0}, set()]" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "all_moves(4)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'HHHHHT', 'HHHHTH', 'HHHTHH', 'HHTHHH', 'HTHHHH', 'THHHHH'}" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + " rotations('HHHHHT')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "With 4 coins we can flip 4, flip 3, flip 2 adjacent, flip 2 opposite, flip 1, or flip nothing.\n", + "Similarly, with 4 coins there can be 4 heads 3 heads, 2 adjacent heads, 2 opposite heads, 1 head, or no heads.\n", + "If we start with 6 coins of which one is `'T'`, and do a single flip (and it doesn't matter what position that flip is), then either the Devil flips the `'T'`, in which case we get all heads, or it can flip an `'H'` which is either adjacent to, 2 away from, or 3 away from the existing `'T'`.\n", + "\n", + "How many distinct canonical coin sequences are there for *N* coins from 1 to 10?" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "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": 20, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "{N: len(all_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 2108 ≌ 1032 belief states, which is infeasible. However, we should be able to easily handle up to N=7, because 220 is only a million.\n", + "\n", + "# Solutions for 1 to 7 Coins" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "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": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "{N: coin_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; they have lengths 1, 3, 15, respectively. That suggests the conjecture: \n", + "\n", + "> For every *N* that is a power of 2, there will be a shortest solution of length 2*N* - 1.\n", + "\n", + "> For every *N* that is not a power of 2, there will be no solution. \n", + "\n", + "# Solution for 8 Coins\n", + "\n", + "For N = 8, there are 236 = 69 billion belief states and the desired solution has 255 steps. All the computations up to now have been instantaneous, but this one should take a few minutes. Let's see:" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 1min 22s, sys: 288 ms, total: 1min 22s\n", + "Wall time: 1min 23s\n" + ] + } + ], + "source": [ + "%time solution8 = coin_search(8)" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "255" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(solution8)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Eureka! That's evidence in favor of the conjecture. But not proof. And it leaves many questions unanswered:\n", + "- Can you show there are no solutions for *N* = 9, 10, 11, ...?\n", + "- Can you prove there are no solutions for any *N* that is not a power of 2?\n", + "- Can you find a solution of length 65,535 for *N* = 16 and verify that it works?\n", + "- Can you generate a solution for any power of 2 (without proving it is shortest)?\n", + "- Can you prove there are no shorter solutions for *N* = 16?\n", + "- Can you prove the conjecture in general?\n", + "- Can you *understand* and *explain* how the solution works, rather than just listing the moves?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Visualizing the Solution\n", + "\n", + "To aid understanding, I'll print a table showing the belief state after each move, using the canonicalized `Belief` form, lined up neatly in columns." + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": { + "collapsed": true + }, + "outputs": [], + "source": [ + "def show(moves, N=4):\n", + " \"For each move, print the move number, move, and belief state.\"\n", + " belief = all_coins(N)\n", + " order = sorted(belief)\n", + " show_line(0, {}, belief, order, N)\n", + " for (i, move) in enumerate(moves, 1):\n", + " belief = update(belief, move)\n", + " show_line(i, move, belief, order, N)\n", + "\n", + "def show_line(i, move, belief, order, N):\n", + " \"Print the move number, move, and belief state.\"\n", + " ordered_belief = [(coins if coins in belief else ' ' * len(coins))\n", + " for coins in order]\n", + " movestr = join((i if i in move else ' ') for i in range(N))\n", + " print('{:3} | {:8} | {} | {}'\n", + " .format(i, movestr, join(ordered_belief, ' '), i))\n", + " \n", + "def join(items, sep='') -> str: return sep.join(map(str, items))" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " 0 | | HHHH HHHT HHTT HTHT HTTT TTTT | 0\n", + " 1 | 0123 | HHHH HHHT HHTT HTHT HTTT | 1\n", + " 2 | 0 2 | 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 | 0 2 | HHHH HHHT HTTT TTTT | 6\n", + " 7 | 0123 | HHHH HHHT HTTT | 7\n", + " 8 | 012 | HHHH HHTT HTHT TTTT | 8\n", + " 9 | 0123 | HHHH HHTT HTHT | 9\n", + " 10 | 0 2 | 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 | 0 2 | HHHH TTTT | 14\n", + " 15 | 0123 | HHHH | 15\n" + ] + } + ], + "source": [ + "show(coin_search(4))" + ] + }, + { + "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 eliminates the \"three heads\" and \"one heads\" sequences, while 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", + "You could call `show(solution8)`, but the results look bad unless you have a very wide (340 characters) screen to view it on. So I'll just show `solution8` itself, with move numbers:\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{1: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 2: {0, 2, 4, 6},\n", + " 3: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 4: {0, 1, 4, 5},\n", + " 5: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 6: {0, 2, 4, 6},\n", + " 7: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 8: {0, 1, 2, 4, 5, 6},\n", + " 9: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 10: {0, 2, 4, 6},\n", + " 11: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 12: {0, 1, 4, 5},\n", + " 13: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 14: {0, 2, 4, 6},\n", + " 15: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 16: {0, 1, 2, 3},\n", + " 17: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 18: {0, 2, 4, 6},\n", + " 19: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 20: {0, 1, 4, 5},\n", + " 21: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 22: {0, 2, 4, 6},\n", + " 23: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 24: {0, 1, 2, 4, 5, 6},\n", + " 25: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 26: {0, 2, 4, 6},\n", + " 27: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 28: {0, 1, 4, 5},\n", + " 29: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 30: {0, 2, 4, 6},\n", + " 31: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 32: {0, 1, 2, 3, 4, 6},\n", + " 33: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 34: {0, 2, 4, 6},\n", + " 35: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 36: {0, 1, 4, 5},\n", + " 37: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 38: {0, 2, 4, 6},\n", + " 39: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 40: {0, 1, 2, 4, 5, 6},\n", + " 41: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 42: {0, 2, 4, 6},\n", + " 43: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 44: {0, 1, 4, 5},\n", + " 45: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 46: {0, 2, 4, 6},\n", + " 47: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 48: {0, 1, 2, 3},\n", + " 49: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 50: {0, 2, 4, 6},\n", + " 51: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 52: {0, 1, 4, 5},\n", + " 53: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 54: {0, 2, 4, 6},\n", + " 55: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 56: {0, 1, 2, 4, 5, 6},\n", + " 57: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 58: {0, 2, 4, 6},\n", + " 59: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 60: {0, 1, 4, 5},\n", + " 61: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 62: {0, 2, 4, 6},\n", + " 63: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 64: {0, 1, 2, 3, 4, 5},\n", + " 65: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 66: {0, 2, 4, 6},\n", + " 67: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 68: {0, 1, 4, 5},\n", + " 69: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 70: {0, 2, 4, 6},\n", + " 71: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 72: {0, 1, 2, 4, 5, 6},\n", + " 73: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 74: {0, 2, 4, 6},\n", + " 75: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 76: {0, 1, 4, 5},\n", + " 77: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 78: {0, 2, 4, 6},\n", + " 79: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 80: {0, 1, 2, 3},\n", + " 81: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 82: {0, 2, 4, 6},\n", + " 83: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 84: {0, 1, 4, 5},\n", + " 85: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 86: {0, 2, 4, 6},\n", + " 87: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 88: {0, 1, 2, 4, 5, 6},\n", + " 89: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 90: {0, 2, 4, 6},\n", + " 91: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 92: {0, 1, 4, 5},\n", + " 93: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 94: {0, 2, 4, 6},\n", + " 95: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 96: {0, 1, 2, 3, 4, 6},\n", + " 97: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 98: {0, 2, 4, 6},\n", + " 99: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 100: {0, 1, 4, 5},\n", + " 101: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 102: {0, 2, 4, 6},\n", + " 103: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 104: {0, 1, 2, 4, 5, 6},\n", + " 105: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 106: {0, 2, 4, 6},\n", + " 107: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 108: {0, 1, 4, 5},\n", + " 109: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 110: {0, 2, 4, 6},\n", + " 111: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 112: {0, 1, 2, 3},\n", + " 113: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 114: {0, 2, 4, 6},\n", + " 115: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 116: {0, 1, 4, 5},\n", + " 117: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 118: {0, 2, 4, 6},\n", + " 119: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 120: {0, 1, 2, 4, 5, 6},\n", + " 121: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 122: {0, 2, 4, 6},\n", + " 123: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 124: {0, 1, 4, 5},\n", + " 125: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 126: {0, 2, 4, 6},\n", + " 127: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 128: {0, 1, 2, 3, 4, 5, 6},\n", + " 129: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 130: {0, 2, 4, 6},\n", + " 131: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 132: {0, 1, 4, 5},\n", + " 133: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 134: {0, 2, 4, 6},\n", + " 135: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 136: {0, 1, 2, 4, 5, 6},\n", + " 137: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 138: {0, 2, 4, 6},\n", + " 139: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 140: {0, 1, 4, 5},\n", + " 141: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 142: {0, 2, 4, 6},\n", + " 143: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 144: {0, 1, 2, 3},\n", + " 145: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 146: {0, 2, 4, 6},\n", + " 147: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 148: {0, 1, 4, 5},\n", + " 149: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 150: {0, 2, 4, 6},\n", + " 151: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 152: {0, 1, 2, 4, 5, 6},\n", + " 153: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 154: {0, 2, 4, 6},\n", + " 155: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 156: {0, 1, 4, 5},\n", + " 157: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 158: {0, 2, 4, 6},\n", + " 159: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 160: {0, 1, 2, 3, 4, 6},\n", + " 161: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 162: {0, 2, 4, 6},\n", + " 163: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 164: {0, 1, 4, 5},\n", + " 165: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 166: {0, 2, 4, 6},\n", + " 167: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 168: {0, 1, 2, 4, 5, 6},\n", + " 169: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 170: {0, 2, 4, 6},\n", + " 171: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 172: {0, 1, 4, 5},\n", + " 173: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 174: {0, 2, 4, 6},\n", + " 175: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 176: {0, 1, 2, 3},\n", + " 177: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 178: {0, 2, 4, 6},\n", + " 179: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 180: {0, 1, 4, 5},\n", + " 181: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 182: {0, 2, 4, 6},\n", + " 183: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 184: {0, 1, 2, 4, 5, 6},\n", + " 185: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 186: {0, 2, 4, 6},\n", + " 187: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 188: {0, 1, 4, 5},\n", + " 189: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 190: {0, 2, 4, 6},\n", + " 191: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 192: {0, 1, 2, 3, 4, 5},\n", + " 193: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 194: {0, 2, 4, 6},\n", + " 195: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 196: {0, 1, 4, 5},\n", + " 197: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 198: {0, 2, 4, 6},\n", + " 199: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 200: {0, 1, 2, 4, 5, 6},\n", + " 201: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 202: {0, 2, 4, 6},\n", + " 203: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 204: {0, 1, 4, 5},\n", + " 205: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 206: {0, 2, 4, 6},\n", + " 207: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 208: {0, 1, 2, 3},\n", + " 209: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 210: {0, 2, 4, 6},\n", + " 211: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 212: {0, 1, 4, 5},\n", + " 213: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 214: {0, 2, 4, 6},\n", + " 215: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 216: {0, 1, 2, 4, 5, 6},\n", + " 217: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 218: {0, 2, 4, 6},\n", + " 219: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 220: {0, 1, 4, 5},\n", + " 221: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 222: {0, 2, 4, 6},\n", + " 223: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 224: {0, 1, 2, 3, 4, 6},\n", + " 225: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 226: {0, 2, 4, 6},\n", + " 227: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 228: {0, 1, 4, 5},\n", + " 229: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 230: {0, 2, 4, 6},\n", + " 231: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 232: {0, 1, 2, 4, 5, 6},\n", + " 233: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 234: {0, 2, 4, 6},\n", + " 235: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 236: {0, 1, 4, 5},\n", + " 237: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 238: {0, 2, 4, 6},\n", + " 239: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 240: {0, 1, 2, 3},\n", + " 241: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 242: {0, 2, 4, 6},\n", + " 243: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 244: {0, 1, 4, 5},\n", + " 245: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 246: {0, 2, 4, 6},\n", + " 247: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 248: {0, 1, 2, 4, 5, 6},\n", + " 249: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 250: {0, 2, 4, 6},\n", + " 251: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 252: {0, 1, 4, 5},\n", + " 253: {0, 1, 2, 3, 4, 5, 6, 7},\n", + " 254: {0, 2, 4, 6},\n", + " 255: {0, 1, 2, 3, 4, 5, 6, 7}}" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dict(zip(range(1, 256), 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.5.3" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +}