From 2aee2752cb60d4c7c807254c8e7aadb1ecce382a Mon Sep 17 00:00:00 2001 From: Peter Norvig Date: Mon, 28 Aug 2017 17:08:38 -0700 Subject: [PATCH] Add files via upload --- Coin Flip.ipynb | 449 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 449 insertions(+) create mode 100644 Coin Flip.ipynb diff --git a/Coin Flip.ipynb b/Coin Flip.ipynb new file mode 100644 index 0000000..8d5ce40 --- /dev/null +++ b/Coin Flip.ipynb @@ -0,0 +1,449 @@ +{ + "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, and on the table, there are 4 coins, arranged in a diamond, at the 12, 3, 6, and 9 o'clock positions. We'll number those as positions 0, 1, 2, and 3, respectively. Your goal is to get all 4 coins showing heads. You are blindfolded the entire time, and do not know the initial or subsequent state of the coins.\n", + "\n", + ">Your only way of interacting with the coins is to tell the devil the position number(s) of some coins you want flipped. We call this a \"move\" on your part. The devil will faithfully perform the requested flips, but will first sneakily rotate the table either 0, 1, 2, or 3 quarter-turns, so that the coins are in different positions. You keep\n", + "making moves until 4 heads come up.\n", + "\n", + "> Example: You tell the devil to flip positions 0 and 2 (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 locations, which are now the two other coins from the ones you specified. You don't know how much the table was rotated, so you won't know which coins were flipped (and of course you can't see the state of the table before, during, or after the rotating/flipping action).\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 hard part is that we are blindfolded. So we don't know the true state of the coins. We need to represent what we do know: 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. However, some of these possibilities are just rotations of other possibilities, and since the devil is free to apply any rotation at any time, it makes more sense to collapse these possibilities together. For example, a set of four possibilities, `{'HHHT', 'HHTH', 'HTHH', 'THHH'}`, all correspond to having one tails somewhere on the table, and for purposes of the belief state, I will represent this as a single canonical possibility, `{'HHHT'}`. (I arbitrarily chose the\n", + "one that comes first in alphabetical order.)\n", + "\n", + "Once we have the notion of a belief state, we can then update the belief state with the player's move, which is a set of positions to flip, such as `{0, 2}`. The updated belief state consists of every coin sequence in the original belief state, rotated in every possible way, and then with the flips applied. \n", + "\n", + "Note that the game is described as a turn-taking game, but it is equivalent to a open-loop game where the player specifies a complete sequence of moves all at once, and the devil then follows the instructions. So to solve the game, I need to come up with a sequence of moves that ends up in a belief state consisting of just `{'HHHH'}`. I want it to be a shortest path, so a breadth-first search seems reasonable.\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", + "- `all_coins`: Every possible coin sequence.\n", + "- `Belief`: A *belief state* is represented as a `frozenset` of `Coins` (frozen so that it can be hashed in a `set`).\n", + "- `rotations`: The function `rotations(coins)` returns the set of all 4 rotations of coins.\n", + "- `initial_belief`: The set of possible (canonical) coin sequences at the start of the game.\n", + "- `move`: A *move* is a set of positions to flip, such as `{0, 2}`, which means to flip the 12 o'clock and 6 o'clock positions.\n", + "- `update`: The function `update(belief, move)` retuns an updated belief state, representing all the possible coin sequences that could result from a devil rotation followed by the specified flip(s).\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 chain, product, combinations\n", + "import random\n", + "\n", + "Coins = ''.join # Function to make a 4-element Coin Sequence, such as 'HHHT'\n", + "\n", + "all_coins = {Coins(x) for x in product('HT', repeat=4)}\n", + "\n", + "def Belief(coinseq):\n", + " \"The set of possible coin sequences (canonicalized).\"\n", + " return frozenset(min(rotations(coins)) for coins in coinseq)\n", + "\n", + "def rotations(coins): return {coins[r:] + coins[:r] for r in range(4)}\n", + "\n", + "initial_belief = Belief(all_coins)\n", + "\n", + "def update(belief, move):\n", + " \"Update belief: consider all rotations, followed by flips of non-winners.\"\n", + " return Belief((flip(c, move) if c != 'HHHH' else c)\n", + " for coins in belief\n", + " for c in rotations(coins))\n", + "\n", + "def flip(coins, move):\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 some of the functions to see if they look right:" + ] + }, + { + "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": [ + "{'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": "code", + "execution_count": 5, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "frozenset({'HHHH', 'HHHT', 'HHTT', 'HTHT', 'HTTT', 'TTTT'})" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "initial_belief" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The above says that there are 16 possible coin sequences, but only 6 of them are distinct after rotations. We can name the 6: all heads, 3 heads, 2 adjacent heads, 2 opposite heads, 1 head, or all tails." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "frozenset({'HHHH', 'HHHT', 'HHTT', 'HTHT', 'HTTT'})" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "update(initial_belief, {0, 1, 2, 3})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "That says that if we flip all 4 coins, we eliminate the possibility of 4 tails, but all other coin sequences are still possible." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Everything looks good so far. One more thing: we need to find all subsets of the 4 positions:" + ] + }, + { + "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": [ + "def powerset(sequence): \n", + " \"All subsets of a sequence.\"\n", + " # See https://docs.python.org/3.6/library/itertools.html#itertools-recipes\n", + " combos = (combinations(sequence, r) for r in range(len(sequence) + 1))\n", + " return [set(x) for x in chain(*combos)]\n", + "\n", + "powerset(range(4))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Search for a Solution\n", + "\n", + "The function `search` does a breadth-first search starting\n", + "at the initial `belief` state and applying a sequences of `moves`, trying to\n", + "find a path that leads to the goal belief state `{'HHHH'}` (meaning that the only possibility is 4 heads).\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.\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "def search(start=initial_belief, moves=powerset(range(4)), goal={'HHHH'}):\n", + " \"Breadth-first search from starting belief state using moves.\"\n", + " explored = set()\n", + " q = deque([Node([], start)])\n", + " while q:\n", + " (path, belief) = q.popleft()\n", + " if belief == goal:\n", + " return path\n", + " for move in moves:\n", + " belief2 = update(belief, move)\n", + " if belief2 not in explored:\n", + " explored.add(belief2)\n", + " q.append(Node(path + [move], belief2))\n", + " \n", + "def Node(path, belief): return (path, belief)" + ] + }, + { + "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. Do I believe it? Well, I looked into it, and it appears to work. Others who have tried the puzzle got the same answer. But here's another technique to give it further validation: The function `random_play` takes a sequence of moves (like this 15-move sequence) and plays it against a devil that chooses randomly:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "collapsed": false + }, + "outputs": [], + "source": [ + "def random_play(moves=search(), valid_coins=list(all_coins - {'HHHH'})):\n", + " \"Play moves against a random devil; return the number of moves until win, or None.\"\n", + " coins = random.choice(valid_coins)\n", + " for (i, move) in enumerate(moves, 1):\n", + " coins = random.choice(list(rotations(coins)))\n", + " coins = flip(coins, move)\n", + " if coins == 'HHHH': \n", + " return i" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "There are 15 `valid_coins` sequences, so let's call `random_play` 15,000 times, and count the results:" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": { + "collapsed": false + }, + "outputs": [ + { + "data": { + "text/plain": [ + "Counter({1: 1021,\n", + " 2: 1011,\n", + " 3: 997,\n", + " 4: 926,\n", + " 5: 1009,\n", + " 6: 999,\n", + " 7: 1019,\n", + " 8: 951,\n", + " 9: 1028,\n", + " 10: 976,\n", + " 11: 1036,\n", + " 12: 1026,\n", + " 13: 999,\n", + " 14: 1036,\n", + " 15: 966})" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "Counter(random_play() for _ in range(15000))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This says that the player always wins (if the player ever lost, there would be an entry for `None` in the Counter), and the number of moves it takes to win is remarkably evenly distributed." + ] + } + ], + "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 +}