{ "cells": [ { "cell_type": "markdown", "metadata": { "button": false, "deletable": true, "new_sheet": false, "run_control": { "read_only": false } }, "source": [ "# Sicherman Dice\n", "\n", "> Note: This notebook takes the form of a conversation between two problem solvers, *Ali* and ***Bo***. Also note, for those who are not native speakers of English: \"dice\" is the plural form; \"die\" is the singular.\n", "\n", "Ali: Huh. This is interesting. You know how in many games, such as craps or Monopoly, you roll two dice and add them up. Only the sum matters, not what number either of the individual dice shows.\n", "\n", "**Bo: Right.**\n", "\n", "Ali: And some of those sums, like 7, can be made multiple ways, while 2 and 12 can only be made one way. \n", "\n", "**Bo: Yeah. 8 can be made 5 ways, so it has a 5/36 probability of occuring.**\n", "\n", "Ali: The interesting thing is that people have been playing dice games for 7,000 years. But it wasn't until 1977 that Colonel George Sicherman asked whether is is possible to have a pair of dice that are not regular dice—that is, they don't have (1, 2, 3, 4, 5, 6) on the six sides—but have the same distribution of sums as a regular pair—so the pair of dice would also have to have 5 ways of making 8, but it could be different ways; maybe 7+1 could be one way. Sicherman assumes that each side has a positive integer.\n", "\n", "**Bo: And what did he find?**\n", "\n", "Ali: Wouldn't it be more fun to figure it out for ourselves?\n", "\n", "**Bo: OK!**\n", "\n", "Ali: How could we proceed?\n", "\n", "**Bo: When in doubt, [use brute force](http://quotes.lifehack.org/quote/ken-thompson/when-in-doubt-use-brute-force/): we can write a program to enumerate the possibilities:**\n", "\n", "\n", "- **Generate all dice that could possibly be part of a solution, such as (1, 2, 2, 4, 8, 9).**\n", "- **Consider all pairs of these dice, such as ((1, 3, 4, 4, 5, 8), (1, 2, 2, 4, 8, 9))**\n", "- **See if we find any pairs that are not the regular pair, but do have the same distribution of sums as the regular pair.**\n", "\n", "\n", "Ali: That's great. I can code up your description almost verbatim. I'll also keep track of our TO DO list:" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "button": false, "collapsed": false, "deletable": true, "new_sheet": false, "run_control": { "read_only": false } }, "outputs": [], "source": [ "def sicherman():\n", " \"\"\"The set of pairs of 6-sided dice that have the same\n", " distribution of sums as a regular pair of dice.\"\"\"\n", " return {pair for pair in pairs(all_dice())\n", " if pair != regular_pair\n", " and sums(pair) == regular_sums}\n", "\n", "# TODO: pairs, all_dice, regular_pair, sums, regular_sums" ] }, { "cell_type": "markdown", "metadata": { "button": false, "deletable": true, "new_sheet": false, "run_control": { "read_only": false } }, "source": [ "**Bo: Looks good to me.**\n", "\n", "Ali: Now we can tick off the items in the TO DO list. The function `pairs` is first, and it is easy:" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "button": false, "collapsed": false, "deletable": true, "new_sheet": false, "run_control": { "read_only": false } }, "outputs": [], "source": [ "def pairs(collection): \n", " \"Return all pairs (A, B) from collection where A <= B.\"\n", " return [(A, B) for A in collection for B in collection if A <= B]\n", "\n", "# TODO: all_dice, regular_pair, sums, regular_sums" ] }, { "cell_type": "markdown", "metadata": { "button": false, "deletable": true, "new_sheet": false, "run_control": { "read_only": false } }, "source": [ "**Bo: That's good. We could have used the library function `itertools.combinations_with_replacement`, but let's just leave it as is. We should test to make sure it works:**" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "button": false, "collapsed": false, "deletable": true, "new_sheet": false, "run_control": { "read_only": false } }, "outputs": [ { "data": { "text/plain": [ "[('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'B'), ('B', 'C'), ('C', 'C')]" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "pairs(['A', 'B', 'C'])" ] }, { "cell_type": "markdown", "metadata": { "button": false, "deletable": true, "new_sheet": false, "run_control": { "read_only": false } }, "source": [ "# TO DO: `sums(pair)`\n", "\n", "\n", "Ali: Now for `sums`: we need some way to represent all the 36 possible sums from a pair of dice. We want a representation that will be the same for two different pairs if all 36 sums are the same, but where the order or composition of the sums doesn't matter. \n", "\n", "**Bo: So we want a set of the sums?**\n", "\n", "Ali: Well, it can't be a set, because we need to know that 8 can be made 5 ways, not just that 8 is a member of the set. The technical term for a collection where order doesn't matter but where you can have repeated elements is a **bag**, or sometimes called a [**multiset**](https://en.wikipedia.org/wiki/Multiset). Can you think of a representation for that?\n", "\n", "**Bo: Well the easiest is just a sorted list or tuple—if we don't want order to matter, sorting takes care of that. Another choice would be a dictionary of {sum: count} entries, like {2: 1, 3: 2, ... 11: 2, 12: 1}. There is even a library class, `collections.Counter`, that does exactly that.**\n", "\n", "Ali: How do we choose between the two representations?\n", "\n", "**Bo: I don't think it matters much. Since there are only 36 entries, I think the sorted list will be simpler, and probably more efficient. For 100-sided dice I'd probably go with the Counter.**\n", "\n", "Ali: OK, here's some code for `Bag` and `sums`, and for regular dice:" ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "button": false, "collapsed": false, "deletable": true, "new_sheet": false, "run_control": { "read_only": false } }, "outputs": [], "source": [ "Bag = sorted # Implement a bag as a sorted list\n", "\n", "def sums(pair):\n", " \"All possible sums of a side from one die plus a side from the other.\"\n", " (A, B) = pair\n", " return Bag(a + b for a in A for b in B)\n", "\n", "def ints(start, end): \n", " \"A tuple of the integers from start to end, inclusive.\"\n", " return tuple(range(start, end + 1))\n", "\n", "regular_die = ints(1, 6)\n", "regular_pair = (regular_die, regular_die)\n", "regular_sums = sums(regular_pair)\n", "\n", "# TODO: all_dice" ] }, { "cell_type": "markdown", "metadata": { "button": false, "deletable": true, "new_sheet": false, "run_control": { "read_only": false } }, "source": [ "Ali: Let's check the `regular_sums`:" ] }, { "cell_type": "code", "execution_count": 5, "metadata": { "button": false, "collapsed": false, "deletable": true, "new_sheet": false, "run_control": { "read_only": false } }, "outputs": [ { "data": { "text/plain": [ "36" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "len(regular_sums)" ] }, { "cell_type": "code", "execution_count": 6, "metadata": { "button": false, "collapsed": false, "deletable": true, "new_sheet": false, "run_control": { "read_only": false } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "[2, 3, 3, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 9, 9, 9, 9, 10, 10, 10, 11, 11, 12]\n" ] } ], "source": [ "print(regular_sums)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Bo: And we can see what that would look like to a `Counter`:**" ] }, { "cell_type": "code", "execution_count": 7, "metadata": { "collapsed": false }, "outputs": [ { "data": { "text/plain": [ "Counter({2: 1, 3: 2, 4: 3, 5: 4, 6: 5, 7: 6, 8: 5, 9: 4, 10: 3, 11: 2, 12: 1})" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import collections\n", "\n", "collections.Counter(regular_sums)" ] }, { "cell_type": "markdown", "metadata": { "button": false, "deletable": true, "new_sheet": false, "run_control": { "read_only": false } }, "source": [ "**Bo: Looks good! Now only one more thing on our TODO list:**\n", "\n", "# TO DO: `all_dice()`\n", "\n", "\n", "Ali: `all_dice` should generate all possible dice, where by \"possible\" I mean the dice that could feasibly be part of a pair that is a solution to the Sicherman problem. Do we know how many dice that will be? Is it a large enough number that efficiency will be a problem?\n", "\n", "**Bo: Let's see. A die has six sides. If each side can be a number from, say, 1 to 10, that's 106 or a million possible dice; a million is a small number for a computer.**\n", "\n", "Ali: True, a million is a relatively small number for `all_dice()`, but how many `pairs(all_dice())` will there be?\n", "\n", "**Bo: Ah. A million squared is a trillion. That's a large number even for a computer. Just counting to a trillion takes hours in Python; checking a trillion pairs will take days.**\n", "\n", "Ali: So we need to get rid of most of the dice. What about permutations?\n", "\n", "**Bo: Good point. If I have the die (1, 2, 3, 4, 5, 6), then I don't need the 6! = 720 different permutations of this die— that is, dice like (2, 4, 6, 1, 3, 5).\n", "Each die should be a bag (I learned a new word!) of sides. So we've already eliminated 719/720 = 99.9% of the work.**\n", "\n", "Ali: One other thing bothers me ... how do you know that the sides can range from 1 to 10? Are you sure that 11 can't be part of a solution? Or 12?\n", "\n", "**Bo: Every side on every die must be a positive integer, right?**\n", "\n", "Ali: Right. No zeroes, no negative numbers, no fractions.\n", "\n", "**Bo: Then I know for sure that 12 can't be on any die, because when you add 12 to whatever is on the other die, you would get at least 13, and 13 is not allowed in the regular distribution of sums.**\n", "\n", "Ali: Good. How about 11?\n", "\n", "**Bo: We can't have a sum that is bigger than 12. So if one die had an 11, the other would have to have all 1s. That wouldn't work, because then we'd have six 12s, but we only want one. So no side can have a number bigger than 10.**\n", "\n", "Ali: What else can we say about the biggest number on a die?\n", "\n", "**Bo: There's one 12 in the sums. But there are several ways to make a 12: 6+6 or 7+5 or 8+4, and so on. So I can't say for sure what the biggest number on any one die will be. But I can say that whatever the biggest number on a die is, it will be involved in summing to 12, so there can be only one of them, because we only want to make one 12.**\n", "\n", "Ali: What about the smallest number on a die?\n", "\n", "**Bo: Well, there's only one 2 allowed in the sums. The only way to sum to 2 is 1+1: a 1 from each of the dice in the pair. If a die had no 1s, we wouldn't get a 2; if a die had more than one 1, we would get too many 2s. So every die has to have exactly one 1.**\n", "\n", "Ali: Good. So each die has exactly one 1, and exactly one of whatever the biggest number is, something in the range up to 10. Here's a picture of the six sides of any one die:\n", "\n", " 1 <\n", "2-10 ≤\n", "2-10 ≤\n", "2-10 ≤\n", "2-10 <\n", "2-10\n", "\n", "Ali: The bag of sides is always listed in non-decreasing order; the first side, 1, is less than the next, and the last side, whatever it is, is greater than the one before it.\n", "\n", "**Bo: Wait a minute: you have [2-10] < [2-10]. But 2 is not less than 2, and 10 is not less than 10. I think it should be [2-9] < [3-10]. So the picture should be like this:**\n", "\n", "\n", " 1 <\n", "2-9 ≤\n", "2-9 ≤\n", "2-9 ≤\n", "2-9 <\n", "3-10\n", "\n", "Ali: Good! We're making progress in cutting down the range. But it That this bothers me because it says the range for the biggest number is 3 to 10. But if one die has a 3 and the other a 10, that adds to 13. So I'm thinking that it is not possible to have a 10 after all—because if one die had a 10, then the other would have to have a 2 as the biggest number, and that can't be. Therefore the biggest number is in the range of 3 to 9. But then the others have to be \n", "less, so make them 2 to 8:\n", "\n", " 1 <\n", "2-8 ≤\n", "2-8 ≤\n", "2-8 ≤\n", "2-8 <\n", "3-9\n", "\n", "**Bo: I can turn this picture into code, and we can see how many dice there are:**" ] }, { "cell_type": "code", "execution_count": 8, "metadata": { "button": false, "collapsed": false, "deletable": true, "new_sheet": false, "run_control": { "read_only": false } }, "outputs": [ { "data": { "text/plain": [ "462" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "def all_dice():\n", " \"A list of all feasible 6-sided dice for the Sicherman problem.\"\n", " return [(1, s2, s3, s4, s5, s6)\n", " for s2 in ints(2, 8) \n", " for s3 in ints(s2, 8)\n", " for s4 in ints(s3, 8)\n", " for s5 in ints(s4, 8) \n", " for s6 in ints(s5+1, 9)]\n", "\n", "len(all_dice())" ] }, { "cell_type": "markdown", "metadata": { "button": false, "deletable": true, "new_sheet": false, "run_control": { "read_only": false } }, "source": [ "Ali: Nice! We got down from a million to 462 different dice. I think we're ready to run `sicherman()`. Any bets on what we'll find out?\n", "\n", "**Bo: I bet that Sicherman is remembered because he discovered a pair of dice that works. If he just proved the non-existence of a pair, I don't think that would be noteworthy.**\n", "\n", "Ali: Makes sense. Here goes:\n", "\n", "# The Answer" ] }, { "cell_type": "code", "execution_count": 9, "metadata": { "button": false, "collapsed": false, "deletable": true, "new_sheet": false, "run_control": { "read_only": false } }, "outputs": [ { "data": { "text/plain": [ "{((1, 2, 2, 3, 3, 4), (1, 3, 4, 5, 6, 8))}" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "sicherman()" ] }, { "cell_type": "markdown", "metadata": { "button": false, "deletable": true, "new_sheet": false, "run_control": { "read_only": false } }, "source": [ "**Bo: Look at that!**\n", "\n", "Ali: It turns out you can buy a pair of dice with just these numbers. Here's a table I borrowed from [Wikipedia](https://en.wikipedia.org/wiki/Sicherman_dice) that shows both pairs of dice have the same sums. \n", "\n", "
\n", " | 2 | \n", "3 | \n", "4 | \n", "5 | \n", "6 | \n", "7 | \n", "8 | \n", "9 | \n", "10 | \n", "11 | \n", "12 | \n", "
Regular dice:\n",
" (1, 2, 3, 4, 5, 6)\n", " (1, 2, 3, 4, 5, 6) | \n",
"1+1 | \n", "1+2 \n", "2+1 | \n",
"1+3 \n", "2+2 \n", "3+1 | \n",
"1+4 \n", "2+3 \n", "3+2 \n", "4+1 | \n",
"1+5 \n", "2+4 \n", "3+3 \n", "4+2 \n", "5+1 | \n",
"1+6 \n", "2+5 \n", "3+4 \n", "4+3 \n", "5+2 \n", "6+1 | \n",
"2+6 \n", "3+5 \n", "4+4 \n", "5+3 \n", "6+2 | \n",
"3+6 \n", "4+5 \n", "5+4 \n", "6+3 | \n",
"4+6 \n", "5+5 \n", "6+4 | \n",
"5+6 \n", "6+5 | \n",
"6+6 | \n", "
Sicherman dice:\n",
" (1, 2, 2, 3, 3, 4)\n", " (1, 3, 4, 5, 6, 8) | \n",
"1+1 | \n", "2+1 \n", "2+1 | \n",
"3+1 \n", "3+1 \n", "1+3 | \n",
"1+4 \n", "2+3 \n", "2+3 \n", "4+1 | \n",
"1+5 \n", "2+4 \n", "2+4 \n", "3+3 \n", "3+3 | \n",
"1+6 \n", "2+5 \n", "2+5 \n", "3+4 \n", "3+4 \n", "4+3 | \n",
"2+6 \n", "2+6 \n", "3+5 \n", "3+5 \n", "4+4 | \n",
"1+8 \n", "3+6 \n", "3+6 \n", "4+5 | \n",
"2+8 \n", "2+8 \n", "4+6 | \n",
"3+8 \n", "3+8 | \n",
"4+8 | \n", "