diff --git a/ipynb/Mean Misanthrope Density.ipynb b/ipynb/Mean Misanthrope Density.ipynb
index 846ab78..07a3bf8 100644
--- a/ipynb/Mean Misanthrope Density.ipynb
+++ b/ipynb/Mean Misanthrope Density.ipynb
@@ -760,7 +760,7 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "Let's measure the deltas for the simulation results (and the `estimated_density` results) compared to the actual `density` results:"
+ "Let's measure the deltas for the simulation results (and the `estimated_sensity` results) compared to the actual `density` results:"
]
},
{
@@ -925,7 +925,7 @@
"def simulate_archery(previous_arrows=()) -> int:\n",
" \"\"\"Simulate the archery tournament and return the number of arrows fired.\"\"\"\n",
" arrow = random_arrow()\n",
- " if any(distance(arrow) > distance(a) for a in previous_arrows):\n",
+ " if any(abs(arrow) > distance(a) for a in previous_arrows):\n",
" return len(previous_arrows) + 1\n",
" else:\n",
" return simulate_archery([*previous_arrows, arrow])"
diff --git a/ipynb/Orderable Cards.ipynb b/ipynb/Orderable Cards.ipynb
index 2e1b9d0..2984f5c 100644
--- a/ipynb/Orderable Cards.ipynb
+++ b/ipynb/Orderable Cards.ipynb
@@ -6,38 +6,39 @@
"source": [
"
Peter Norvig October 2018 Type annotations 2026
\n",
"\n",
- "# Properly Ordered Card Hands\n",
+ "# Properly Organized Card Hands\n",
"\n",
- "The 538 Riddler [presented](https://fivethirtyeight.com/features/who-will-capture-the-most-james-bonds/) this problem by Matt Ginsberg:\n",
+ "The 538 Riddler [presented](https://fivethirtyeight.com/features/who-will-capture-the-most-james-bonds/) this problem by [Matt Ginsberg](https://www.linkedin.com/in/matthew-ginsberg-8172bb77/) (slightly rephrased):\n",
" \n",
- "> *You play so many card games that you’ve developed a very specific organizational obsession. When you’re dealt your hand, you want to organize it such that the cards of a given suit are grouped together and, if possible, such that no suited groups of the same color are adjacent. (Numbers don’t matter to you.) Moreover, when you receive your randomly ordered hand, you want to achieve this organization with a single motion, moving only one adjacent block of cards to some other position in your hand, maintaining the original order of that block and other cards, except for that one move.*\n",
- ">\n",
- "> *Suppose you’re playing pitch, in which a hand has six cards. What are the odds that you can accomplish your obsessive goal? What about for another game, where a hand has N cards, somewhere between 1 and 13?*\n",
+ "> *When you’re dealt a hand of cards, you want to organize them such that (1) the cards of a given suit are grouped together and (2) the suits alternate color (except that if all the cards are the same color, they don't have to alternate). Numbers don’t matter to you, just suits. You want to achieve this proper organization with a single move, relocating one adjacent block of cards to some other position in your hand. What is the probability that you can accomplish your obsessive goal for the game of pitch, which has 6-card hands? What about for any number of cards from 1 and 13?*\n",
"\n",
- "# Brute Force \n",
+ "Here are four example 6-card hands, two that can be properly organized with a move of a single block (in parens), and two that can't:\n",
"\n",
- "The first thing to decide is how many `N`-card hands are there? If there are only a few, I could just enumerate them all and count how many are orderable. If there are a lot, I'll have to be more clever. The answer is (52 choose `N`), so we have:\n",
+ "|6-card hand|block to move|result of move|properly organized?|\n",
+ "|---|---|---|---|\n",
+ "|♠️♥️♦️♣️♣️♦️|♠️♥️♦️(♣️♣️♦️)|♠️♥️(♣️♣️♦️)♦️|yes|\n",
+ "|♠️♣️♣️♠️♠️♣️|♠️(♣️♣️)♠️♠️♣️|♠️♠️♠️(♣️♣️)♣️|yes; adjacent ♠️♣️ ok because hand is all black|\n",
+ "|♠️♥️♦️♣️♣️♠️|♠️♥️♦️♣️♣️(♠️)|(♠️)♠️♥️♦️♣️♣️|no; adjacent ♥️♦️ not ok|\n",
+ "|♠️♥️♠️♥️♠️♥️|♠️♥️(♠️)♥️♠️♥️|♠️♥️♥️(♠️)♠️♥️|no; suits not grouped together|\n",
"\n",
- "- 6 cards: 20,358,520 hands\n",
- "- 13 cards: 635,013,559,600 hands \n",
"\n",
- "I could handle 6-card hands, but 600 billion is a lot of hands. Can we reduce the number of hands we have to deal with?\n",
+ "# Brute Force versus Abstraction\n",
"\n",
- "# Abstract Hands: Suits Only\n",
+ "The **brute force** approach says: consider every possible hand and check if it is organizable. There are (52 choose *N*) possible *N*-card hands, which is a lot of hands when *N* = 13. The **abstraction** approach asks if there is a way in which one abstract hand can represent multiple concrete hands. It turns out there is: the problem states *\"Numbers don’t matter,\"* so we can abstract from, say, \"seven of spades\" to just \"a spade.\" Then there are only 4*N* abstract hands. Compare:\n",
"\n",
- "The problem says *\"Numbers don’t matter,\"* so I can abstract away from \"seven of spades\" to just \"a spade.\" Then there are only 4*N* abstract hands (for *N* ≤ 13), so we have:\n",
+ "|*N*|Concrete Hands|Abstract Hands|\n",
+ "|--|---|---|\n",
+ "|6|20,358,520|4,096|\n",
+ "13|635,013,559,600|67,108,864|\n",
+ "|*N*|(52 choose *N*) | *N*4|\n",
"\n",
- "- 6 cards: 4,096 abstract hands\n",
- "- 13 cards: 67,108,864 abstract hands\n",
- "\n",
- "That's a big improvement! \n",
- "\n",
- "Now let's start coding:\n",
+ "That's a big improvement! Now let's start designing a program to solve the problem:\n",
"- There are two red suits and two black suits, so I'll represent the four suits as `'rRbB'`.\n",
- "- An abstract hand can be represented as a string of suits: `'rrBrbr'` is a 6-card hand. \n",
- "- `deals(N)` will return a dict of all possible abstract hands of length *N*, each mapped to the probability of the hand. \n",
- "- With actual hands, every hand has the same probability, because every card is equally likely to be the next card dealt. But with abstract hands, the probability of the next suit depends on how many cards of that suit have already been dealt. If I've already dealt the 12 cards `'rrrrrrrrrrrr'`, then the probability of the next card being an `'r'` is 1/40, and the probability of it being a `'b'` is 13/40. So as I build up the abstract hands, I'll need to keep track of the number of remaining cards of each suit.\n",
- "- I'll use `Fraction` to get exact arithmetic and `cache` to avoid repeated computations."
+ "- An abstract hand can be represented as a string of suits, for example `'rrBrbr'` is a 6-card hand. \n",
+ "- The function `deals(N)` will return a [discrete probability distribution](https://en.wikipedia.org/wiki/Probability_distribution) of possible abstract hands of length *N*.\n",
+ " - For example, `deals(6)` might include the entry `{'rrBrbr': 0.00019}`.\n",
+ " - With concrete hands, every hand has the same probability, and every card is equally likely to be the next card dealt. But with abstract hands, the probability of the next suit depends on how many cards of that suit have already been dealt. If I've already dealt the 12 cards `'rrrrrrrrrrrr'`, then the probability of the next card being an `'r'` is 1/40, and the probability of it being a `'b'` is 13/40. So as I build up the abstract hands, I'll need to keep track of how many cards of each suit I've already dealt and how many remain.\n",
+ "- I'll use `fractions.Fraction` to get exact arithmetic and `functools.cache` to avoid repeated computations."
]
},
{
@@ -53,19 +54,31 @@
"source": [
"from fractions import Fraction\n",
"from functools import cache\n",
- "from typing import Iterable\n",
- "\n",
- "type Hand = str\n",
- "type Probability = Fraction | float\n",
+ "from itertools import combinations\n",
"\n",
+ "type Hand = str # A hand is a string of card suits, like 'rrBBrB'\n",
+ "type Probability = Fraction"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {
+ "collapsed": false,
+ "jupyter": {
+ "outputs_hidden": false
+ }
+ },
+ "outputs": [],
+ "source": [
"suits = 'rbRB'\n",
"\n",
"@cache\n",
"def deals(N: int) -> dict[Hand, Probability]:\n",
- " \"\"\"A dict of {hand: probability} for all hands of N cards.\"\"\"\n",
- " if N == 0:\n",
+ " \"\"\"A probability distribution of {hand: probability} for all abstract hands of N cards.\"\"\"\n",
+ " if N == 0: # There is only one zero-card hand: the empty hand.\n",
" return {'': Fraction(1)}\n",
- " else:\n",
+ " else: # Every way of adding one card to (N-1)-card hands\n",
" P = deals(N - 1)\n",
" return {hand + suit: P[hand] * (13 - hand.count(suit)) / (52 - len(hand))\n",
" for hand in P\n",
@@ -74,7 +87,7 @@
},
{
"cell_type": "code",
- "execution_count": 2,
+ "execution_count": 3,
"metadata": {},
"outputs": [
{
@@ -86,7 +99,7 @@
" 'B': Fraction(1, 4)}"
]
},
- "execution_count": 2,
+ "execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
@@ -97,7 +110,7 @@
},
{
"cell_type": "code",
- "execution_count": 3,
+ "execution_count": 4,
"metadata": {
"collapsed": false,
"jupyter": {
@@ -126,7 +139,7 @@
" 'BB': Fraction(1, 17)}"
]
},
- "execution_count": 3,
+ "execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
@@ -139,16 +152,16 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "Is that right? Yes it is. For `deals(1)`, each suit has probability 1/4. For `deals(2)`, the probability of `'BB'` is 1/17, beause the probability of the first `'B'` is 1/4, and when we deal the second card, one `'B'` is gone, so the probability is 12/51, so that simplifies to 1/4 × 12/51 = 1/17. The probability of `'BR'` is 1/4 × 13/51 = 13/204.\n",
+ "Is that right? Yes it is. For `deals(1)`, each suit has probability 1/4. For `deals(2)`, the probability of `'BB'` is 1/17, beause the probability of the first `'B'` is 1/4, and when we deal the second card, one `'B'` is gone, so the probability is 12/51, and the product is 1/4 × 12/51 = 1/17. The probability of `'BR'` is 1/4 × 13/51 = 13/204.\n",
"\n",
"# More Abstraction: Collapsed Hands\n",
"\n",
- "Now for a second abstraction: an abstract hand can be *collapsed* by replacing a run of cards of the same suit with a single card, so that `'BBBBBrrrrBBBB'` collapses to `'BrB'`. We're interested in grouping cards of the same suit together, so any number of cards of the same suit is the same as a single card of that suit. "
+ "Now for a second abstraction: a hand can be **collapsed** by replacing a **run** of cards of the same suit with a single card, so that `'BBBBBrrrrBBBB'` collapses to `'BrB'`. We're interested in grouping cards of the same suit together, so for our purposes any number of cards of the same suit is the same as a single card of that suit. "
]
},
{
"cell_type": "code",
- "execution_count": 4,
+ "execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
@@ -161,7 +174,7 @@
},
{
"cell_type": "code",
- "execution_count": 5,
+ "execution_count": 6,
"metadata": {
"collapsed": false,
"jupyter": {
@@ -177,38 +190,9 @@
"cell_type": "markdown",
"metadata": {},
"source": [
- "# Properly Ordered Hands\n",
+ "# Properly Organized Hands\n",
"\n",
- "A hand is considered properly `ordered` if *\"the cards of a given suit are grouped together and, if possible, such that no suited groups of the same color are adjacent.\"* I was initially confused about the meaning of *\"if possible\";* Matt Ginsberg confirmed it means *\"if it is possible to separate the colors in any number of moves\"*, and thus that the hand `'BBBbbb'` is properly ordered, because the suits are all together, and it is not possible to separate the two black suits, while `'BBBbbR'` is not properly ordered, because the red card could be inserted between the two black runs.\n",
- "\n",
- "In other words: a hand is properly ordered if and only if its collapsed hand is properly ordered, and a collapsed hand is properly ordered if each suit appears only once, and either all the colors are the same, or suits of the same color don't appear adjacent to each other."
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 6,
- "metadata": {
- "collapsed": false,
- "jupyter": {
- "outputs_hidden": false
- }
- },
- "outputs": [],
- "source": [
- "def ordered(hand: Hand) -> bool:\n",
- " \"\"\"Properly ordered if each suit run appears only once, and same color suits are not adjacent (or there is only one color).\"\"\"\n",
- " hand1 = collapse(hand)\n",
- " return once_each(hand1) and (len(set(colors(hand1))) == 1 or not adjacent_colors(hand1))\n",
- "\n",
- "colors = str.lower # The color of both 'B' and 'b' is 'b'; the color of both 'R' and 'r' is 'r'\n",
- " \n",
- "def adjacent_colors(hand: Hand) -> bool: \n",
- " \"\"\"Do two suits of the same color appear next to each other in a hand?\"\"\"\n",
- " return 'bb' in colors(hand) or 'rr' in colors(hand)\n",
- "\n",
- "def once_each(hand: CollapsedHand) -> bool: \n",
- " \"\"\"Do all the suits in a collapsed hand appear just once each?\"\"\"\n",
- " return len(hand) == len(set(hand))"
+ "A hand is considered properly organized if *\"(1) the cards of a given suit are grouped together and (2) the suits alternate color,\"* with the provision that *\"if the cards are all the same color, they don't have to alternate.\"* In other words: a hand is properly organized if and only if its collapsed hand is properly organized, and a collapsed hand is properly organized if each suit appears only once, and the suits are properly alternating: either all the colors are the same, or suits of the same color don't appear adjacent to each other."
]
},
{
@@ -222,23 +206,30 @@
},
"outputs": [],
"source": [
- "assert ordered('BBBbbb')\n",
- "assert ordered('BBBRbb')\n",
- "assert not ordered('BBBbbR') \n",
- "assert not ordered('BBBrBB') "
+ "def organized(hand: Hand) -> bool:\n",
+ " \"\"\"Properly organized if each suit appears only once, and suits alternate colors.\"\"\"\n",
+ " hand1 = collapse(hand)\n",
+ " return once_each(hand1) and alternating_colors(hand1)\n",
+ "\n",
+ "colors = str.lower # e.g. colors('RRbbrrBB') == 'rrbbrrbb'\n",
+ " \n",
+ "def alternating_colors(hand: CollapsedHand) -> bool: \n",
+ " \"\"\"Do the colors of suits in the hand alternate (or is there only one color)?\"\"\"\n",
+ " C = colors(hand)\n",
+ " return ('bb' not in C and 'rr' not in C) or len(set(C)) == 1\n",
+ "\n",
+ "def once_each(hand: CollapsedHand) -> bool: \n",
+ " \"\"\"Do all the suits in a collapsed hand appear just once each?\"\"\"\n",
+ " return len(hand) == len(set(hand))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "# Moving Cards to Make a Collapsed Hand Ordered\n",
+ "# Moving Cards\n",
"\n",
- "I'll say that a hand is `orderable` if any of the possible `moves` of a block of consecutive cards makes the hand `ordered`. It is more efficient to do this on collapsed hands. I'll throw a `cache` onto `orderable` so that it won't have to repeat computations.\n",
- "\n",
- "To find all possible `moves`, consider every possible block of cards within a hand, and every way of placing the block into any position in the rest of the cards.\n",
- "\n",
- "I'll define `orderable_probability(N)` to give the probability that a random *N*-card hand is orderable. "
+ "A hand is organizable if it is already organized or if any of the possible moves of a block of consecutive cards makes the hand organized. I'll throw a `@cache` onto `organizable` so that it won't have to repeat computations."
]
},
{
@@ -248,38 +239,61 @@
"outputs": [],
"source": [
"@cache\n",
- "def orderable(hand: CollapsedHand) -> bool: \n",
- " \"\"\"Can this collapsed hand be put into proper order in one move?\"\"\"\n",
- " return any(ordered(new_seq) for new_seq in moves(hand))\n",
- "\n",
- "def moves(hand: Hand) -> Iterable[Hand]:\n",
- " \"\"\"All ways of moving a block of cards to get a new hand.\"\"\"\n",
- " N = len(hand)\n",
- " for i in range(N):\n",
- " for j in range(i + 1, N + 1):\n",
- " block = hand[i:j]\n",
- " rest = hand[:i] + hand[j:]\n",
- " for k in range(len(rest) + 1):\n",
- " yield rest[:k] + block + rest[k:]\n",
- "\n",
- "def orderable_probability(N: int) -> Probability:\n",
- " \"\"\"What's the probability that an N-card hand is orderable?\"\"\"\n",
- " P = deals(N)\n",
- " return sum(P[hand] for hand in P if orderable(collapse(hand)))"
+ "def organizable(hand: CollapsedHand) -> bool: \n",
+ " \"\"\"Can this collapsed hand be put into proper organize in one move or less?\"\"\"\n",
+ " return organized(hand) or any(map(organized, moves(hand)))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "# First Answer\n",
+ "To find all possible moves, consider every possible start and end position of a block of cards to move, and every possible position to move it too. I'll do that by considering every combination of three index positions (i, j, k) in the hand, with (i < j < k). At first I thought that I would need code for two kinds of moves: moving a block of cards to the right or to the left. But then I realized that if we swap hand[i:j] with hand[j:k], that covers all possible moves: either moving hand[i:j] to the right, or hand[j:k] to the left."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def moves(hand: Hand) -> set[Hand]:\n",
+ " \"\"\"All ways of moving a block of cards to get a new hand.\"\"\"\n",
+ " return {hand[:i] + hand[j:k] + hand[i:j] + hand[k:] # swap hand[i:j] with hand[j:k]\n",
+ " for (i, j, k) in combinations(range(len(hand) + 1), 3)}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "I'll define `organizable_probability(N)` to give the probability that a random *N*-card hand is organizable. "
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def organizable_probability(N: int) -> Probability:\n",
+ " \"\"\"What's the probability that an N-card hand is organizable?\"\"\"\n",
+ " P = deals(N)\n",
+ " return sum(P[hand] for hand in P if organizable(collapse(hand)))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Answers for up to 6-Card Hands\n",
"\n",
"Here's the answer for 6 cards:"
]
},
{
"cell_type": "code",
- "execution_count": 9,
+ "execution_count": 11,
"metadata": {
"collapsed": false,
"jupyter": {
@@ -293,99 +307,20 @@
"Fraction(51083, 83895)"
]
},
- "execution_count": 9,
+ "execution_count": 11,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
- "orderable_probability(6) "
+ "organizable_probability(6) "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "And an easier-to-read answer for everything up to 6 cards:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 10,
- "metadata": {
- "collapsed": false,
- "jupyter": {
- "outputs_hidden": false
- }
- },
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- " 1: 100.000% or 1\n",
- " 2: 100.000% or 1\n",
- " 3: 100.000% or 1\n",
- " 4: 100.000% or 1\n",
- " 5: 85.242% or 213019/249900\n",
- " 6: 60.889% or 51083/83895\n"
- ]
- }
- ],
- "source": [
- "def report(Ns: range) -> None:\n",
- " \"\"\"Show the probability of orderability, for each N in Ns.\"\"\"\n",
- " for N in Ns:\n",
- " P = orderable_probability(N)\n",
- " print('{:2}: {:8.3%} or {}'.format(N, float(P), P))\n",
- " \n",
- "report(range(1, 7))"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "Let's make sure that each `deals(N)` covers everything: that I got all 4N hands, and that the probabilities sum to 1:"
- ]
- },
- {
- "cell_type": "code",
- "execution_count": 11,
- "metadata": {
- "collapsed": false,
- "jupyter": {
- "outputs_hidden": false
- }
- },
- "outputs": [],
- "source": [
- "for N in range(7):\n",
- " assert len(deals(N)) == 4 ** N\n",
- " assert sum(deals(N).values()) == 1"
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "# Getting to 13-Card Hands\n",
- "\n",
- "So far so good, but if we want to get to 13-card hands, we would have to handle 413 = 67,108,864 `deals`, which would take multiple minutes. But I discovered two key properties that can speed things up:\n",
- "\n",
- "1. **An orderable collapsed hand can have at most 7 characters.** We know that a properly ordered CollapsedHand can have at most 4 characters. But a move can reduce the number of characters by at most 3: one can be reduced when we remove the block of cards (if the cards on either side of the block are the same), and up to two more can be reduced when we re-insert the block (if the left and/or right ends of the block match the surrounding suits). Here's an example of moving a block (in parens) to reduce the number of runs from 6 to 3:\n",
- "\n",
- " bRB(bR)B => b(bR)RBB = bRB\n",
- " \n",
- "3. **Adding cards to the end of an unorderable collapsed hand can't make it orderable.** To show that, take an unordered CollapsedHand, and see what happens if you take the extra suit and insert it anywhere in the CollapsedHand. If the CollapsedHand was unordered because it repeats a suit, adding a suit can't fix that. If it was unordered because suits of the same color are adjacent, then adding a suit of the other color *could* fix that: `'bBR'` could be fixed by inserting a `'r'` to get `'brBR'`. But here's the catch: `'bBR'` is not unorderable. And if we are going to insert a new suit between two others, that means that the original CollapsedHand must have had at most three suits (because when we add one, we can't get more than four suits in an ordered CollapsedHand), and *every* three-suit CollapsedHand is orderable. "
- ]
- },
- {
- "cell_type": "markdown",
- "metadata": {},
- "source": [
- "I'll define `orderable_deals(N)` to return a {hand: probability\\} dict of just the orderable hands, and\n",
- "I'll redefine `orderable` and `orderable_probability` to take advantage of this."
+ "And an easier-to-read format for everything up to 6 cards:"
]
},
{
@@ -397,28 +332,107 @@
"outputs_hidden": false
}
},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Probability that an N-card hand is organizable:\n",
+ " 0: 100.00% or 1\n",
+ " 1: 100.00% or 1\n",
+ " 2: 100.00% or 1\n",
+ " 3: 100.00% or 1\n",
+ " 4: 100.00% or 1\n",
+ " 5: 85.24% or 213019/249900\n",
+ " 6: 60.89% or 51083/83895\n"
+ ]
+ }
+ ],
+ "source": [
+ "def report(nums: range) -> None:\n",
+ " \"\"\"Show the probability that an N-card hand is organizable, for each N in nums.\"\"\"\n",
+ " print('Probability that an N-card hand is organizable:')\n",
+ " for N in nums:\n",
+ " P = organizable_probability(N)\n",
+ " print(f'{N:2}: {float(P):7.2%} or {P}')\n",
+ " \n",
+ "report(range(7))"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Getting to 13-Card Hands\n",
+ "\n",
+ "So far so good, but 413 = 67,108,864 is a big number; `organizable_probability(13)` would take several minutes to run. But I discovered three key properties that can speed things up. These are genral properties that appear in many combinatoric problems:\n",
+ "\n",
+ "- **Upper bounds:** We know that a properly organized collapsed hand can have at most 4 characters. But a move can reduce the number of characters by at most 3: one can be reduced when we remove the block of cards (if the cards on either side of the block are the same), and up to two more can be reduced when we re-insert the block (if the left and/or right ends of the block match the surrounding suits). That means that the upper bound on the length of an organizable collapsed hand is 7 characters. Here's an example of moving a block (in parens) to reduce the number of runs from 7 to 4:\n",
+ "\n",
+ "|dealt hand|block to move|result of move|properly organized?|\n",
+ "|---|---|---|---|\n",
+ "|♥️♣️♦️♠️♥️♣️♠️|♥️♣️♦️♠️(♥️♣️)♠️|♥️(♥️♣️)♣️♦️♠️♠️|yes, collapses to ♥️♣️♦️♠️|\n",
+ "\n",
+ " \n",
+ "- **Monotonicity:** Adding an additional card to an unorganizable collapsed hand can't make it organizable. To see that, take an unorganized collapsed hand, and see what happens if you take the extra card and insert it anywhere in the CollapsedHand. If the CollapsedHand was unorganized because it repeats a suit, adding a suit can't fix that. If it was unorganized because suits of the same color are adjacent, then adding a suit of the other color *could* fix that: `'bBR'` could be fixed by adding an 'r' at the end and moving it to get `'brBR'`. But here's the catch: `'bBR'` is not unorganizable. And if we are going to insert a new suit between two others, that means that the original CollapsedHand must have had at most three suits (because when we add one, we can't get more than four suits in an organized CollapsedHand), and *every* three-suit CollapsedHand is organizable.\n",
+ "\n",
+ "- **Symmetry:** Each of the four suits plays the same role in the game; they are symmetric. Thus, for the first card in a hand, we can arbitrarily choose one suit, say 'B', and not consider any of the other suits. Starting with any of the other three suits would give us the same probability, so by only considering 'B' as the first card we get the same answer with 1/4 the computation.\n",
+ "\n",
+ "The property of monotonicity means that we can use a strategy where we only keep track of the organizable hands, (not all possible hands), and we stop building onto a hand when it is unorganizable. I'll define `organizable_deals(N)` to return a {hand: probability} dict of just the organizable hands:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "metadata": {
+ "collapsed": false,
+ "jupyter": {
+ "outputs_hidden": false
+ }
+ },
"outputs": [],
"source": [
"@cache\n",
- "def orderable_deals(N: int) -> dict[Hand, Probability]:\n",
- " \"\"\"A dict of {hand: probability} for all orderable hands of length N.\"\"\"\n",
- " if N == 0:\n",
+ "def organizable_deals(N: int) -> dict[Hand, Probability]:\n",
+ " \"\"\"A dict of {hand: probability} for all organizable hands of length N.\"\"\"\n",
+ " if N == 0: # There is only one zero-card hand: the empty hand.\n",
" return {'': Fraction(1)}\n",
- " else:\n",
- " P = orderable_deals(N - 1)\n",
+ " elif N == 1: # Use symmetry; only consider one suit for first card\n",
+ " return {'B': Fraction(1)}\n",
+ " else: # Every way of adding one card to (N-1)-card hands to make organizable hands\n",
+ " P = organizable_deals(N - 1)\n",
" return {hand + suit: P[hand] * (13 - hand.count(suit)) / (52 - len(hand))\n",
" for hand in P\n",
" for suit in suits\n",
- " if orderable(collapse(hand + suit))}\n",
- " \n",
+ " if organizable(collapse(hand + suit))}"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Now I'll redefine `organizable` to take advantage of the 7-character upper bound, and `organizable_probability` to quickly sum the probabilities of the organizable deals:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "metadata": {
+ "collapsed": false,
+ "jupyter": {
+ "outputs_hidden": false
+ }
+ },
+ "outputs": [],
+ "source": [
"@cache\n",
- "def orderable(hand: CollapsedHand) -> bool: \n",
- " \"\"\"Can this collapsed hand be put into proper order in one move?\"\"\"\n",
- " return len(hand) <= 7 and any(ordered(move) for move in moves(hand))\n",
+ "def organizable(hand: CollapsedHand) -> bool: \n",
+ " \"\"\"Can this collapsed hand be put into proper organize in one move?\"\"\"\n",
+ " return len(hand) <= 7 and (organized(hand) or any(map(organized, moves(hand))))\n",
"\n",
- "def orderable_probability(N: int) -> Probability:\n",
- " \"\"\"What's the probability that an N-card hand is orderable?\"\"\"\n",
- " return sum(orderable_deals(N).values())"
+ "def organizable_probability(N: int) -> Probability:\n",
+ " \"\"\"What's the probability that an N-card hand is organizable?\"\"\"\n",
+ " return sum(organizable_deals(N).values())"
]
},
{
@@ -432,7 +446,7 @@
},
{
"cell_type": "code",
- "execution_count": 13,
+ "execution_count": 15,
"metadata": {
"collapsed": false,
"jupyter": {
@@ -444,22 +458,23 @@
"name": "stdout",
"output_type": "stream",
"text": [
- " 0: 100.000% or 1\n",
- " 1: 100.000% or 1\n",
- " 2: 100.000% or 1\n",
- " 3: 100.000% or 1\n",
- " 4: 100.000% or 1\n",
- " 5: 85.242% or 213019/249900\n",
- " 6: 60.889% or 51083/83895\n",
- " 7: 37.321% or 33606799/90047300\n",
- " 8: 20.185% or 29210911/144718875\n",
- " 9: 9.861% or 133194539/1350709500\n",
- "10: 4.432% or 367755247/8297215500\n",
- "11: 1.859% or 22673450197/1219690678500\n",
- "12: 0.736% or 1751664923/238130084850\n",
- "13: 0.277% or 30785713171/11112737293000\n",
- "CPU times: user 2.18 s, sys: 33.6 ms, total: 2.22 s\n",
- "Wall time: 2.22 s\n"
+ "Probability that an N-card hand is organizable:\n",
+ " 0: 100.00% or 1\n",
+ " 1: 100.00% or 1\n",
+ " 2: 100.00% or 1\n",
+ " 3: 100.00% or 1\n",
+ " 4: 100.00% or 1\n",
+ " 5: 85.24% or 213019/249900\n",
+ " 6: 60.89% or 51083/83895\n",
+ " 7: 37.32% or 33606799/90047300\n",
+ " 8: 20.18% or 29210911/144718875\n",
+ " 9: 9.86% or 133194539/1350709500\n",
+ "10: 4.43% or 367755247/8297215500\n",
+ "11: 1.86% or 22673450197/1219690678500\n",
+ "12: 0.74% or 1751664923/238130084850\n",
+ "13: 0.28% or 30785713171/11112737293000\n",
+ "CPU times: user 491 ms, sys: 6.36 ms, total: 497 ms\n",
+ "Wall time: 497 ms\n"
]
}
],
@@ -471,14 +486,16 @@
"cell_type": "markdown",
"metadata": {},
"source": [
+ "Not bad. We got exact answers for all *N* up to 13 in a half second total run time.\n",
+ "\n",
"# Inspecting the Cache\n",
"\n",
- "Let's look at the cache for `orderable(Hand)`:"
+ "Let's look at the cache for `organizable(hand)`:"
]
},
{
"cell_type": "code",
- "execution_count": 14,
+ "execution_count": 16,
"metadata": {
"collapsed": false,
"jupyter": {
@@ -489,25 +506,59 @@
{
"data": {
"text/plain": [
- "CacheInfo(hits=1438512, misses=1540, maxsize=None, currsize=1540)"
+ "CacheInfo(hits=359627, misses=385, maxsize=None, currsize=385)"
]
},
- "execution_count": 14,
+ "execution_count": 16,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
- "orderable.cache_info()"
+ "organizable.cache_info()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
- "So `hits` say we looked at 1.4 million hands, but only 1540 were unique: distinct collapsed hands. And once we hit *N* = 7, we've seen all the collapsed hands we're ever going to see. From *N* = 8 and up, almost all the computation goes into computing the probability of each hand, and collapsing each hand, not into deciding orderability.\n",
+ "So `hhits=359627` menas that that's the number of hands we considered, but only 385 were unique distinct collapsed hands. And once we hit *N* = 7, we've seen all the collapsed hands we're ever going to see. From *N* = 8 and up, almost all the computation goes into computing the probability of each hand, and collapsing each hand, not into deciding which are organizeable.\n",
"\n",
- "We also save a lot of space in the `deals(N)` caches. Instead of storing all 413 hands for `deals(13)`, the `report` above says that just 0.277% of the hands are orderable, so we reduced the storage requirements by a factor of 360.\n",
+ "Now let's look at the number of hands considered by `deals(N)` compared to `organizable_deals(N)`:"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 17,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "import matplotlib.pyplot as plt\n",
+ "\n",
+ "Ns = range(9)\n",
+ "plt.plot(Ns, [len(deals(N)) for N in Ns], 'ro-', label='deals(N)')\n",
+ "Ns = range(14)\n",
+ "plt.plot(Ns, [len(organizable_deals(N)) for N in Ns], 'bo-', label='organizable_deals(N)')\n",
+ "plt.grid(True)\n",
+ "plt.legend();"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "The number of deals grows with 4*N*, but the number of organizable deals less than doubles each step; it is growing slower than 2*N*.\n",
"\n",
"# Unit Tests\n",
"\n",
@@ -516,7 +567,7 @@
},
{
"cell_type": "code",
- "execution_count": 15,
+ "execution_count": 18,
"metadata": {
"collapsed": false,
"jupyter": {
@@ -530,7 +581,7 @@
"True"
]
},
- "execution_count": 15,
+ "execution_count": 18,
"metadata": {},
"output_type": "execute_result"
}
@@ -538,27 +589,39 @@
"source": [
"def test():\n",
" assert deals(1) == {'B': 1/4, 'R': 1/4, 'b': 1/4, 'r': 1/4}\n",
- " assert ordered('BBBBBrrrrBBBB') is False\n",
- " assert ordered('BBBBBrrrrRRRR') is False\n",
- " assert ordered('BBBbbr') is False # Bb\n",
- " assert ordered('BBBbbrB') is False # two B's\n",
- " assert ordered('BBBbbb') \n",
- " assert ordered('BBBbbbB') is False # two B's\n",
- " assert ordered('BBBBBrrrrbbbb')\n",
+ " for N in range(7):\n",
+ " assert len(deals(N)) == 4 ** N # A deal has all 4**N abstract hands\n",
+ " assert sum(deals(N).values()) == 1 # The sum of the probabilities must be 1.\n",
+ " assert organizable_deals(1) == {'B': Fraction(1, 1)}\n",
+ " assert organized('BBBBBrrrrBBBB') is False\n",
+ " assert organized('BBBBBrrrrRRRR') is False\n",
+ " assert organized('BBBbbr') is False # Bb\n",
+ " assert organized('BBBbbrB') is False # two B's\n",
+ " assert organized('BBBbbb') \n",
+ " assert organized('BBBbbbB') is False # two B's\n",
+ " assert organized('BBBBBrrrrbbbb')\n",
+ " assert organized('BBBbbb')\n",
+ " assert organized('BBBbbb')\n",
+ " assert organized('BBBRbb')\n",
+ " assert organized('BBBbbR') is False\n",
+ " assert organized('BBBrBB') is False\n",
+ " assert organized('BBrrBR') is False\n",
" assert colors('BBBBBrrrrbbbb') == 'bbbbbrrrrbbbb'\n",
" assert once_each('Bb')\n",
" assert once_each('BbRr')\n",
" assert once_each('BbRB') is False\n",
- " assert adjacent_colors('BbR')\n",
- " assert adjacent_colors('Brb') is False\n",
+ " assert alternating_colors('Brb')\n",
+ " assert alternating_colors('BbR') is False\n",
+ " assert alternating_colors('BBb')\n",
" assert collapse('BBBBBrrrrBBBB') == 'BrB'\n",
" assert collapse('brBBrrRR') == 'brBrR'\n",
" assert collapse('bbbbBBBrrr') == 'bBr'\n",
- " assert set(moves('bRb')) == {'Rbb', 'bRb', 'bbR'}\n",
- " assert set(moves('bRB')) == {'BbR', 'RBb', 'RbB', 'bBR', 'bRB'}\n",
- " assert orderable('bBr') # move 'r' between 'bB'\n",
- " assert orderable('bBrbRBr') # move 'bRB' after first 'b' to get 'bbRBBrr'\n",
- " assert orderable('bBrbRBrb') is False\n",
+ " assert moves('bRb') == {'Rbb', 'bbR'}\n",
+ " assert moves('bRB') == {'BbR', 'RBb', 'RbB', 'bBR'}\n",
+ " assert organizable('bBr') # move 'r' between 'bB'\n",
+ " assert organizable('bBrbRBr') # move 'bRB' after first 'b' to get 'bbRBBrr'\n",
+ " assert organizable('bBrbRBrb') is False\n",
+ " \n",
" return True\n",
"\n",
"test()"