From 43ff71b9062941e8b07513d00832aa2a2792cec3 Mon Sep 17 00:00:00 2001 From: Peter Norvig Date: Tue, 24 Jan 2023 09:07:01 -0800 Subject: [PATCH] Add files via upload --- ipynb/Sicherman Dice.ipynb | 2128 +++++++++++++++--------------------- 1 file changed, 876 insertions(+), 1252 deletions(-) diff --git a/ipynb/Sicherman Dice.ipynb b/ipynb/Sicherman Dice.ipynb index e8367d7..ee62721 100644 --- a/ipynb/Sicherman Dice.ipynb +++ b/ipynb/Sicherman Dice.ipynb @@ -11,21 +11,23 @@ } }, "source": [ + "
Peter Norvig
2014, 2023
\n", + "\n", "# 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", + "> *Note: This notebook takes the form of a conversation/collaboration between two problem solvers, *Ali* and **Bo**.*\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", + "Ali: Huh. [**This**](http://wordplay.blogs.nytimes.com/2014/06/16/dice-3) is interesting. You know how, in a board game like Monopoly, you roll two dice, add them up, and then move that number of spaces? \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", + "Ali: And a sum like 2 can only be made one way, 1 + 1, but 9 can be made multiple ways. \n", "\n", - "**Bo: Yeah. 8 can be made 5 ways, so it has a 5/36 probability of occuring.**\n", + "**Bo: Yeah. 9 can be made 4 ways: 3 + 6, 4 + 5, 5 + 4, or 6 + 3.**\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", + "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 \"standard\" dice (standard dice have [1, 2, 3, 4, 5, 6] on their six sides) but have the same **distribution of sums** as a standard pair–so the pair of Sicherman dice would also have one way of making a 2 and four ways of making 9, but it could be different ways; maybe 8+1 could be one way. Like standard dice, each of the sides on a Sicherman die is a positive integer, but some of them could be greater than 6.\n", "\n", - "**Bo: And what did he find?**\n", + "**Bo: And what did Sicherman find?**\n", "\n", "Ali: Wouldn't it be more fun to figure it out for ourselves?\n", "\n", @@ -33,15 +35,15 @@ "\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", + "**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 and check 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", + "- **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, 2, 2, 4, 8, 9], [1, 3, 4, 4, 5, 8])**\n", + "- **Check each pair to see if it has the same distribution of sums as the standard pair (but does not use standard dice).**\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:" + "Ali: Good. First I'll write down some data types:" ] }, { @@ -51,6 +53,9 @@ "button": false, "collapsed": false, "deletable": true, + "jupyter": { + "outputs_hidden": false + }, "new_sheet": false, "run_control": { "read_only": false @@ -58,30 +63,18 @@ }, "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", + "from typing import *\n", "\n", - "# TODO: pairs, all_dice, regular_pair, sums, regular_sums" + "Side = int # Type for a side of a die: a positive integer, e.g. 3\n", + "Die = list # Type for a die: a list of numbers on sides, e.g. [1, 2, 2, 4, 8, 9]\n", + "Pair = tuple # Type for a pair of dice, e.g. ([1, 3, 4, 4, 5, 8], [1, 2, 2, 4, 8, 9])" ] }, { "cell_type": "markdown", - "metadata": { - "button": false, - "deletable": true, - "new_sheet": false, - "run_control": { - "read_only": false - } - }, + "metadata": {}, "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:" + "Ali: Now I can code up your description almost verbatim. I'll also keep track of our TO DO list:" ] }, { @@ -91,6 +84,9 @@ "button": false, "collapsed": false, "deletable": true, + "jupyter": { + "outputs_hidden": false + }, "new_sheet": false, "run_control": { "read_only": false @@ -98,11 +94,18 @@ }, "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", + "def sicherman() -> List[Pair]:\n", + " \"\"\"A list of nonstandard pairs of dice that have the same\n", + " distribution of sums as a standard pair of dice.\"\"\"\n", + " return [pair for pair in all_pairs(all_dice())\n", + " if distribution_of_sums(pair) == standard_sums\n", + " and pair != standard_pair]\n", "\n", - "# TODO: all_dice, regular_pair, sums, regular_sums" + "def all_pairs(dice) -> List[Pair]: \n", + " \"\"\"Return all dice pairs (A, B) where A <= B (so, not also (B, A)).\"\"\"\n", + " return [(A, B) for A in dice for B in dice if A <= B]\n", + "\n", + "# TODO: all_dice(), distribution_of_sums(pair), standard_sums, standard_pair" ] }, { @@ -116,7 +119,7 @@ } }, "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:**" + "**Bo: Looks good to me. We should test `all_pairs` to make sure it works:**" ] }, { @@ -126,6 +129,9 @@ "button": false, "collapsed": false, "deletable": true, + "jupyter": { + "outputs_hidden": false + }, "new_sheet": false, "run_control": { "read_only": false @@ -135,7 +141,7 @@ { "data": { "text/plain": [ - "[('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'B'), ('B', 'C'), ('C', 'C')]" + "[('a', 'a'), ('a', 'b'), ('a', 'c'), ('b', 'b'), ('b', 'c'), ('c', 'c')]" ] }, "execution_count": 3, @@ -144,7 +150,7 @@ } ], "source": [ - "pairs(['A', 'B', 'C'])" + "all_pairs(['a', 'b', 'c'])" ] }, { @@ -158,22 +164,20 @@ } }, "source": [ - "# TO DO: `sums(pair)`\n", + "# Distribution of Sums\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", + "Ali: Now for `distribution_of_sums(pair)`: 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 of the sums doesn't matter.\n", "\n", - "**Bo: So we want a set of the sums?**\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", + "Ali: Well, it can't be a *set*, because we need to know that, say, a 9 is made four times in the pair's distribution of sums, not just that 9 is a member of the set of sums. The technical term for a collection where order doesn't matter but where you can have repeated elements is a [**multiset**](https://en.wikipedia.org/wiki/Multiset) or **bag**. What's a good 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", + "**Bo: Well if we don't want order to matter, the easiest representation is just a sorted list.**\n", "\n", - "Ali: How do we choose between the two representations?\n", + "Ali: OK, so `Bag` is just the function `sorted` and here's some code for `distribution_of_sums`, and for \"standard\" dice. \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:" + "Dice start at one, not zero, so I can't use `range(6)` for the possible sides of a die; instead I decided to define `ints` and use `ints(1, 6)`." ] }, { @@ -183,6 +187,9 @@ "button": false, "collapsed": false, "deletable": true, + "jupyter": { + "outputs_hidden": false + }, "new_sheet": false, "run_control": { "read_only": false @@ -192,20 +199,20 @@ "source": [ "Bag = sorted # Implement a bag as a sorted list\n", "\n", - "def sums(pair):\n", + "def distribution_of_sums(pair) -> List[int]:\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", + "def ints(start, end) -> range: \n", + " \"The integers from start to end, inclusive.\"\n", + " return 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", + "standard_die = Die(ints(1, 6))\n", + "standard_pair = (standard_die, standard_die)\n", + "standard_sums = distribution_of_sums(standard_pair)\n", "\n", - "# TODO: all_dice" + "# TODO: all_dice()" ] }, { @@ -219,7 +226,7 @@ } }, "source": [ - "Ali: Let's check the `regular_sums`:" + "**Bo: Let's check the `standard_sums`:**" ] }, { @@ -229,34 +236,9 @@ "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, + "jupyter": { + "outputs_hidden": false + }, "new_sheet": false, "run_control": { "read_only": false @@ -272,27 +254,94 @@ } ], "source": [ - "print(regular_sums)" + "assert len(standard_sums) == 36\n", + "\n", + "print(standard_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 TO DO list:**\n", + "\n", + "# All Possible 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 big 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 small number. But how many pairs of dice will there be?\n", + "\n", + "**Bo: Ah. That's right. A million squared is a trillion. That's a big number even for a computer. An empty loop that just counts to a million takes milliseconds in Python, but counting to a *trillion* takes hours. Checking a trillion pairs will take days.**\n", + "\n", + "Ali: So we really need to pare down the number of possible dice. What about permutations?\n", + "\n", + "**Bo: Good point! If I have the die [1, 2, 3, 4, 5, 7], then I don't need the different permutations–that is, dice like [2, 4, 7, 1, 3, 5]. We can arrange to only generate dice whose sides are in sorted order.**\n", + "\n", + "Ali: Any other constraints on the sides?\n", + "\n", + "**Bo: Here's something: the distribution of sums for a standard pair has exactly one 2. The only way to get a sum of 2 from two positive integers is 1+1. So that means that each die must have exactly one 1; no more, no less.**\n", + "\n", + "Ali: You said the sides can range from 1 to 10. Are you *sure* that 11 can't be part of a legal solution? \n", + "\n", + "\n", + "**Bo: I was just guessing, but now I can see that it *is* true. The distribution of sums for a pair has a 12, and nothing higher. Assume one die had an 11 on some side. We know the other die can only have one 1, so it must have some sides that are greater than 1, so the pair would make some sums that are greater than 12. That's wrong, so that means the assumption must be wrong: it can't be true that a die has an 11.**\n", + "\n", + "Ali: So all together the smallest number must be 1, the next smallest is at least 2, they're all 10 or less, and we'll make sure the numbers are in non-decreasing order to avoid listing two permutations of the same die. We can code that up:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "button": false, + "collapsed": false, + "deletable": true, + "jupyter": { + "outputs_hidden": false + }, + "new_sheet": false, + "run_control": { + "read_only": false + } + }, + "outputs": [], + "source": [ + "def all_dice() -> List[Die]:\n", + " \"\"\"A list of all feasible 6-sided dice for the Sicherman problem.\"\"\"\n", + " return [[1, b, c, d, e, f]\n", + " for b in ints(2, 10) \n", + " for c in ints(b, 10)\n", + " for d in ints(c, 10)\n", + " for e in ints(d, 10) \n", + " for f in ints(e, 10)]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "**Bo: And we can see what that would look like to a `Counter`:**" + "Ali: Let's see how many dice there are:" ] }, { "cell_type": "code", "execution_count": 7, - "metadata": { - "collapsed": false - }, + "metadata": {}, "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})" + "1287" ] }, "execution_count": 7, @@ -301,111 +350,37 @@ } ], "source": [ - "import collections\n", - "\n", - "collections.Counter(regular_sums)" + "len(all_dice())" ] }, { "cell_type": "markdown", - "metadata": { - "button": false, - "deletable": true, - "new_sheet": false, - "run_control": { - "read_only": false - } - }, + "metadata": {}, "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:**" + "Ali: Nice! We got down from a million to 1287 different dice, which means the number of pairs is reduced from a trillion to under a million! I don't want to look at all 1287 of them, but here's every hundreth die:" ] }, { "cell_type": "code", "execution_count": 8, - "metadata": { - "button": false, - "collapsed": false, - "deletable": true, - "new_sheet": false, - "run_control": { - "read_only": false - } - }, + "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "462" + "[[1, 2, 2, 2, 2, 2],\n", + " [1, 2, 2, 4, 7, 8],\n", + " [1, 2, 3, 3, 10, 10],\n", + " [1, 2, 4, 4, 6, 8],\n", + " [1, 2, 5, 6, 8, 9],\n", + " [1, 3, 3, 3, 3, 8],\n", + " [1, 3, 3, 7, 8, 9],\n", + " [1, 3, 5, 5, 5, 6],\n", + " [1, 3, 7, 8, 8, 8],\n", + " [1, 4, 4, 8, 8, 9],\n", + " [1, 4, 7, 7, 7, 7],\n", + " [1, 5, 6, 6, 8, 8],\n", + " [1, 6, 7, 7, 8, 8]]" ] }, "execution_count": 8, @@ -414,16 +389,7 @@ } ], "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())" + "all_dice()[::100]" ] }, { @@ -437,9 +403,9 @@ } }, "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", + "Ali: 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", + "**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 there would be an article about him.**\n", "\n", "Ali: Makes sense. Here goes:\n", "\n", @@ -453,6 +419,9 @@ "button": false, "collapsed": false, "deletable": true, + "jupyter": { + "outputs_hidden": false + }, "new_sheet": false, "run_control": { "read_only": false @@ -462,7 +431,7 @@ { "data": { "text/plain": [ - "{((1, 2, 2, 3, 3, 4), (1, 3, 4, 5, 6, 8))}" + "[([1, 2, 2, 3, 3, 4], [1, 3, 4, 5, 6, 8])]" ] }, "execution_count": 9, @@ -487,111 +456,17 @@ "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", + "Ali: It turns out you can buy a pair of dice with just these numbers, and there are some nice [charts and tables on Wikipedia](https://en.wikipedia.org/wiki/Sicherman_dice) explaining it all. \n", "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
23456789101112
Regular dice:\n", - "
(1, 2, 3, 4, 5, 6)\n", - "
(1, 2, 3, 4, 5, 6)
1+11+2
\n", - "2+1
1+3
\n", - "2+2
\n", - "3+1
1+4
\n", - "2+3
\n", - "3+2
\n", - "4+1
1+5
\n", - "2+4
\n", - "3+3
\n", - "4+2
\n", - "5+1
1+6
\n", - "2+5
\n", - "3+4
\n", - "4+3
\n", - "5+2
\n", - "6+1
2+6
\n", - "3+5
\n", - "4+4
\n", - "5+3
\n", - "6+2
3+6
\n", - "4+5
\n", - "5+4
\n", - "6+3
4+6
\n", - "5+5
\n", - "6+4
5+6
\n", - "6+5
6+6
Sicherman dice:\n", - "
(1, 2, 2, 3, 3, 4)\n", - "
(1, 3, 4, 5, 6, 8)
1+12+1
\n", - "2+1
3+1
\n", - "3+1
\n", - "1+3
1+4
\n", - "2+3
\n", - "2+3
\n", - "4+1
1+5
\n", - "2+4
\n", - "2+4
\n", - "3+3
\n", - "3+3
1+6
\n", - "2+5
\n", - "2+5
\n", - "3+4
\n", - "3+4
\n", - "4+3
2+6
\n", - "2+6
\n", - "3+5
\n", - "3+5
\n", - "4+4
1+8
\n", - "3+6
\n", - "3+6
\n", - "4+5
2+8
\n", - "2+8
\n", - "4+6
3+8
\n", - "3+8
4+8
\n", + "![Sicherman dice from MathArtFun.com](http://www.mathartfun.com/shopsite_sc/store/html/SichermanDots.jpg)\n", "\n", "\n", - "Ali: We could stop here. Or we could try to solve it for *N*-sided dice.\n", "\n", - "# Why stop now? Onward!" + "Ali: We could stop here. Or ... what if we looked at other-shaped dice? \n", + "\n", + "# *N*-Sided Dice\n", + "\n", + "![A set of isohedral dice](https://digitalambler.files.wordpress.com/2013/05/dice.jpg)" ] }, { @@ -605,27 +480,9 @@ } }, "source": [ - "Ali: OK. I know 4-, 12-, and 20-sided dice are common, but we'll try to handle any *N* > 1. My guess is we won't go too far before our program becomes too slow. So, before we try *N*-sided dice, let's analyze six-sided dice a little better, to see if we can eliminate some of the pairs before we start. The picture says that (1, 2, 2, 2, 2, 3) could be a valid die. Could it?\n", + "Ali: I know 4-, 8–, 12-, and 20-sided dice are common. Let's try to handle any *N*-sided dice. \n", "\n", - "**Bo: No! If a die had four 2s, then we know that since the other die has one 1, we could make 2 + 1 = 3 four ways. But the `regular_sums` has only two 3s. So that means that a die can have no more than two 2s. New picture:**\n", - "\n", - " 1  <\n", - "2-8 ≤\n", - "2-8 ≤\n", - "3-8 ≤\n", - "3-8 <\n", - "3-9\n", - "\n", - "Ali: Now we've got [3-8] < [3-9]; that's not right. If a die can only have one 1 and two 2s, then it must have at least one number that is a 3 or more, followed by the biggest number, which must be 4 or more, and we know a pair of biggest numbers must sum to 12, so the range of the biggest can't be [4-9], it must be [4-8]:\n", - "\n", - " 1  <\n", - "2-7 ≤\n", - "2-7 ≤\n", - "3-7 ≤\n", - "3-7 <\n", - "4-8\n", - "\n", - "We can update the code and count again:" + "**Bo: Well, the number of dice pairs is going to be *O*(*N*2*N*), because when you add an *N*-th side to a die it can have (almost) *N* possible values, and then the number of pairs is proportional to the square of the number of dice. So my guess is we won't get up to 20 before our program becomes too slow. But we can go about parameterizing `sicherman` and the other pieces for now, and worry about efficiency later:**" ] }, { @@ -635,114 +492,9 @@ "button": false, "collapsed": false, "deletable": true, - "new_sheet": false, - "run_control": { - "read_only": false - } - }, - "outputs": [ - { - "data": { - "text/plain": [ - "231" - ] - }, - "execution_count": 10, - "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, 7) \n", - " for s3 in ints(s2, 7)\n", - " for s4 in ints(max(s3, 3), 7)\n", - " for s5 in ints(s4, 7) \n", - " for s6 in ints(s5+1, 8)]\n", - "\n", - "len(all_dice())" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "button": false, - "deletable": true, - "new_sheet": false, - "run_control": { - "read_only": false - } - }, - "source": [ - "**Bo: Nice—we cut the number of dice in half. I don't want to print `all_dice()`, but I can sample a few:**" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": { - "button": false, - "collapsed": false, - "deletable": true, - "new_sheet": false, - "run_control": { - "read_only": false - } - }, - "outputs": [ - { - "data": { - "text/plain": [ - "[(1, 2, 3, 3, 5, 7),\n", - " (1, 2, 2, 5, 6, 7),\n", - " (1, 4, 4, 5, 7, 8),\n", - " (1, 3, 3, 6, 6, 8),\n", - " (1, 4, 4, 4, 4, 5),\n", - " (1, 4, 5, 5, 7, 8),\n", - " (1, 2, 3, 6, 6, 8),\n", - " (1, 2, 2, 3, 6, 8),\n", - " (1, 3, 4, 5, 7, 8),\n", - " (1, 6, 6, 6, 6, 7)]" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "import random\n", - "\n", - "random.sample(all_dice(), 10)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "button": false, - "deletable": true, - "new_sheet": false, - "run_control": { - "read_only": false - } - }, - "source": [ - "# `sicherman(N)`\n", - "\n", - "Ali: OK, I think we're ready to update `sicherman()` to `sicherman(N)`. \n", - "\n", - "**Bo: Sure, most of that will be easy, just parameterizing with `N`:**" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": { - "button": false, - "collapsed": false, - "deletable": true, + "jupyter": { + "outputs_hidden": false + }, "new_sheet": false, "run_control": { "read_only": false @@ -750,22 +502,146 @@ }, "outputs": [], "source": [ - "def sicherman(N=6):\n", - " \"\"\"The set of pairs of N-sided dice that have the same\n", - " distribution of sums as a regular pair of N-sided dice.\"\"\"\n", - " reg_sums = regular_sums(N)\n", - " reg_pair = regular_pair(N)\n", - " return {pair for pair in pairs(all_dice(N))\n", - " if pair != reg_pair\n", - " and sums(pair) == reg_sums}\n", + "def sicherman(N=6) -> List[Pair]:\n", + " \"\"\"A list of pairs of N-sided dice that have the same\n", + " distribution of sums as a standard pair of N-sided dice.\"\"\"\n", + " standard_pair = nth_standard_pair(N)\n", + " standard_sums = distribution_of_sums(standard_pair)\n", + " return [pair for pair in all_pairs(all_dice(N))\n", + " if distribution_of_sums(pair) == standard_sums\n", + " and pair != standard_pair]\n", "\n", - "def regular_die(N): return ints(1, N)\n", - "def regular_pair(N): return (regular_die(N), regular_die(N))\n", - "def regular_sums(N): return sums(regular_pair(N))\n", + "def nth_standard_pair(N) -> Die: return (Die(ints(1, N)), Die(ints(1, N)))\n", "\n", "# TODO: all_dice(N)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Ali: Now, for `all_dice(N)`, I think most of the analysis from `N=6` still holds: \n", + "- The first side must be 1.\n", + "- Subsequent sides must be at least 2.\n", + "- No side can be more than `2 * N - 2`\n", + "- To avoid permutations, the sides must be in sorted order.\n", + "\n", + "For the old version of `all_dice()`, we had five nested loops for the five sides after the first. But the number of nested loops in a program is detemined at compile time; I can't write a program that has `N - 1` nested loops. We'll have to find some other implementation technique.\n", + "\n", + "**Bo: Here's an iterative approach: we keep track of a list of partially-formed dice, and on each iteration, we add a side to all the partially-formed dice in all possible ways, until the dice all have `N` sides. Our list starts with just one partially-formed die, with a single side containing a 1:**\n", + "\n", + " dice = [ [1] ]\n", + " \n", + "**Suppose `N = 4`. Then the biggest number on any side is 6, and we get these partially-formed dice with 2 sides:**\n", + "\n", + " dice = [[1, 2], [1, 3], [1, 4], [1, 5], [1, 6]\n", + " \n", + "**To extend to 3 sides we would add a number to each of those five in all possible ways, giving:**\n", + "\n", + " dice = [[1, 2, 2], [1, 2, 3], [1, 2, 4], [1, 2, 5], [1, 2, 6],\n", + " [1, 3, 3], [1, 3, 4], [1, 3, 5], [1, 3, 6], \n", + " [1, 4, 4], [1, 4, 5], [1, 4, 6],\n", + " [1, 5, 5], [1, 5, 6],\n", + " [1, 5, 6]]\n", + " \n", + "**The final iteration would add one more side to each of these. Here's how the code looks:**\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "def all_dice(N=6) -> List[Die]:\n", + " \"\"\"All feasible Sicherman dice with N sides.\"\"\"\n", + " dice = [ [1] ] # The first side must be 1\n", + " for _ in range(N - 1): # Add N - 1 sides, each nondecreasing, between 2 and 2N-2\n", + " dice = [die + [side] \n", + " for die in dice \n", + " for side in ints(max(2, die[-1]), 2 * N - 2)]\n", + " return dice\n", + "\n", + "assert len(all_dice(6)) == 1287 # Make sure this hasn't changed" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Ali: Let's try `sicherman(N)` for some small values of `N`.\n", + "\n", + "# Results up to *N* = 6" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 2.44 s, sys: 21.3 ms, total: 2.46 s\n", + "Wall time: 2.46 s\n" + ] + }, + { + "data": { + "text/plain": [ + "{2: [],\n", + " 3: [],\n", + " 4: [([1, 2, 2, 3], [1, 3, 3, 5])],\n", + " 5: [],\n", + " 6: [([1, 2, 2, 3, 3, 4], [1, 3, 4, 5, 6, 8])]}" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time {N: sicherman(N) for N in ints(2, 6)}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Bo: It is certainly reassuring that we get the same result for `sicherman(6)` as with the old version of `sicherman()`.** \n", + "\n", + "Ali: And comforting that the run time is about the same.\n", + "\n", + "**Bo: And we learned something new: there is a result for `sicherman(4)` but not for 2, 3, and 5. That's interesting!** \n", + "\n", + "Ali: Maybe there can be no solution for primes? How much farther can we go beyond `N = 6`?\n", + "\n", + "**Bo: Let's count how many possible dice there are for various values of `N`:**" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{2: 1, 3: 6, 4: 35, 5: 210, 6: 1287, 7: 8008, 8: 50388, 9: 319770}" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "{N: len(all_dice(N)) for N in ints(2, 9)}" + ] + }, { "cell_type": "markdown", "metadata": { @@ -777,16 +653,25 @@ } }, "source": [ - "Ali: Good. I think it would be helpful to look at a table of `regular_sums`:" + "Ali: That doesn't look encouraging. With run time proportional to the square of the number of dice, it looks like it will be on the order of a minute for `N=7`, an hour for `N=8`, and days for `N=9`. \n", + "\n", + "# Considering Fewer Dice\n", + "\n", + "Ali: We still need to pare down the number of dice! Can we tighten up the constraints on sides? \n", + "\n", + "I think it would be helpful to me to have a look at a table of distributions of sums:" ] }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 14, "metadata": { "button": false, "collapsed": false, "deletable": true, + "jupyter": { + "outputs_hidden": false + }, "new_sheet": false, "run_control": { "read_only": false @@ -797,19 +682,21 @@ "name": "stdout", "output_type": "stream", "text": [ - "N: 1 {2: 1}\n", - "N: 2 {2: 1, 3: 2, 4: 1}\n", - "N: 3 {2: 1, 3: 2, 4: 3, 5: 2, 6: 1}\n", - "N: 4 {2: 1, 3: 2, 4: 3, 5: 4, 6: 3, 7: 2, 8: 1}\n", - "N: 5 {2: 1, 3: 2, 4: 3, 5: 4, 6: 5, 7: 4, 8: 3, 9: 2, 10: 1}\n", - "N: 6 {2: 1, 3: 2, 4: 3, 5: 4, 6: 5, 7: 6, 8: 5, 9: 4, 10: 3, 11: 2, 12: 1}\n", - "N: 7 {2: 1, 3: 2, 4: 3, 5: 4, 6: 5, 7: 6, 8: 7, 9: 6, 10: 5, 11: 4, 12: 3, 13: 2, 14: 1}\n" + "N = 2: {2: 1, 3: 2, 4: 1}\n", + "N = 3: {2: 1, 3: 2, 4: 3, 5: 2, 6: 1}\n", + "N = 4: {2: 1, 3: 2, 4: 3, 5: 4, 6: 3, 7: 2, 8: 1}\n", + "N = 5: {2: 1, 3: 2, 4: 3, 5: 4, 6: 5, 7: 4, 8: 3, 9: 2, 10: 1}\n", + "N = 6: {2: 1, 3: 2, 4: 3, 5: 4, 6: 5, 7: 6, 8: 5, 9: 4, 10: 3, 11: 2, 12: 1}\n", + "N = 7: {2: 1, 3: 2, 4: 3, 5: 4, 6: 5, 7: 6, 8: 7, 9: 6, 10: 5, 11: 4, 12: 3, 13: 2, 14: 1}\n" ] } ], "source": [ - "for N in ints(1, 7):\n", - " print(\"N:\", N, dict(collections.Counter(regular_sums(N))))" + "from collections import Counter\n", + "\n", + "for N in ints(2, 7):\n", + " ctr = Counter(distribution_of_sums(nth_standard_pair(N)))\n", + " print(f\"N = {N}: {dict(ctr)}\")" ] }, { @@ -823,90 +710,70 @@ } }, "source": [ - "**Bo: That is helpful. I can see that any `regular_sums` must have one 2 and two 3s, and three 4s, and so on, not just for `N=6` but for any `N` (except for trivially small `N`). And that means that any regular die can have at most two 2s, three 3s, four 4s, and so on. So we have this picture:**\n", - "\n", - " 1  <\n", - "2+ ≤\n", - "2+ ≤\n", - "3+ ≤\n", - "3+ ≤\n", - "3+ ≤\n", - "4+ ≤\n", - "4+ ≤\n", - "4+ ≤\n", - "4+ ≤ ...\n", - "\n", - "**where [2+] means the lower bound is 2, but we haven't figured out yet what the upper bound is.**\n", - "\n", - "Ali: Let's figure out upper bounds starting from the biggest number. What can the biggest number be?\n", - "\n", - "**Bo: For a pair of *N*-sided die, the biggest sides from each one must add up to 2*N*. Let's take *N*=10 as an example. The biggest numbers on two 10-sided Sicherman dice must sum to 20. According to the picture above, the lower bound on the biggest number would be 4, but because there can only be one of the biggest number, the lower bound is 5. So to add up to 20, the range must be [5-15]:**\n", - "\n", - "\n", - " 1  <\n", - "2+ ≤\n", - "2+ ≤\n", - "3+ ≤\n", - "3+ ≤\n", - "3+ ≤\n", - "4+ ≤\n", - "4+ ≤\n", - "4+ <\n", - "5-15 \n", - "\n", - "Ali: There's probably some tricky argument for the upper bounds of the other sides, but I'm just going to say the upper bound is one less than the upper bound of the biggest number:\n", - "\n", - " 1  <\n", - "2-14 ≤\n", - "2-14 ≤\n", - "3-14 ≤\n", - "3-14 ≤\n", - "3-14 ≤\n", - "4-14 ≤\n", - "4-14 ≤\n", - "4-14 <\n", - "5-15 \n", - "\n", - "\n", - "Let's start by coding up `lower_bounds(N)`:" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": { - "button": false, - "collapsed": true, - "deletable": true, - "new_sheet": false, - "run_control": { - "read_only": false - } - }, - "outputs": [], - "source": [ - "def lower_bounds(N):\n", - " \"A list of lower bounds for respective sides of an N-sided die.\"\n", - " lowers = [1]\n", - " for _ in range(N-1):\n", - " m = lowers[-1] # The last number in lowers so far\n", - " lowers.append(m if (lowers.count(m) < m) else m + 1)\n", - " lowers[-1] = lowers[-2] + 1\n", - " return lowers" + "**Bo: You're right! That is helpful! I'm reminded of the very first die generated by the first version of `all_dice()`:**" ] }, { "cell_type": "code", "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[1, 2, 2, 2, 2, 2]" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "all_dice()[0]" + ] + }, + { + "cell_type": "markdown", "metadata": { "button": false, - "collapsed": false, "deletable": true, "new_sheet": false, "run_control": { "read_only": false } }, + "source": [ + "**Bo: I had a feeling that was too many twos, and could never be part of a solution. I can see now that every `nth_standard_sums` distribution has at most one 2, two 3s, three 4s, and so on. How does the distribution relate to the dice? We know every die has a 1. So a die with five 2s, when paired with any other die, would make six 3s. That's too many; we can only have two 3s in a distribution. That means that a die can never have more two 2s, three 3s, four 4s, and so on. It all works becuase we know the other die has to have a 1.**\n", + "\n", + "**I'm going to define a function `lower_bounds(N)` which will produce a list of integers that are the lower bounds for each side of an `N`-sided die. Previously, our lower bounds for a 6-sided die were implicitly specified as [1, 2, 2, 2, 2, 2], but we now know that's not right: we don't want to allow a die with five 2s.**\n", + "\n", + "Ali: Before you do that, I thought of something else: the first side is special in that there can only be one copy of it; otherwise we'd get two 2s, and we only want one 2. The last side should be similarly special. We don't know what the last value will be, but we do know that we only want one of them, because if there were more than one, then we'd get more than one of the biggest sum in the distribution of sums, and we know we only want one.\n", + "\n", + "**Bo: Very good. That means the lower bound of the last side must be one more than the lower bound of the penultimate side. Here we go:**" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "def lower_bounds(N):\n", + " \"A list of lower bounds for respective sides of an N-sided die.\"\n", + " bounds = [1] # First side is 1\n", + " while len(bounds) < N:\n", + " s = bounds[-1] + 1\n", + " bounds.extend(s * [s]) # Allow two 2s, three 3s, etc.\n", + " bounds = bounds[:N] # If we added too many s's, drop the extras\n", + " bounds[-1] = bounds[-2] + 1 # Last side is one more than second-to-last\n", + " return bounds" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, "outputs": [ { "data": { @@ -914,7 +781,7 @@ "[1, 2, 2, 3, 3, 4]" ] }, - "execution_count": 15, + "execution_count": 17, "metadata": {}, "output_type": "execute_result" } @@ -925,16 +792,8 @@ }, { "cell_type": "code", - "execution_count": 16, - "metadata": { - "button": false, - "collapsed": false, - "deletable": true, - "new_sheet": false, - "run_control": { - "read_only": false - } - }, + "execution_count": 18, + "metadata": {}, "outputs": [ { "data": { @@ -942,7 +801,7 @@ "[1, 2, 2, 3, 3, 3, 4, 4, 4, 5]" ] }, - "execution_count": 16, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } @@ -953,278 +812,121 @@ }, { "cell_type": "markdown", - "metadata": { - "button": false, - "deletable": true, - "new_sheet": false, - "run_control": { - "read_only": false - } - }, + "metadata": {}, "source": [ - "Ali: And `upper_bounds(N)`:" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": { - "button": false, - "collapsed": true, - "deletable": true, - "new_sheet": false, - "run_control": { - "read_only": false - } - }, - "outputs": [], - "source": [ - "def upper_bounds(N):\n", - " \"A list of upper bounds for respective sides of an N-sided die.\"\n", - " U = 2 * N - lower_bounds(N)[-1]\n", - " return [1] + (N - 2) * [U - 1] + [U]" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": { - "button": false, - "collapsed": false, - "deletable": true, - "new_sheet": false, - "run_control": { - "read_only": false - } - }, - "outputs": [ - { - "data": { - "text/plain": [ - "[1, 7, 7, 7, 7, 8]" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "upper_bounds(6)" + "Ali: Yeah, that looks right.\n", + "\n", + "**Bo: And now for the upper bounds. For, say, *N*=10, the biggest sum in the distribution of sums is 20. One of the lower bounds for *N*=10 is 5, meaning that every 10-sided die will have a 5 or higher. That implies that 15 is an upper bound for every side: no 10-sided die can have a number more than 15, because otherwise there would be a sum over 20.**\n", + "\n", + "Ali: So 15 is the upper bound for the final side (and 1 is the upper bound for the first side). And as I just said, we know that whatever the biggest number is, there can only be one of them, so the upper bound for the penultimate number is one less than the final number. What else do we know?\n", + "\n", + "**Bo: It gets complicated. It was easier with the lower bounds, because we know every die has a 1. We don't know exactly what the biggest number will be on a die. So I'm happy letting all the upper bounds between the first and last sides be one less than the last:**" ] }, { "cell_type": "code", "execution_count": 19, - "metadata": { - "button": false, - "collapsed": false, - "deletable": true, - "new_sheet": false, - "run_control": { - "read_only": false - } - }, - "outputs": [ - { - "data": { - "text/plain": [ - "[1, 14, 14, 14, 14, 14, 14, 14, 14, 15]" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], + "metadata": {}, + "outputs": [], "source": [ - "upper_bounds(10)" + "def upper_bounds(N):\n", + " \"A list of upper bounds for respective sides of an N-sided die.\"\n", + " biggest = 2 * N - lower_bounds(N)[-1]\n", + " middle = (N - 2) * [biggest - 1]\n", + " return [1, *middle, biggest]" ] }, { "cell_type": "markdown", - "metadata": { - "button": false, - "deletable": true, - "new_sheet": false, - "run_control": { - "read_only": false - } - }, + "metadata": {}, "source": [ - "Ali: Now, what do we have to do for `all_dice(N)`? When we knew we had six sides, we wrote six nested loops. We can't do that for *N* sides, so what do we do?\n", - "\n", - "**Bo: Here's an iterative approach: we keep track of a list of partially-formed dice, and on each iteration, we add a side to all the partially-formed dice in all possible ways, until the dice all have `N` sides. So for eaxmple, we'd start with:**\n", - "\n", - " dice = [(1,)]\n", - " \n", - "**and then on the next iteration (let's assume *N*=6, so the lower bound is 2 and the upper bound is 7), we'd get this:**\n", - "\n", - " dice = [(1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7)]\n", - " \n", - "**on the next iteration, we find all the ways of adding a third side, and so on. Like this:**" + "**Bo: I'll make a little thing to display the bounds:**" ] }, { "cell_type": "code", "execution_count": 20, - "metadata": { - "button": false, - "collapsed": false, - "deletable": true, - "new_sheet": false, - "run_control": { - "read_only": false - } - }, + "metadata": {}, "outputs": [], "source": [ - "def all_dice(N):\n", - " \"Return a list of all possible N-sided dice for the Sicherman problem.\"\n", - " lowers = lower_bounds(N)\n", - " uppers = upper_bounds(N)\n", - " def possibles(die, i):\n", - " \"The possible numbers for the ith side of an N-sided die.\"\n", - " return ints(max(lowers[i], die[-1] + int(i == N-1)),\n", - " uppers[i])\n", - " dice = [(1,)]\n", - " for i in range(1, N):\n", - " dice = [die + (side,)\n", - " for die in dice\n", - " for side in possibles(die, i)]\n", - " return dice" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "button": false, - "deletable": true, - "new_sheet": false, - "run_control": { - "read_only": false - } - }, - "source": [ - "**Bo: The tricky part was with the `max`: the actual lower bound at least `lowers[i]`, but it must be as big as the previous side, `die[-1]`. And just to make things complicated, the very last side has to be strictly bigger than the previous; `\" + int(i == N-1)\"` does that by adding 1 just in case we're on the last side, and 0 otherwise.**\n", - "\n", - "**Let's check it out:**" + "def bounds(N): \n", + " return f'N = {N:2}: ' + ', '.join(f'{L}-{U}' for L, U in zip(lower_bounds(N), upper_bounds(N)))" ] }, { "cell_type": "code", "execution_count": 21, - "metadata": { - "button": false, - "collapsed": false, - "deletable": true, - "new_sheet": false, - "run_control": { - "read_only": false - } - }, + "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "231" - ] - }, - "execution_count": 21, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "N = 6: 1-1, 2-7, 2-7, 3-7, 3-7, 4-8\n", + "N = 10: 1-1, 2-14, 2-14, 3-14, 3-14, 3-14, 4-14, 4-14, 4-14, 5-15\n", + "N = 12: 1-1, 2-17, 2-17, 3-17, 3-17, 3-17, 4-17, 4-17, 4-17, 4-17, 5-17, 6-18\n", + "N = 20: 1-1, 2-32, 2-32, 3-32, 3-32, 3-32, 4-32, 4-32, 4-32, 4-32, 5-32, 5-32, 5-32, 5-32, 5-32, 6-32, 6-32, 6-32, 6-32, 7-33\n" + ] } ], "source": [ - "len(all_dice(6))" + "for N in (6, 10, 12, 20):\n", + " print(bounds(N))" ] }, { "cell_type": "markdown", - "metadata": { - "button": false, - "deletable": true, - "new_sheet": false, - "run_control": { - "read_only": false - } - }, + "metadata": {}, "source": [ - "Ali: Reassuring that we get the same number we got with the old version of `all_dice()`." + "**Bo: I can use the bounds to make a more constrained version of `all_dice(N)`, like this:**" ] }, { "cell_type": "code", "execution_count": 22, - "metadata": { - "button": false, - "collapsed": false, - "deletable": true, - "new_sheet": false, - "run_control": { - "read_only": false - } - }, - "outputs": [ - { - "data": { - "text/plain": [ - "[(1, 4, 5, 5, 7, 8),\n", - " (1, 4, 4, 5, 6, 8),\n", - " (1, 2, 6, 6, 6, 7),\n", - " (1, 3, 6, 6, 7, 8),\n", - " (1, 2, 2, 3, 6, 7),\n", - " (1, 2, 2, 3, 6, 8),\n", - " (1, 3, 3, 3, 3, 6),\n", - " (1, 3, 4, 5, 5, 7)]" - ] - }, - "execution_count": 22, - "metadata": {}, - "output_type": "execute_result" - } - ], + "metadata": {}, + "outputs": [], "source": [ - "random.sample(all_dice(6), 8)" + "def all_dice(N=6) -> List[Die]:\n", + " \"\"\"All feasible Sicherman dice with N sides.\"\"\"\n", + " uppers = upper_bounds(N)\n", + " dice = [ [1] ] # The first side must be 1\n", + " for i in range(1, N): # Add N - 1 more sides\n", + " dice = [die + [side] \n", + " for die in dice \n", + " for side in ints(die[-1] + (i == (N - 1)), uppers[i])\n", + " if die.count(side) < side]\n", + " return dice" ] }, { "cell_type": "markdown", - "metadata": { - "button": false, - "deletable": true, - "new_sheet": false, - "run_control": { - "read_only": false - } - }, + "metadata": {}, "source": [ - "# Running `sicherman(N)` for small `N`\n", + "Ali: What's up with `(i == (N - 1))`?\n", "\n", - "Ali: Let's try `sicherman(N)` for some small values of `N`:" + "**Bo: Oh. In Python a Boolean expression is equivalent to 0 or 1, so this says \"the lower bound for the next side is the same as the lower bound for the previous side, `die[-1]`, except that if this is the last side, then the lower bound is one more.\"**\n", + "\n", + "Ali: How come you don't even reference `lower_bounds(N)` within `all_dice(N)`? And how come you have the `if die.count(side) < side]` clause? Is this code repetition intentional, or did you forget that `lower_bound(N)` already dealt with having only two 2s, three 3s, etc.?\n", + "\n", + "**Bo: It is intentional, but I agree with you that it is confusing and look repetitious. Even though `lower_bounds` is not used in `all_dice`, it is used in `upper_bounds`; without it, the bounds wouldn't be as tight. The difference is that in `lower_bounds(N)` we're answering the question \"what's the lowest possible value for side *s* across every single *N*-sided die?\" In `all_dice(N)`, we're considering a different question: \"for this particular dice, what numbers can come next?\" That's two different things, so we need two pieces of code.**\n", + "\n", + "**Suppose that with `N = 6` we have the partial die `[1, 3, 3, 3]`. The lower bound for the next side is 3, and there are many die that have a 3 on the 5th side. But for this particular die we can't allow a fourth 3; that's why `all_dice(N)` needs to check, and can't rely on `lower_bounds(N)` alone.**\n", + "\n", + "Ali: I got it. \n", + "\n", + "Let's see how much we have reduced the numbers of `all_dice`:" ] }, { "cell_type": "code", "execution_count": 23, - "metadata": { - "button": false, - "collapsed": false, - "deletable": true, - "new_sheet": false, - "run_control": { - "read_only": false - } - }, + "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{2: set(),\n", - " 3: set(),\n", - " 4: {((1, 2, 2, 3), (1, 3, 3, 5))},\n", - " 5: set(),\n", - " 6: {((1, 2, 2, 3, 3, 4), (1, 3, 4, 5, 6, 8))}}" + "{2: 1, 3: 1, 4: 10, 5: 31, 6: 226, 7: 1555, 8: 5728, 9: 39416, 10: 266931}" ] }, "execution_count": 23, @@ -1233,51 +935,38 @@ } ], "source": [ - "{N: sicherman(N)\n", - " for N in ints(2, 6)}" + "{N: len(all_dice(N)) for N in ints(2, 10)}" ] }, { "cell_type": "markdown", - "metadata": { - "button": false, - "deletable": true, - "new_sheet": false, - "run_control": { - "read_only": false - } - }, + "metadata": {}, "source": [ - "Again, reassuring that we get the same result for `sicherman(6)`. And interesting that there is a result for `sicherman(4)` but not for the other *N*.\n", + "Ali: Wow! For `N = 6` we're down from 1287 to 226 dice. \n", "\n", - "Let's go onwards from *N*=6, but let's check the timing as we go:" + "**Bo: Let's check out some of the `all_dice(6)`; hopefully we got rid of the die with five 2s:**" ] }, { "cell_type": "code", "execution_count": 24, - "metadata": { - "button": false, - "collapsed": false, - "deletable": true, - "new_sheet": false, - "run_control": { - "read_only": false - } - }, + "metadata": {}, "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 277 ms, sys: 9.04 ms, total: 286 ms\n", - "Wall time: 344 ms\n" - ] - }, { "data": { "text/plain": [ - "{((1, 2, 2, 3, 3, 4), (1, 3, 4, 5, 6, 8))}" + "[[1, 2, 2, 3, 3, 4],\n", + " [1, 2, 2, 4, 5, 7],\n", + " [1, 2, 3, 3, 4, 5],\n", + " [1, 2, 3, 5, 5, 6],\n", + " [1, 2, 4, 5, 5, 6],\n", + " [1, 2, 6, 6, 6, 7],\n", + " [1, 3, 3, 4, 5, 7],\n", + " [1, 3, 4, 4, 5, 7],\n", + " [1, 3, 5, 5, 7, 8],\n", + " [1, 4, 4, 5, 5, 6],\n", + " [1, 4, 6, 6, 6, 7],\n", + " [1, 6, 6, 6, 6, 7]]" ] }, "execution_count": 24, @@ -1286,7 +975,14 @@ } ], "source": [ - "%time sicherman(6)" + "list(all_dice(6))[::20]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Ali: Let's rerun `sicherman(N)` on small values of `N`:" ] }, { @@ -1296,6 +992,9 @@ "button": false, "collapsed": false, "deletable": true, + "jupyter": { + "outputs_hidden": false + }, "new_sheet": false, "run_control": { "read_only": false @@ -1306,14 +1005,18 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 14.4 s, sys: 303 ms, total: 14.7 s\n", - "Wall time: 16.1 s\n" + "CPU times: user 66.2 ms, sys: 1.27 ms, total: 67.4 ms\n", + "Wall time: 66.4 ms\n" ] }, { "data": { "text/plain": [ - "set()" + "{2: [],\n", + " 3: [],\n", + " 4: [([1, 2, 2, 3], [1, 3, 3, 5])],\n", + " 5: [],\n", + " 6: [([1, 2, 2, 3, 3, 4], [1, 3, 4, 5, 6, 8])]}" ] }, "execution_count": 25, @@ -1321,6 +1024,59 @@ "output_type": "execute_result" } ], + "source": [ + "%time {N: sicherman(N) for N in ints(2, 6)}" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "button": false, + "deletable": true, + "new_sheet": false, + "run_control": { + "read_only": false + } + }, + "source": [ + "Ali: Awesome! We get the same results, but faster. Let's proceed cautiously on to 7:" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": { + "button": false, + "collapsed": false, + "deletable": true, + "jupyter": { + "outputs_hidden": false + }, + "new_sheet": false, + "run_control": { + "read_only": false + } + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 4.23 s, sys: 23 ms, total: 4.25 s\n", + "Wall time: 4.26 s\n" + ] + }, + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 26, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "%time sicherman(7)" ] @@ -1336,75 +1092,52 @@ } }, "source": [ - "# Estimating run time of `sicherman(N)` for larger `N`\n", + "Ali: Too bad! Too bad we didn't get a solution for *N* = 7, and too bad that it took 60 times longer than *N* = 6. We could do *N* = 8 with a wait of a few minutes, but *N* = 9 will take hours.\n", "\n", - "Ali: OK, it takes 50 times longer to do 7, compared to 6. At that rate, *N*=8 will take 15 minutes, 9 will take 15 hours, and 10 will take a month.\n", + "**Bo: We need a brand new approach; there are just too many dice. I'm not sure what to do to reduce them.**\n", "\n", - "**Bo: Do we know it will continue to rise at the same rate? You're saying the run time is exponential in *N*? **\n", + "Ali: Maybe we could concentrate on considering fewer *pairs* of dice, without worrying about considering fewer dice?\n", "\n", - "Ali: I think so. The run time is proportional to the number of pairs. The number of pairs is proportional to the square of the number of dice. The number of dice is roughly exponential in *N*, because each time you increase *N* by 1, you have to try a number of new sides that is similar to the number for the previous side (but not quite the same). I should plot the number of pairs on a log scale and see if it looks like a straight line.\n", + "**Bo: How could we do that? Isn't the number of pairs always determined by the number of dice?**\n", "\n", - "I can count the number of pairs without explicitly generating the pairs. If there are *D* dice, then the number of pairs is what? Something like *D* × (*D* + 1) / 2? Or is it *D* × (*D* - 1) / 2?\n", + "# Considering Fewer Pairs\n", "\n", - "**Bo: Let's draw a picture. With *D* = 4, here are all the ways to pair one die with another to yield 10 distinct pairs:**\n", + "Ali: Remember, we're looking for *feasible* pairs. So if there was some way of knowing ahead of time that two dice were incompatible as a pair, we wouldn't even need to consider the pair; we'd spend zero run time on them.\n", + "\n", + "**Bo: By incompatible, you mean they can't form a pair that is a solution.**\n", + "\n", + "Ali: Right. Consider this: in any valid pair, the sum of the biggest number on each die must be 2*N*. For example, with *N* = 6:\n", + "\n", + " ([1, 2, 2, 3, 3, 4], [1, 3, 4, 5, 6, 8]) sum of biggests = 4 + 8 = 12\n", + " ([1, 2, 3, 4, 5, 6], [1, 2, 3, 4, 5, 6]) sum of biggests = 6 + 6 = 12\n", " \n", - " 11 .. .. ..\n", - " 21 22 .. ..\n", - " 31 32 33 ..\n", - " 41 42 43 44\n", - " \n", - "**To figure out the formula, add a row to the top:**\n", + "So if we have a 6-sided die with biggest number 7, what dice should we consider pairing it with?\n", "\n", - " .. .. .. ..\n", - " 11 .. .. ..\n", - " 21 22 .. ..\n", - " 31 32 33 ..\n", - " 41 42 43 44\n", - " \n", - "**Now we have a *D* × (*D* + 1) rectangle, and we can see that half (10) of them are pairs, and half (the other 10) are not pairs (because they would be repetitions). So the formula is *D* × (*D* + 1)/2, and checking for *D*=4, (4 × 5) / 2 = 10, so we're good.**\n", - " \n", - "Ali: OK, let's try it. First some boilerplate for plotting:" - ] - }, - { - "cell_type": "code", - "execution_count": 26, - "metadata": { - "button": false, - "collapsed": false, - "deletable": true, - "new_sheet": false, - "run_control": { - "read_only": false - } - }, - "outputs": [], - "source": [ - "%matplotlib inline\n", + "**Bo: Only ones with biggest number 5.**\n", "\n", - "import matplotlib\n", - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", + "**I get it: we sort all the die into bins labeled with their biggest number. Then we look at each bin, and for the \"7\" bin, we pair them up with the dice in the \"5\" bin. In general, the *B* bin can only pair with the 2*N* - *B* bin.**\n", "\n", - "def logplot(X, Y, *options):\n", - " \"Plot Y on a log scale vs X.\"\n", - " fig, ax = plt.subplots()\n", - " ax.set_yscale('log')\n", - " ax.plot(X, Y, *options)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "button": false, - "deletable": true, - "new_sheet": false, - "run_control": { - "read_only": false - } - }, - "source": [ - "Now we can plot and display the number of pairs:" + "Ali: Exactly. \n", + "\n", + "**Bo: Cool. I can see how that can cut the amount of work by a factor of 10 or so. But I was hoping for a factor of a million.**\n", + "\n", + "Ali: There are other properties of a feasible pair.\n", + "\n", + "**Bo: Like what?**\n", + "\n", + "Ali: Well, what about the number of 2s in a pair?\n", + "\n", + "**Bo: Let's see. We know that any distribution of sums has to have two 3s, and the only way to make a 3 is 2+1. And each die has only one 1, so that means that each pair of dice has to have a total of exactly two 2s.**\n", + "\n", + "Ali: Does it have to be one 2 on each die?\n", + "\n", + "**No. It could be one each, or it could be two on one die and none on the other. So a die with *t* twos can only pair with dice that have 2 - *t* twos.**\n", + "\n", + "Ali: Great. Can you think of another property?\n", + "\n", + "**Bo: No. You got anything?**\n", + "\n", + "Ali: Let's look at the sums of 6-sided Sicherman and standard pairs:" ] }, { @@ -1414,6 +1147,9 @@ "button": false, "collapsed": false, "deletable": true, + "jupyter": { + "outputs_hidden": false + }, "new_sheet": false, "run_control": { "read_only": false @@ -1423,105 +1159,16 @@ { "data": { "text/plain": [ - "{2: 1,\n", - " 3: 1,\n", - " 4: 55,\n", - " 5: 496,\n", - " 6: 26796,\n", - " 7: 1274406,\n", - " 8: 17502486,\n", - " 9: 823794345,\n", - " 10: 37613250675,\n", - " 11: 1688821500366,\n", - " 12: 25925418283128}" + "42" ] }, "execution_count": 27, "metadata": {}, "output_type": "execute_result" - }, - { - "data": { - "image/png": "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\n", - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" } ], "source": [ - "def plot_pairs(Ns):\n", - " \"Given a list of N values, plot the number of pairs and return a dict of them.\"\n", - " Ds = [len(all_dice(N)) for N in Ns]\n", - " Npairs = [D * (D + 1) // 2 for D in Ds]\n", - " logplot(Ns, Npairs, 'bo-')\n", - " return {Ns[i]: Npairs[i] for i in range(len(Ns))}\n", - "\n", - "plot_pairs(ints(2, 12))" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "button": false, - "deletable": true, - "new_sheet": false, - "run_control": { - "read_only": false - } - }, - "source": [ - "Ali: OK, we've learned two things. One, it *is* roughly a straight line on a log scale, so the number of pairs is roughly exponential. Two, there are a *lot* of pairs. 1014, just for *N*=12. I don't want to even think about *N*=20.\n", - "\n", - "**Bo: So if we want to get much beyond *N*=8, we're either going to need a brand new approach, or we need to make far fewer pairs of dice.**\n", - "\n", - "\n", - "\n", - "# Making Fewer `pairs`\n", - "\n", - "Ali: Maybe we could tighten up the upper bounds on dice, but I don't think that will help very much.\n", - "How about if we concentrate on making fewer pairs, without worrying about making fewer dice?\n", - "\n", - "**Bo: How could we do that? Isn't the number of pairs always (*D*2 + *D*)/2 ?**\n", - "\n", - "Ali: Remember, we're looking for *feasible* pairs. So if there was some way of knowing ahead of time that two dice were incompatible as a pair, we wouldn't even need to consider the pair.\n", - "\n", - "**Bo: By incompatible, you mean they can't form a pair that is a solution.**\n", - "\n", - "Ali: Right. Consider this: in any valid pair, the sum of the biggest number on each die must be 2*N*. For example, with *N* = 6:\n", - "\n", - " ((1, 2, 2, 3, 3, 4), (1, 3, 4, 5, 6, 8)) sum of biggests = 4 + 8 = 12\n", - " ((1, 2, 3, 4, 5, 6), (1, 2, 3, 4, 5, 6)) sum of biggests = 6 + 6 = 12\n", - " \n", - "So if we have a die with biggest number 7, what dice should we consider pairing it with?\n", - "\n", - "**Bo: Only ones with biggest number 5.**\n", - "\n", - "**I get it: we sort all the die into bins labeled with their biggest number. Then we look at each bin, and for the \"7\" bin, we pair them up with the dice in the \"5\" bin. In general, the *B* bin can only pair with the 2*N* - *B* bin.**\n", - "\n", - "Ali: Exactly. \n", - "\n", - "**Bo: Cool. I can see how that can cut the amount of work by a factor of 10 or so. But I was hoping for a factor of a million or so.**\n", - "\n", - "Ali: There are other properties of a feasible pair.\n", - "\n", - "**Bo: Like what?**\n", - "\n", - "Ali: Well, what about the number of 2s in a pair?\n", - "\n", - "**Bo: Let's see. We know that any `regular_sums` has to have two 3s, and the only way to make a 3 is 2+1. And each die has only one 1, so that means that each pair of dice has to have a total of exactly two 2s.**\n", - "\n", - "Ali: Does it have to be one 2 on each die?\n", - "\n", - "**No. It could be one each, or it could be two on one die and none on the other. So a die with *T* twos can only pair with dice that have 2 - *T* twos.**\n", - "\n", - "Ali: Great. Can you think of another property?\n", - "\n", - "**Bo: Give me a hint.**\n", - "\n", - "Ali: Let's look at the sums of 6-sided Sicherman and regular pairs:" + "sum([1, 2, 2, 3, 3, 4] + [1, 3, 4, 5, 6, 8])" ] }, { @@ -1531,6 +1178,9 @@ "button": false, "collapsed": false, "deletable": true, + "jupyter": { + "outputs_hidden": false + }, "new_sheet": false, "run_control": { "read_only": false @@ -1549,7 +1199,27 @@ } ], "source": [ - "sum((1, 2, 2, 3, 3, 4) + (1, 3, 4, 5, 6, 8))" + "sum([1, 2, 3, 4, 5, 6] + [1, 2, 3, 4, 5, 6])" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "button": false, + "deletable": true, + "new_sheet": false, + "run_control": { + "read_only": false + } + }, + "source": [ + "**Bo: They're the same. Is that [the question](http://hitchhikers.wikia.com/wiki/42) that 42 is the answer to? But does a Sicherman pair always have to have the same sum as a standard pair? I guess it does, because the sum of `distribution_of_sums(pair)` is just all the sides added up *N* times each, so two pairs have the same sum of `distribution_of_sums(pair)` if and only if they have the same sum.**\n", + "\n", + "Ali: So consider the die [1, 3, 3, 3, 4, 5]. What do we know about the dice that it can possibly pair with?\n", + "\n", + "**Bo: OK, that die has a biggest side of 5, so it can only pair with dice that have a biggest side of 12 - 5 = 7. It has a sum of 19, so it can only pair with dice that have a sum of 42 - 19 = 23. And it has no 2s, so it can only pair with dice that have two 2s.**\n", + "\n", + "Ali: I wonder how many such dice there are, out of all 226 `all_dice(6)`?" ] }, { @@ -1559,6 +1229,9 @@ "button": false, "collapsed": false, "deletable": true, + "jupyter": { + "outputs_hidden": false + }, "new_sheet": false, "run_control": { "read_only": false @@ -1568,7 +1241,7 @@ { "data": { "text/plain": [ - "42" + "[[1, 2, 2, 5, 6, 7]]" ] }, "execution_count": 29, @@ -1577,7 +1250,8 @@ } ], "source": [ - "sum((1, 2, 3, 4, 5, 6) + (1, 2, 3, 4, 5, 6))" + "[die for die in all_dice(6) \n", + " if max(die) == 7 and sum(die) == 23 and die.count(2) == 2]" ] }, { @@ -1591,13 +1265,15 @@ } }, "source": [ - "**Bo: They're the same. Is that [the question](http://hitchhikers.wikia.com/wiki/42) that 42 is the answer to? But does a Sicherman pair always have to have the same sum as a regular pair? I guess it doea, because the sum of `sums(pair)` is just all the sides added up *N* times each, so two pairs have the same sum of `sums(pair)` if and only if they have the same sum.**\n", + "**Bo: There's only one! So, (1, 3, 3, 3, 4, 5) only has to try to pair with one die, rather than 226. Nice improvement!**\n", "\n", - "Ali: So consider the die (1, 3, 3, 3, 4, 5). What do we know about the dice that it can possibly pair with?\n", + "Ali: In general, what's the sum of the sides of a standard pair?\n", "\n", - "**Bo: OK, that die has a biggest side of 5, so it can only pair with dice that have a biggest side of 12 - 5 = 7. It has a sum of 19, so it can only pair with dice that have a sum of 42 - 19 = 23. And it has no 2s, so it can only pair with dice that have two 2s.**\n", + "**Bo: Easy, that's *N* × (*N* + 1). [Gauss](http://betterexplained.com/articles/techniques-for-adding-the-numbers-1-to-100/) knew that when he was in elementary school!**\n", "\n", - "Ali: I wonder how many such dice there are, out of all 231 `all_dice(6)`?" + "# Better Efficiency\n", + "\n", + "**Bo: OK, we can code this up easily enough. The point of all the code below is to redefine `all_pairs(dice)` to be more efficient:**:" ] }, { @@ -1607,56 +1283,9 @@ "button": false, "collapsed": false, "deletable": true, - "new_sheet": false, - "run_control": { - "read_only": false - } - }, - "outputs": [ - { - "data": { - "text/plain": [ - "{(1, 2, 2, 5, 6, 7)}" - ] - }, - "execution_count": 30, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "{die for die in all_dice(6) if max(die) == (12 - 5) and sum(die) == (42 - 19) and die.count(2) == 2}" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "button": false, - "deletable": true, - "new_sheet": false, - "run_control": { - "read_only": false - } - }, - "source": [ - "**Bo: There's only one! So, (1, 3, 3, 3, 4, 5) only has to try to pair with one die, rather than 231. Nice improvement!**\n", - "\n", - "Ali: In general, I wonder what the sum of the sides of a regular pair is?\n", - "\n", - "**Bo: Easy, that's `N * (N + 1)`. [Gauss](http://betterexplained.com/articles/techniques-for-adding-the-numbers-1-to-100/) knew that when he was in elementary school!**\n", - "\n", - "# More efficient `pairs(dice)`\n", - "\n", - "**Bo: OK, we can code this up easily enough**:" - ] - }, - { - "cell_type": "code", - "execution_count": 31, - "metadata": { - "button": false, - "collapsed": false, - "deletable": true, + "jupyter": { + "outputs_hidden": false + }, "new_sheet": false, "run_control": { "read_only": false @@ -1666,69 +1295,109 @@ "source": [ "from collections import defaultdict\n", "\n", - "def tabulate(dice):\n", - " \"\"\"Put all dice into bins in a hash table, keyed by bin_label(die).\n", - " Each bin holds a list of dice with that key.\"\"\"\n", - " # Example: {(21, 6, 1): [(1, 2, 3, 4, 5, 6), (1, 2, 3, 4, 4, 7), ...]\n", - " table = defaultdict(list)\n", - " for die in dice:\n", - " table[bin_label(die)].append(die)\n", - " return table\n", + "Label = tuple # Type for the label of a bin in the hash table\n", "\n", - "def pairs(dice):\n", - " \"Return all pairs of dice that could possibly be a solution to the Sicherman problem.\"\n", - " table = tabulate(dice)\n", + "def label(die) -> Label: \n", + " \"\"\"Return a tuple of the sum of the die, the biggest side, and the number of 2's.\"\"\"\n", + " return sum(die), max(die), die.count(2)\n", + "\n", + "def all_pairs(dice: List[Die]) -> Iterable[Pair]:\n", + " \"\"\"Yield all pairs of dice that could possibly be a solution to the Sicherman problem.\"\"\"\n", + " table = tabulate_dice(dice)\n", " N = len(dice[0])\n", " for bin1 in table:\n", - " bin2 = compatible_bin(bin1, N)\n", + " bin2 = compatible_label(bin1, N)\n", " if bin2 in table and bin1 <= bin2:\n", " for A in table[bin1]:\n", " for B in table[bin2]:\n", " yield (A, B)\n", "\n", - "def bin_label(die): return sum(die), max(die), die.count(2)\n", + "def tabulate_dice(dice: List[Die]) -> Dict[Label, List[Die]]:\n", + " \"\"\"Put all dice into bins in a hash table, keyed by label(die).\n", + " Each bin holds a list of dice with that key.\"\"\"\n", + " # Example: {(21, 6, 1): [(1, 2, 3, 4, 5, 6), (1, 2, 3, 4, 4, 7), ...]\n", + " table = defaultdict(list)\n", + " for die in dice:\n", + " table[label(die)].append(die)\n", + " return table\n", "\n", - "def compatible_bin(bin1, N): \n", - " \"Return a bin label that is compatible with bin1.\"\n", - " (S1, M1, T1) = bin1\n", - " return (N * (N + 1) - S1, 2 * N - M1, 2 - T1)" + "def compatible_label(bin1, N) -> Label: \n", + " \"\"\"Return a bin label that is compatible with bin1.\"\"\"\n", + " (sum1, biggest1, twos1) = bin1\n", + " return (N * (N + 1) - sum1, 2 * N - biggest1, 2 - twos1)" ] }, { "cell_type": "markdown", - "metadata": { - "button": false, - "deletable": true, - "new_sheet": false, - "run_control": { - "read_only": false - } - }, + "metadata": {}, "source": [ - "**Bo: Let's make sure we haven't broken anything:**" + "**Bo: let's see what a table looks like. Here's with *N* = 5:**" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{(12, 4, 2): [[1, 2, 2, 3, 4]],\n", + " (13, 5, 2): [[1, 2, 2, 3, 5]],\n", + " (14, 6, 2): [[1, 2, 2, 3, 6]],\n", + " (14, 5, 2): [[1, 2, 2, 4, 5]],\n", + " (15, 6, 2): [[1, 2, 2, 4, 6]],\n", + " (16, 6, 2): [[1, 2, 2, 5, 6]],\n", + " (13, 4, 1): [[1, 2, 3, 3, 4]],\n", + " (14, 5, 1): [[1, 2, 3, 3, 5]],\n", + " (15, 6, 1): [[1, 2, 3, 3, 6]],\n", + " (15, 5, 1): [[1, 2, 3, 4, 5]],\n", + " (16, 6, 1): [[1, 2, 3, 4, 6]],\n", + " (17, 6, 1): [[1, 2, 3, 5, 6], [1, 2, 4, 4, 6]],\n", + " (16, 5, 1): [[1, 2, 4, 4, 5]],\n", + " (18, 6, 1): [[1, 2, 4, 5, 6]],\n", + " (19, 6, 1): [[1, 2, 5, 5, 6]],\n", + " (14, 4, 0): [[1, 3, 3, 3, 4]],\n", + " (15, 5, 0): [[1, 3, 3, 3, 5]],\n", + " (16, 6, 0): [[1, 3, 3, 3, 6]],\n", + " (16, 5, 0): [[1, 3, 3, 4, 5]],\n", + " (17, 6, 0): [[1, 3, 3, 4, 6]],\n", + " (18, 6, 0): [[1, 3, 3, 5, 6], [1, 3, 4, 4, 6]],\n", + " (17, 5, 0): [[1, 3, 4, 4, 5]],\n", + " (19, 6, 0): [[1, 3, 4, 5, 6], [1, 4, 4, 4, 6]],\n", + " (20, 6, 0): [[1, 3, 5, 5, 6], [1, 4, 4, 5, 6]],\n", + " (18, 5, 0): [[1, 4, 4, 4, 5]],\n", + " (21, 6, 0): [[1, 4, 5, 5, 6]],\n", + " (22, 6, 0): [[1, 5, 5, 5, 6]]}" + ] + }, + "execution_count": 31, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dict(tabulate_dice(all_dice(5)))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Ali: That's good! The dice are well spread out into small bins, most of them of size 1; a few of size 2.\n", + "\n", + "**Bo: Let's look at some statistics for *N* = 8:**" ] }, { "cell_type": "code", "execution_count": 32, - "metadata": { - "button": false, - "collapsed": false, - "deletable": true, - "new_sheet": false, - "run_control": { - "read_only": false - } - }, + "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{2: set(),\n", - " 3: set(),\n", - " 4: {((1, 2, 2, 3), (1, 3, 3, 5))},\n", - " 5: set(),\n", - " 6: {((1, 2, 2, 3, 3, 4), (1, 3, 4, 5, 6, 8))}}" + "(5728, 395, 6039)" ] }, "execution_count": 32, @@ -1737,49 +1406,68 @@ } ], "source": [ - "{N: sicherman(N)\n", - " for N in ints(2, 6)}" + "N = 8\n", + "table = tabulate_dice(all_dice(N))\n", + "len(all_dice(N)), len(table), len(list(all_pairs(all_dice(N))))" ] }, { "cell_type": "markdown", - "metadata": { - "button": false, - "deletable": true, - "new_sheet": false, - "run_control": { - "read_only": false - } - }, + "metadata": {}, "source": [ - "Ali: Good, those are the same answers as before. But how much faster is it?" + "**Bo: This says there are 5,728 8-sided dice; they're sorted into 395 bins, and `all_pairs` only needs to look at 6,039 pairs, not the 32 million pairs that the old version of `all_pairs` would need to consider.**\n", + "\n", + "Ali: That's fantastic. Why are there so few pairs to consider? Less than one pair per die?\n", + "\n", + "**Bo: I'm not sure. I can get a Counter of how many times we see each size bin, as we go through `table`:**" ] }, { "cell_type": "code", "execution_count": 33, - "metadata": { - "button": false, - "collapsed": false, - "deletable": true, - "new_sheet": false, - "run_control": { - "read_only": false - } - }, + "metadata": {}, "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 21.3 ms, sys: 2.41 ms, total: 23.7 ms\n", - "Wall time: 26.4 ms\n" - ] - }, { "data": { "text/plain": [ - "set()" + "[(0, 192),\n", + " (1, 30),\n", + " (2, 25),\n", + " (3, 16),\n", + " (5, 13),\n", + " (6, 11),\n", + " (11, 11),\n", + " (16, 10),\n", + " (4, 9),\n", + " (8, 8),\n", + " (9, 6),\n", + " (32, 6),\n", + " (7, 6),\n", + " (14, 5),\n", + " (30, 4),\n", + " (13, 4),\n", + " (55, 3),\n", + " (25, 3),\n", + " (29, 3),\n", + " (18, 3),\n", + " (20, 3),\n", + " (10, 2),\n", + " (17, 2),\n", + " (48, 2),\n", + " (49, 2),\n", + " (19, 2),\n", + " (12, 2),\n", + " (23, 2),\n", + " (90, 1),\n", + " (58, 1),\n", + " (88, 1),\n", + " (81, 1),\n", + " (51, 1),\n", + " (28, 1),\n", + " (76, 1),\n", + " (42, 1),\n", + " (21, 1),\n", + " (37, 1)]" ] }, "execution_count": 33, @@ -1788,7 +1476,7 @@ } ], "source": [ - "%time sicherman(7)" + "Counter(len(table.get(compatible_label(label1, N), [])) for label1 in table).most_common()" ] }, { @@ -1802,9 +1490,18 @@ } }, "source": [ - "Ali: Wow, that's 1000 times faster than before.\n", + "**Bo: OK, that was kind of dense, but what it says is:**\n", + "- **Of the 395 bins in the *N* = 8 table, 192 of them have a compatible bin with zero dice in it.**\n", + "- **That means that half the time, we don't have to do *any* checking of pairs.**\n", + "- **That's the main reason why the number of pairs is less than the number of dice.**\n", + "- **30 bins have a compatible bin with just 1 die, 25 with just 2 dice, and so on.**\n", + "- **So for most bins, there will be zero checking of pairs, or just a few checks.**\n", "\n", - "**Bo: I want to take a peek at what some of the bins look like:**" + "Ali: Amazing!\n", + "\n", + "# Final Results\n", + "\n", + "**Bo: I think this will be a lot faster. Let's try to go up to 9:**" ] }, { @@ -1814,43 +1511,36 @@ "button": false, "collapsed": false, "deletable": true, + "jupyter": { + "outputs_hidden": false + }, "new_sheet": false, "run_control": { "read_only": false } }, "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 495 ms, sys: 2.27 ms, total: 498 ms\n", + "Wall time: 497 ms\n" + ] + }, { "data": { "text/plain": [ - "defaultdict(list,\n", - " {(12, 4, 2): [(1, 2, 2, 3, 4)],\n", - " (13, 4, 1): [(1, 2, 3, 3, 4)],\n", - " (13, 5, 2): [(1, 2, 2, 3, 5)],\n", - " (14, 4, 0): [(1, 3, 3, 3, 4)],\n", - " (14, 5, 1): [(1, 2, 3, 3, 5)],\n", - " (14, 5, 2): [(1, 2, 2, 4, 5)],\n", - " (14, 6, 2): [(1, 2, 2, 3, 6)],\n", - " (15, 5, 0): [(1, 3, 3, 3, 5)],\n", - " (15, 5, 1): [(1, 2, 3, 4, 5)],\n", - " (15, 6, 1): [(1, 2, 3, 3, 6)],\n", - " (15, 6, 2): [(1, 2, 2, 4, 6)],\n", - " (16, 5, 0): [(1, 3, 3, 4, 5)],\n", - " (16, 5, 1): [(1, 2, 4, 4, 5)],\n", - " (16, 6, 0): [(1, 3, 3, 3, 6)],\n", - " (16, 6, 1): [(1, 2, 3, 4, 6)],\n", - " (16, 6, 2): [(1, 2, 2, 5, 6)],\n", - " (17, 5, 0): [(1, 3, 4, 4, 5)],\n", - " (17, 6, 0): [(1, 3, 3, 4, 6)],\n", - " (17, 6, 1): [(1, 2, 3, 5, 6), (1, 2, 4, 4, 6)],\n", - " (18, 5, 0): [(1, 4, 4, 4, 5)],\n", - " (18, 6, 0): [(1, 3, 3, 5, 6), (1, 3, 4, 4, 6)],\n", - " (18, 6, 1): [(1, 2, 4, 5, 6)],\n", - " (19, 6, 0): [(1, 3, 4, 5, 6), (1, 4, 4, 4, 6)],\n", - " (19, 6, 1): [(1, 2, 5, 5, 6)],\n", - " (20, 6, 0): [(1, 3, 5, 5, 6), (1, 4, 4, 5, 6)],\n", - " (21, 6, 0): [(1, 4, 5, 5, 6)],\n", - " (22, 6, 0): [(1, 5, 5, 5, 6)]})" + "{2: [],\n", + " 3: [],\n", + " 4: [([1, 2, 2, 3], [1, 3, 3, 5])],\n", + " 5: [],\n", + " 6: [([1, 2, 2, 3, 3, 4], [1, 3, 4, 5, 6, 8])],\n", + " 7: [],\n", + " 8: [([1, 2, 2, 3, 3, 4, 4, 5], [1, 3, 5, 5, 7, 7, 9, 11]),\n", + " ([1, 2, 2, 3, 5, 6, 6, 7], [1, 3, 3, 5, 5, 7, 7, 9]),\n", + " ([1, 2, 3, 3, 4, 4, 5, 6], [1, 2, 5, 5, 6, 6, 9, 10])],\n", + " 9: [([1, 2, 2, 3, 3, 3, 4, 4, 5], [1, 4, 4, 7, 7, 7, 10, 10, 13])]}" ] }, "execution_count": 34, @@ -1859,7 +1549,7 @@ } ], "source": [ - "tabulate(all_dice(5))" + "%time {N: sicherman(N) for N in ints(2, 9)}" ] }, { @@ -1873,100 +1563,56 @@ } }, "source": [ - "**Bo: Pretty good: four of the bins have two dice, and all the rest have only one die.**\n", + "Ali: Excellent! Very fast; we got the same answers as before for 2–7, and we got **three** solutions for 8 and one for 9, all in half a second. \n", "\n", - "Ali: And let's see how many pairs we're producing now. We'll tabulate *N* (the number of sides); *D* (the number of *N*-sided dice), the number `pairs(dice)` using the new `pairs`, and the number using the old `pairs`:" + "I'm confident we can [go to 11](https://www.youtube.com/watch?v=hW008FcKr3Q):" ] }, { "cell_type": "code", "execution_count": 35, - "metadata": { - "button": false, - "collapsed": false, - "deletable": true, - "new_sheet": false, - "run_control": { - "read_only": false - } - }, + "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - " N: D #pairs(dice) D*(D-1)/2\n", - " 2: 1 1 0\n", - " 3: 1 1 0\n", - " 4: 10 3 45\n", - " 5: 31 9 465\n", - " 6: 231 71 26,565\n", - " 7: 1,596 670 1,272,810\n", - " 8: 5,916 6,614 17,496,570\n", - " 9: 40,590 76,215 823,753,755\n", - "10: 274,274 920,518 37,612,976,401\n", - "11: 1,837,836 11,506,826 1,688,819,662,530\n" - ] - } - ], - "source": [ - "print(' N: D #pairs(dice) D*(D-1)/2')\n", - "for N in ints(2, 11):\n", - " dice = list(all_dice(N))\n", - " D = len(dice)\n", - " print('{:2}: {:9,d} {:12,d} {:17,d}'.format(N, D, len(list(pairs(dice))), D*(D-1)//2))" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "button": false, - "collapsed": true, - "deletable": true, - "new_sheet": false, - "run_control": { - "read_only": false - } - }, - "source": [ - "Ali: OK, we're doing 100,000 times better for *N*=11. But it would still take a long time to test 11 million pairs. Let's just get the answers up to *N*=10:" - ] - }, - { - "cell_type": "code", - "execution_count": 36, - "metadata": { - "button": false, - "collapsed": false, - "deletable": true, - "new_sheet": false, - "run_control": { - "read_only": false - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "CPU times: user 21.4 s, sys: 197 ms, total: 21.6 s\n", - "Wall time: 22 s\n" + "CPU times: user 6.03 s, sys: 17 ms, total: 6.04 s\n", + "Wall time: 6.05 s\n" ] }, { "data": { "text/plain": [ - "{2: set(),\n", - " 3: set(),\n", - " 4: {((1, 2, 2, 3), (1, 3, 3, 5))},\n", - " 5: set(),\n", - " 6: {((1, 2, 2, 3, 3, 4), (1, 3, 4, 5, 6, 8))},\n", - " 7: set(),\n", - " 8: {((1, 2, 2, 3, 3, 4, 4, 5), (1, 3, 5, 5, 7, 7, 9, 11)),\n", - " ((1, 2, 2, 3, 5, 6, 6, 7), (1, 3, 3, 5, 5, 7, 7, 9)),\n", - " ((1, 2, 3, 3, 4, 4, 5, 6), (1, 2, 5, 5, 6, 6, 9, 10))},\n", - " 9: {((1, 2, 2, 3, 3, 3, 4, 4, 5), (1, 4, 4, 7, 7, 7, 10, 10, 13))},\n", - " 10: {((1, 2, 2, 3, 3, 4, 4, 5, 5, 6), (1, 3, 5, 6, 7, 8, 9, 10, 12, 14))}}" + "[([1, 2, 2, 3, 3, 4, 4, 5, 5, 6], [1, 3, 5, 6, 7, 8, 9, 10, 12, 14])]" + ] + }, + "execution_count": 35, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time sicherman(10)" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 1min 28s, sys: 206 ms, total: 1min 28s\n", + "Wall time: 1min 28s\n" + ] + }, + { + "data": { + "text/plain": [ + "[]" ] }, "execution_count": 36, @@ -1975,9 +1621,55 @@ } ], "source": [ - "%%time\n", - "{N: sicherman(N)\n", - " for N in ints(2, 10)}" + "%time sicherman(11)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Bo: Why not 12? It will take at least 10 or 15 minutes, so we can go get a coffee and come back to see the result.**" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "CPU times: user 19min 29s, sys: 1.57 s, total: 19min 31s\n", + "Wall time: 19min 32s\n" + ] + }, + { + "data": { + "text/plain": [ + "[([1, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 6],\n", + " [1, 4, 5, 7, 8, 9, 10, 11, 12, 14, 15, 18]),\n", + " ([1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7],\n", + " [1, 3, 5, 7, 7, 9, 9, 11, 11, 13, 15, 17]),\n", + " ([1, 2, 2, 3, 3, 4, 7, 8, 8, 9, 9, 10],\n", + " [1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14]),\n", + " ([1, 2, 2, 3, 5, 6, 6, 7, 9, 10, 10, 11],\n", + " [1, 3, 3, 5, 5, 7, 7, 9, 9, 11, 11, 13]),\n", + " ([1, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 8],\n", + " [1, 2, 5, 6, 7, 8, 9, 10, 11, 12, 15, 16]),\n", + " ([1, 2, 3, 4, 4, 5, 5, 6, 6, 7, 8, 9],\n", + " [1, 2, 3, 7, 7, 8, 8, 9, 9, 13, 14, 15]),\n", + " ([1, 2, 3, 3, 4, 5, 7, 8, 9, 9, 10, 11],\n", + " [1, 2, 4, 5, 5, 6, 8, 9, 9, 10, 12, 13])]" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%time sicherman(12)" ] }, { @@ -1991,100 +1683,32 @@ } }, "source": [ - "Ali: Not bad; we solved it for all *N* up to 10 in just a few seconds more than it took for just *N*=7 with the old version. Interestingly, there are solutions for all composite *N* but no prime *N*. So I have some questions:\n", + "Ali: Let's see what we got. \n", + "\n", + "Wow! Seven distinct solutions for *N* = 12. Why 7?\n", + "\n", + "**Bo: I don't know, but I think we can stop here.**\n", + "\n", + "Ali: I agree. But I still have some questions:\n", "\n", "- Do all composite *N* have solutions?\n", - "- Do no prime *N* have solutions?\n", + "- Does any prime *N* have a solution?\n", "- Do some *N* have more than the 3 solutions that 8 has?\n", "- Does the fact that 8 has 3 solutions have anything to do with the fact that 8 is a perfect cube?\n", - "- Could we handle larger *N* if we worked with [generating functions](https://en.wikipedia.org/wiki/Sicherman_dice)?\n", + "- What about the 7 solutions for 12? 12 has 6 factors; does that matter?\n", + "- But 9 is a perfect square, and it only has 1 solution.\n", + "- Could we handle greater values of *N* if we worked with [generating functions](https://en.wikipedia.org/wiki/Sicherman_dice)?\n", + "- What about Sicherman triples (3 nonstandard dice with the same distribution of sums as 3 standard dice)?\n", "\n", + "**Bo: Good questions, but I think I'd rather leave those for the reader to deal with. (Hi, reader!)**\n", "\n", - "**Bo: Good questions, but I think I'd rather leave those for the reader to deal with (hi, reader!).**\n", - "\n", - "**I do have one other question I'd like to handle, though: are there Sicherman *triples*? **\n", - "\n", - "# Sicherman Triples\n", - "\n", - "Ali: You mean three dice that add up to the same sums as a \"regular\" set of three dice?\n", - "\n", - "**Bo: Right. I can define `triples(dice)` in analogy to `pairs(dice)`. For now, I'm only interested in *N*=6, so I won't bother making an efficient version that tabulates into bins. I'll also extend `sums` to handle triples:**" - ] - }, - { - "cell_type": "code", - "execution_count": 37, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "import itertools\n", - "\n", - "def triples(dice): \n", - " \"Return all triples (A, B, C) from dice where A <= B <= C and sum matches regular.\"\n", - " regular_sum3 = 3 * sum(regular_die(6))\n", - " return [(A, B, C) for A in dice for B in dice for C in dice \n", - " if A <= B <= C and sum(A) + sum(B) + sum(C) == regular_sum3]\n", - "\n", - "def sums(dice):\n", - " \"All possible sums of a side from each of the dice.\"\n", - " return Bag(map(sum, itertools.product(*dice)))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Ali: Here's a version of `sicherman` that takes triples:" - ] - }, - { - "cell_type": "code", - "execution_count": 38, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/plain": [ - "{((1, 2, 2, 3, 3, 4), (1, 2, 3, 4, 5, 6), (1, 3, 4, 5, 6, 8))}" - ] - }, - "execution_count": 38, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "def sicherman3(N=6):\n", - " \"\"\"The set of triples of 6-sided dice that have the same\n", - " distribution of sums as a regular triple of dice.\"\"\"\n", - " reg_trip = 3 * (regular_die(N),)\n", - " reg_sums = sums(reg_trip)\n", - " return {trip for trip in triples(all_dice(N))\n", - " if trip != reg_trip\n", - " and sums(trip) == reg_sums}\n", - "\n", - "sicherman3()" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "**Bo: Of course! I should have known. This is just a pair of Sicherman dice, with a regular die thrown in. But I didn't know whether there would be any other solutions; I see there aren't.** \n", - "\n", - "Ali: Maybe ... but are we being too strict here? We defined `all_dice` to eliminate any die that wouldn't work as one of a Sicherman pair; but maybe we eliminated some that could work as a triple? Like, we decided that the biggest feasible number on a side was 8, but now that sums go all the way up to 18, not 12, maybe we should allow more? \n", - "\n", - "**Bo: Good point. But let's leave that question for the reader too (hi, reader!)**." + "Ali: Hi reader! Let us know what you find out!" ] } ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -2098,9 +1722,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.0" + "version": "3.8.15" } }, "nbformat": 4, - "nbformat_minor": 0 + "nbformat_minor": 4 }