diff --git a/ipynb/TwelveBalls.ipynb b/ipynb/TwelveBalls.ipynb index a0f61c0..7477025 100644 --- a/ipynb/TwelveBalls.ipynb +++ b/ipynb/TwelveBalls.ipynb @@ -6,24 +6,23 @@ "source": [ "
Peter Norvig
2012; updated 18 August 2019
\n", "\n", - "# Twelve Balls and a Balance Scale\n", + "# Weighing Twelve Balls on a Balance Scale\n", "\n", "> *You are given twelve identical-looking balls and a two-sided scale. One of the balls is of a different weight, although you don't know whether it's lighter or heavier. How can you use just three weighings of the scale to determine not only what the different ball is, but also whether it's lighter or heavier?*\n", "\n", "This is a traditional brain-teaser puzzle, meant to be solved with paper and pencil. \n", - "But I want to not just solve this specific puzzle, but show how to write a program that can solve related puzzles where you can vary (a) the number of balls, (b) the number of weighings allowed, and (c) whether the odd ball might be heavier, lighter, or either. (I originally solved this in 2012, but am republishing it here in revised form because the problem was mentioned in the [538 Riddler](https://fivethirtyeight.com/features/which-billiard-ball-is-rigged/) for 16 August 2019.)\n", + "But I want to solve not just this specific puzzle, but related puzzles where you can vary (a) the number of balls, (b) the number of weighings allowed, and (c) whether the odd ball might be heavier, lighter, or either. For that I'll ned a program. (I originally solved wrote this program in 2012, but am republishing it here in revised form because the problem was mentioned in the [538 Riddler](https://fivethirtyeight.com/features/which-billiard-ball-is-rigged/) for 16 August 2019.)\n", "\n", "# Design\n", "\n", "Here are the concepts I'm dealing with:\n", "\n", - "- **balls**: In the general case I have N balls; for example I'll represent N = 3 balls with `[1, 2, 3]`.\n", - "- **oddballs**: Exactly one of the balls is **odd** in its weight; if ball N is heavier, I'll represent that as +N; if it is lighter, as -N. With N = 3, I will represent the collection of possible oddballs as `[+1, -1, +2, -2, +3]`.\n", - "- **puzzle**: A specific puzzle declares the number of balls, the maximum number of weighings allowed, and the oddballs. \n", - "- **weighing**: I can weigh a collection of balls on the left versus a collection on the right, and the result (given that we know the oddball) will be that the left side is greater than, equal to, or less than the right in weight.\n", - "I'll denote that with the call `weigh(L, R, oddball)`, which returns a string, `'gt'`, `'eq'`, or `'lt'`.\n", + "- **balls**: In the general case I have N balls. I'll represent with a list like `[1, 2, 3]` for N = 3.\n", + "- **oddballs**: Exactly one of the balls is **odd** in its weight; if ball N is heavier, I'll represent that as +N; if it is lighter, as -N. With N = 3, I will represent the list of possible oddballs as `[+1, -1, +2, -2, +3, -3]`.\n", + "- **puzzle**: A specific puzzle declares the number of balls, the maximum number of weighings allowed, and the possible oddballs. \n", + "- **weighing**: I can weigh a collection of balls on the left versus a collection on the right, and the result will be that the left side is greater than, equal to, or less than the right in weight. I'll denote that with the call `weigh(L, R, oddball)`, which returns a string, `'gt'`, `'eq'`, or `'lt'`.\n", "- **weight**: I'll arbitrarily say that a normal ball weighs 100, a lighter ball 99, and a heavier ball 101.\n", - "- **solution**: A particular puzzle states the number of balls, the number of weighings allowed, and whether the odd ball can be lighter, heavier, or either. For example, the original puzzle is solved with a call to `solve(12, 3, {+1, -1})`. The solution is a **strategy tree**.\n", + "- **solution**: Given a puzzle, a solution is a data structure that can correctly states the number of balls, the number of weighings allowed, and whether the odd ball can be lighter, heavier, or either. For example, the original puzzle is solved with a call to `solve(12, 3, {+1, -1})`. The solution is a **strategy tree**.\n", "- **strategy tree**: a tree where each node is either a leaf node consisting of an *oddball* integer, or is an interior node with 5 components: the balls to be placed on the left and right side of the scale, and a subtree for each of the three possible outcomes: `'gt'`, `'eq'`, or `'lt'`. The constructor `Tree(L, R, gt, eq, lt)` creates a tree, where `L` and `R` are collections of balls, and `gt`, `eq`, and `lt` are trees.\n", "- **following a path in a tree**: I'll use `follow(tree, oddball)` to say \"follow the path through the tree, at each weighing assuming the given oddball, and return the leaf node reached—the oddball that the tree predicts.\n", "- **valid tree**: a tree is a valid solution if no branch uses more than the allowable number of weighings, and if, for every possible oddball, following the path through the tree gives the correct oddball as the answer. We'll delay the discussion of how to find a valid tree until later.\n", @@ -31,7 +30,7 @@ "\n", "# Implementation\n", "\n", - "Let's start implementing:" + "Let's start implementing the concepts:" ] }, { @@ -45,6 +44,8 @@ "\n", "#### Types\n", "\n", + "Ball = Oddball = int\n", + "\n", "class Puzzle:\n", " \"Represent a specific ball-weighing puzzle.\"\n", " def __init__(self, N=12, weighings=3, oddities={+1, -1}):\n", @@ -55,8 +56,6 @@ " \n", "Tree = namedtuple('Tree', 'L, R, gt, eq, lt')\n", "\n", - "Oddball = int\n", - "\n", "#### Functions\n", " \n", "def weigh(L, R, oddball) -> str:\n", @@ -86,7 +85,8 @@ "def valid(tree, puzzle) -> bool:\n", " \"Does the strategy tree solve the puzzle correctly for all possible oddballs?\"\n", " return (depth(tree) <= puzzle.weighings and \n", - " all(follow(tree, oddball) == oddball for oddball in puzzle.oddballs))\n", + " all(follow(tree, oddball) == oddball \n", + " for oddball in puzzle.oddballs))\n", "\n", "def depth(tree) -> int:\n", " \"Maximum depth of a strategy tree.\"\n", @@ -98,7 +98,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Let's try out these functions:" + "Let's try out some of these functions:" ] }, { @@ -145,18 +145,58 @@ "weigh([1, 2], [3, 4], -5)" ] }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "100" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "weight(1, oddball=-5)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "99" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "weight(5, oddball=-5)" + ] + }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Strategy for Finding a Valid Tree\n", "\n", - "Now for the tricky part. We want to find a valid tree to solve a puzzle. The key idea is that a **weighing** gives us information by making a **partition** of the possible **oddballs**; subsequent subtrees can handle each of the partitions.\n" + "Now for the tricky part. We want to find a valid tree to solve a puzzle. The key idea is that a **weighing** gives us information by making a **partition** of the possible **oddballs** into entries for eaach of the three possible weighing results: `gt`, `eq`, or `lt`. Subsequent subtrees can handle each of the partitions.\n" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ @@ -172,12 +212,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "For example, with 12 balls, if we weigh balls 1 and 2 on the left versus 11 and 12 on the right, then there are four ways the left side can be greater than the right: either 1 or 2 is heavier or 11 or 12 is lighter. The opposite would lead to the left side being less than the right. And if any of balls 3 through 12 is either heavier or lighter, the result would be that the weighing is equal." + "For example, with 12 balls, if we weigh balls 1 and 2 on the left versus 11 and 12 on the right, then there are 4 ways the left side can be greater than the right: either 1 or 2 is heavier or 11 or 12 is lighter. Similarly there are 4 ways of getting a `lt` weighing result. The remaining 16 possible oddballs—balls 3 through 10 being either heavier or lighter—show up in the `eq` entry of the partition:" ] }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 7, "metadata": {}, "outputs": [ { @@ -188,7 +228,7 @@ " 'lt': [-1, -2, 11, 12]}" ] }, - "execution_count": 5, + "execution_count": 7, "metadata": {}, "output_type": "execute_result" } @@ -201,14 +241,14 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Given that the puzzle is to solve the problem in 3 weighings, we call this a **bad partition**, because the `'eq'` entry has 16 possibilities, which is too many to solve in the remaining 2 weighings. Each weighing can at best partition the possibilities into three equal groups. So with two remaining weighings, we can only handle up to 3 × 3 = 9 possible oddballs, not 16.\n", + "If this was the first weighing in our strategy tree, could we go on to solve the puzzle in 3 weighings? **No!** Any one weighing can at best partition the remaining possibilities into 3 equal entries. To solve the puzzle, we need every path in the tree to end up with only one possibility. So any two weighings can handle at most 3 × 3 = 9 possibilities; here we have 16, which is too many. We call this a **bad partition**.\n", "\n", - "The following is a **good partition** because each of the entries has 8 possibilities, and 8 is less than 9. (Note: being a good partition does not guarantee that the problem is solvable from there; but being a bad partition guarantees that it is not.)" + "The following is a **good partition** because each of the entries has 8 possibilities, and 8 is less than 9. (Note: being a good partition does not guarantee that the problem is solvable from there; but being a bad partition guarantees that it is not solvable.)" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 8, "metadata": {}, "outputs": [ { @@ -219,7 +259,7 @@ " 'lt': [-1, -2, -3, -4, 9, 10, 11, 12]}" ] }, - "execution_count": 6, + "execution_count": 8, "metadata": {}, "output_type": "execute_result" } @@ -232,45 +272,46 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "So now we have a viable approach to implementing `find_tree`, which is the core of `solve`:\n", + "So now we have a viable approach to implementing `find_tree`:\n", "\n", " - We call `find_tree(puzzle, oddballs, weighings)`. At the top level, the oddballs and number of weighings come from the puzzle. At recursive levels, we will reduce the number of oddball possibilities according to the partition, and the number of remaining weighings by 1 each time.\n", " - At each step we will randomly select two groups of balls, `L` and `R`, to be weighed.\n", " - We will then see what partition `L` and `R` gives us, and whether the partition is good or bad.\n", - " - We will adopt a **greedy** approach where we accept the first good partition.\n", + " - If the partition is bad, try another random selection of `L` and `R`.\n", + " - Use a **greedy** approach where we accept the first good partition.\n", " (If we don't find a good partition after 10,000 tries, we give up.)\n", " - Once we have a good partition, we recursively find a tree for each of the branches of the partition." ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ - "def find_tree(puzzle, oddballs, weighings):\n", + "def find_tree(puzzle, oddballs, weighings) -> Tree or Oddball:\n", " \"Find a strategy tree that covers all the oddballs in the given number of weighings.\"\n", " if len(oddballs) == 1:\n", " return oddballs[0] # One oddball possibility left; we're done\n", " elif len(oddballs) == 0 or weighings == 0:\n", " return 0 # No valid strategy or an impossible situation\n", " else:\n", - " L, R, part = good_partition(puzzle, oddballs, weighings - 1)\n", - " return Tree(L, R, **{r: find_tree(puzzle, part[r], weighings - 1) for r in part})\n", + " L, R, part = find_good_partition(puzzle, oddballs, weighings - 1)\n", + " subtrees = {r: find_tree(puzzle, part[r], weighings - 1) for r in part}\n", + " return Tree(L, R, **subtrees)\n", " \n", - "def good_partition(puzzle, oddballs, weighings):\n", - " \"Randomly pick L, R balls such that no partition entry has more than 3**weighings oddballs.\"\n", + "def find_good_partition(puzzle, oddballs, weighings) -> tuple:\n", + " \"Return (L, R, partition) such that no partition entry has more than 3**weighings oddballs.\"\n", " for _ in range(10000): \n", " L, R = random_LR(puzzle, oddballs)\n", " part = partition(L, R, oddballs)\n", - " good = all(len(entry) <= 3 ** weighings for entry in part.values())\n", - " if good:\n", + " if all(len(entry) <= 3 ** weighings for entry in part.values()):\n", " return L, R, part\n", - " raise ValueError('good_partition not found')\n", + " raise ValueError('find_good_partition: not found')\n", " \n", "def random_LR(puzzle, oddballs):\n", " \"Random choice of balls for L and R side.\"\n", - " # Pick a random number of balls, B, then pick B balls for each side.\n", + " # Pick a random number of balls, B, then randomly pick B balls for each side.\n", " B = random.choice(range(1, (len(puzzle.balls) - 1) // 3 + 2))\n", " random.shuffle(puzzle.balls) \n", " return sorted(puzzle.balls[:B]), sorted(puzzle.balls[-B:])" @@ -280,31 +321,31 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Here we see that `good_partition` does its job:" + "Here we see that `find_good_partition` does its job:" ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "([5, 10, 11, 12],\n", - " [1, 2, 4, 8],\n", - " {'gt': [-1, -2, -4, 5, -8, 10, 11, 12],\n", - " 'eq': [3, -3, 6, -6, 7, -7, 9, -9],\n", - " 'lt': [1, 2, 4, -5, 8, -10, -11, -12]})" + "([2, 3, 7, 9],\n", + " [1, 6, 10, 11],\n", + " {'gt': [-1, 2, 3, -6, 7, 9, -10, -11],\n", + " 'eq': [4, -4, 5, -5, 8, -8, 12, -12],\n", + " 'lt': [1, -2, -3, 6, -7, -9, 10, 11]})" ] }, - "execution_count": 8, + "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "good_partition(p12, p12.oddballs, 2)" + "find_good_partition(p12, p12.oddballs, 2)" ] }, { @@ -316,47 +357,49 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "([3, 4, 9, 12],\n", - " [1, 5, 8, 11],\n", - " {'gt': [-1, 3, 4, -5, -8, 9, -11, 12],\n", - " 'eq': [2, -2, 6, -6, 7, -7, 10, -10],\n", - " 'lt': [1, -3, -4, 5, 8, -9, 11, -12]})" + "([1, 2, 8, 11],\n", + " [3, 4, 7, 10],\n", + " {'gt': [1, 2, -3, -4, -7, 8, -10, 11],\n", + " 'eq': [5, -5, 6, -6, 9, -9, 12, -12],\n", + " 'lt': [-1, -2, 3, 4, 7, -8, 10, -11]})" ] }, - "execution_count": 9, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "good_partition(p12, p12.oddballs, 2)" + "find_good_partition(p12, p12.oddballs, 2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ + "# Solving Some Puzzles\n", + "\n", "Now we're ready to solve puzzles!" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "Tree(L=[2, 3, 6, 8], R=[1, 4, 9, 10], gt=Tree(L=[6, 8, 10], R=[2, 3, 11], gt=Tree(L=[3, 6, 10], R=[1, 2, 4], gt=6, eq=8, lt=0), eq=Tree(L=[9, 10], R=[4, 5], gt=-4, eq=-1, lt=-9), lt=Tree(L=[3, 7], R=[2, 5], gt=3, eq=-10, lt=2)), eq=Tree(L=[1, 5, 8, 12], R=[2, 4, 7, 9], gt=Tree(L=[5, 8, 10], R=[1, 3, 12], gt=5, eq=-7, lt=12), eq=Tree(L=[4, 7, 12], R=[2, 10, 11], gt=-11, eq=0, lt=11), lt=Tree(L=[2, 5, 7], R=[1, 4, 9], gt=7, eq=-12, lt=-5)), lt=Tree(L=[3, 6, 10, 11], R=[2, 5, 7, 8], gt=Tree(L=[6, 8, 10], R=[1, 11, 12], gt=10, eq=-2, lt=-8), eq=Tree(L=[1, 5, 7, 11], R=[2, 3, 4, 12], gt=1, eq=9, lt=4), lt=Tree(L=[3, 8, 10], R=[2, 7, 12], gt=0, eq=-6, lt=-3)))" + "Tree(L=[4, 7, 10, 12], R=[3, 6, 8, 9], gt=Tree(L=[1, 4, 5, 7], R=[2, 6, 10, 12], gt=Tree(L=[7, 10], R=[3, 4], gt=7, eq=-6, lt=4), eq=Tree(L=[3, 5, 6, 10], R=[1, 4, 7, 9], gt=-9, eq=-8, lt=-3), lt=Tree(L=[3, 5, 6, 8], R=[2, 4, 10, 11], gt=0, eq=12, lt=10)), eq=Tree(L=[1, 3, 11, 12], R=[5, 6, 7, 9], gt=Tree(L=[2, 8, 12], R=[1, 4, 5], gt=-5, eq=11, lt=1), eq=Tree(L=[2, 4, 11, 12], R=[1, 3, 5, 10], gt=2, eq=0, lt=-2), lt=Tree(L=[1, 2, 6], R=[3, 9, 11], gt=-11, eq=5, lt=-1)), lt=Tree(L=[5, 7, 9, 10], R=[1, 2, 4, 12], gt=Tree(L=[3, 7, 8], R=[2, 9, 12], gt=-12, eq=-4, lt=9), eq=Tree(L=[1, 4, 8], R=[2, 6, 10], gt=8, eq=3, lt=6), lt=Tree(L=[12], R=[7], gt=-7, eq=-10, lt=0)))" ] }, - "execution_count": 10, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -369,53 +412,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "OK, that's hard to read—my bad. Let's look at an easier puzzle (3 balls in 2 weighings):" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "Tree(L=[3], R=[1], gt=Tree(L=[2], R=[3], gt=0, eq=-1, lt=3), eq=Tree(L=[3], R=[2], gt=-2, eq=0, lt=2), lt=Tree(L=[2], R=[3], gt=-3, eq=1, lt=0))" - ] - }, - "execution_count": 11, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "solve(Puzzle(3, 2))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You should be able to follow through that tree by hand, if you're careful. But let's make the trees easier to read by formatting them:" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "metadata": {}, - "outputs": [], - "source": [ - "def show(tree):\n", - " \"Print an indented tree.\"\n", - " print(pp(tree))\n", - " \n", - "def pp(tree, i=0, suffix=''):\n", - " \"Pretty, indented string of a strategy tree.\"\n", - " if isinstance(tree, Tree):\n", - " indent = '' if i == 0 else ('\\n' + \" \" * 5 * i)\n", - " return f'{indent}Tree({tree.L}, {tree.R}, {pp(tree.gt, i+1)}, {pp(tree.eq, i+1)}, {pp(tree.lt, i+1)})'\n", - " else:\n", - " return f'{tree:+d}'" + "OK, that's hard to read—my bad. Let's look at a much easier puzzle: 3 balls, 1 weighing allowed, and the odd ball can only be lighter:" ] }, { @@ -424,100 +421,53 @@ "metadata": {}, "outputs": [ { - "name": "stdout", - "output_type": "stream", - "text": [ - "Tree([2], [1], \n", - " Tree([2], [1], \n", - " Tree([3], [1], -1, +2, +0), +0, +0), \n", - " Tree([1], [3], -3, +0, +3), \n", - " Tree([3], [1], +0, -2, +1))\n" - ] + "data": { + "text/plain": [ + "Tree(L=[1], R=[3], gt=-3, eq=-2, lt=-1)" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" } ], "source": [ - "show(solve(Puzzle(3)))" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Tree([3, 6, 7, 9], [2, 5, 10, 12], \n", - " Tree([9, 10, 12], [2, 7, 11], \n", - " Tree([2], [1], +0, +9, -2), \n", - " Tree([4, 6, 9], [2, 3, 10], +6, -5, +3), \n", - " Tree([3, 10], [9, 12], -12, +7, -10)), \n", - " Tree([4, 5, 6, 11], [2, 8, 10, 12], \n", - " Tree([4, 8], [5, 10], +4, +11, -8), \n", - " Tree([1, 6, 10, 11], [3, 4, 7, 12], +1, +0, -1), \n", - " Tree([4, 10], [5, 11], -11, +8, -4)), \n", - " Tree([3, 4, 5, 7], [1, 8, 9, 12], \n", - " Tree([6], [5], +0, -9, +5), \n", - " Tree([1, 2, 6], [5, 8, 11], +2, +10, -6), \n", - " Tree([4, 8, 9], [1, 3, 12], -3, -7, +12)))\n" - ] - } - ], - "source": [ - "# Back to the original puzzle\n", - "show(solve(p12))" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Tree([6, 7, 11, 12], [2, 5, 8, 9], \n", - " Tree([2, 11, 12], [3, 6, 9], \n", - " Tree([1, 5, 12], [2, 3, 11], +12, -9, +11), \n", - " Tree([1, 5, 7, 9], [2, 3, 4, 11], +7, -8, -5), \n", - " Tree([1, 7, 9, 11], [3, 6, 8, 10], +0, -2, +6)), \n", - " Tree([2, 3, 6, 11], [1, 5, 8, 10], \n", - " Tree([1], [10], -10, +3, -1), \n", - " Tree([1, 4, 6, 8], [2, 10, 11, 12], +4, +0, -4), \n", - " Tree([5, 7, 12], [1, 2, 3], -3, +10, +1)), \n", - " Tree([3, 4, 5, 10], [2, 6, 8, 9], \n", - " Tree([3, 7, 11, 12], [1, 4, 5, 9], +0, -6, +5), \n", - " Tree([2, 3, 9, 11], [5, 8, 10, 12], -12, -7, -11), \n", - " Tree([6, 7, 9, 10], [3, 5, 8, 12], +9, +2, +8)))\n" - ] - } - ], - "source": [ - "# You get different solutions on different runs.\n", - "show(solve(p12))" + "tree = solve(Puzzle(3, 1, {-1}))\n", + "tree" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "We can solve larger puzzles with 4 weighings, but before showing that, let's do two things.\n", - "- Condense each line a bit by replacing `', '` with just `','`. This makes more fit on one line.\n", - "- At the top level, there's no sense randomly shuffling the balls; the only choice that matters is how many balls, `B`, to put on each side." + "This tree says you weigh one ball against another (leaving the third unweighed), and the three possible weighing results tell you which of the three balls is lighter.\n", + "\n", + "To handle bigger puzzles, let's make the trees easier to read by producing formatted, indented output. Also, note that at the top node of a tree, there's no sense randomly shuffling the balls; the only choice that matters is how many balls, `B`, to put on each side. I'll alter `random_LR` to do that; that should also make trees easier to understand." ] }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 14, "metadata": {}, "outputs": [], "source": [ - "def show(tree):\n", - " \"Print a condensed representation tree.\"\n", - " print(pp(tree).replace(', ', ','))\n", + "def do(puzzle):\n", + " \"Print the solution to the puzzle as indented text.\"\n", + " print(indented(solve(puzzle)))\n", " \n", + "def indented(tree, i=0, prefix=''):\n", + " \"Pretty, indented string of a strategy tree.\"\n", + " if isinstance(tree, Tree):\n", + " subtrees = f\"{indented(tree.gt, i+1, '>:')} {indented(tree.eq, i+1, '=:')} {indented(tree.lt, i+1, '<:')}\"\n", + " indent = '' if i == 0 else ('\\n' + \" \" * 5 * i)\n", + " return f'{indent}{prefix}({items(tree.L)} ⟘ {items(tree.R)}) ➔ {subtrees})'\n", + " elif tree == 0:\n", + " return f'{prefix}0'\n", + " else:\n", + " return f'{prefix}{tree:+d}'\n", + " \n", + "def items(collection): return '·'.join(map(str, collection))\n", + "\n", "def random_LR(puzzle, oddballs):\n", " \"Random choice of balls for L and R side.\"\n", " # Pick a random number of balls, B, then pick B balls for each side.\n", @@ -529,6 +479,45 @@ " return sorted(puzzle.balls[:B]), sorted(puzzle.balls[-B:])" ] }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(1 ⟘ 3) ➔ >:-3 =:-2 <:-1)\n" + ] + } + ], + "source": [ + "# 3 balls, 1 weighing, only lighter\n", + "do(Puzzle(3, 1, {-1}))" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(1 ⟘ 3) ➔ \n", + " >:(2 ⟘ 1) ➔ >:0 =:-3 <:+1) \n", + " =:(1 ⟘ 2) ➔ >:-2 =:0 <:+2) \n", + " <:(2 ⟘ 3) ➔ >:0 =:-1 <:+3))\n" + ] + } + ], + "source": [ + "# 3 balls, 2 weighings, lighter or heavier\n", + "do(Puzzle(3, 2))" + ] + }, { "cell_type": "code", "execution_count": 17, @@ -538,58 +527,25 @@ "name": "stdout", "output_type": "stream", "text": [ - "Tree([1,2,3,4,5,6,7,8,9,10,11,12,13],[27,28,29,30,31,32,33,34,35,36,37,38,39],\n", - " Tree([2,3,6,8,12,18,19,24,27,31,34,36,39],[7,11,13,15,16,20,22,25,26,28,30,33,37],\n", - " Tree([3,6,8,9,14,25,26,30,31,36,37,39],[1,2,4,5,10,13,17,18,19,22,23,29],\n", - " Tree([3,13,21,23,27,30,31,32,38],[1,2,4,6,9,14,25,37,39],+3,+8,+6),\n", - " Tree([1,7,9,11,16,23,26,28,30,39],[6,10,13,14,15,19,21,32,33,38],-33,+12,-28),\n", - " Tree([2,10,11,12,13,22,23,26,30,31,34],[3,7,14,18,19,21,24,27,29,38,39],+2,-37,-30)),\n", - " Tree([2,15,18,21,22,23,26,27,28,31,33,34,39],[1,4,10,12,16,17,19,25,29,30,35,37,38],\n", - " Tree([10,14,17,18,21,22,27,30,32,34,35],[1,5,7,9,12,13,19,24,28,33,38],-38,-29,-35),\n", - " Tree([2,7,9,10,18,23,26,27,28,31,36,38],[3,5,6,11,14,16,19,21,24,25,35,39],+9,-32,+5),\n", - " Tree([1,16,17,21,25,36,38,39],[7,10,12,14,20,28,30,33],+1,+4,+10)),\n", - " Tree([2,8,15,16,23,25,31,32],[11,13,18,19,29,30,34,36],\n", - " Tree([6,19],[8,34],-34,-36,+0),\n", - " Tree([2,3,6,7,8,11,12,21,24,27,30,35,38],[9,13,16,17,18,22,23,26,28,29,31,34,36],+7,-39,-27),\n", - " Tree([3,11,19,30,31],[17,25,26,27,34],+11,+13,-31))),\n", - " Tree([4,7,10,11,13,19,20,22,24,26,28,29,34],[1,3,8,15,18,21,23,27,32,33,36,37,38],\n", - " Tree([4,7,9,14,17,19,21,27,34,39],[1,8,12,13,18,20,23,24,28,29],\n", - " Tree([1,5,8,14,20,25,26,33,35,37,39],[4,7,15,16,19,22,23,28,31,36,38],-23,-18,+19),\n", - " Tree([1,9,12,18,19,22,29,34,35,38],[2,6,11,13,17,21,26,32,33,36],+22,-15,+26),\n", - " Tree([7,13,14,15,22,28,29,32,33,34,35,36],[1,2,5,6,8,18,19,20,21,26,31,39],-21,+24,+20)),\n", - " Tree([4,5,6,9,14,19,21,22,24,31,36,38,39],[1,2,8,13,15,16,18,25,28,30,32,35,37],\n", - " Tree([3,7,12,14,16,19,23,27,28,36],[1,2,6,20,26,29,30,31,32,34],+14,-25,-16),\n", - " Tree([8,29,35],[10,17,39],-17,+0,+17),\n", - " Tree([3,4,8,10,11,17,19,20,22,23,26,30],[2,7,12,14,16,21,24,31,33,35,36,39],-14,+25,+16)),\n", - " Tree([6,7,8,9,13,15,17,26,30,35,37,38],[1,5,12,16,18,20,22,23,27,29,31,34],\n", - " Tree([16,20,25],[14,19,22],-22,+15,-20),\n", - " Tree([7,10,14,18,21,24,25,26,29,35,36],[1,3,5,9,11,12,15,16,20,27,31],+21,-19,-24),\n", - " Tree([1,2,3,4,6,15,16,18,20,21,27,29,39],[8,10,11,12,19,22,23,24,31,35,36,37,38],+18,-26,+23))),\n", - " Tree([3,7,12,13,16,17,18,27,32,37,38],[2,5,8,9,10,15,20,30,31,33,34],\n", - " Tree([1,5,6,11,12,29,31,32,39],[2,4,8,13,16,19,26,27,38],\n", - " Tree([2,13,15,18,20,21,26,29,32,33,34,38],[1,4,6,14,16,19,22,24,27,28,30,35],+32,-8,-2),\n", - " Tree([3,4,5,9,14,15,21,30,32,34,36],[10,12,13,16,17,23,29,31,35,38,39],-10,+37,-9),\n", - " Tree([3,20,23,25,27,37],[1,7,10,15,28,38],+27,-5,+38)),\n", - " Tree([2,4,7,13,16,17,18,20,22,27,36,37],[1,8,10,11,21,25,29,30,32,34,35,38],\n", - " Tree([7,9,13,14,17,20,21,24,26,33,34,35],[3,5,6,8,11,16,18,19,22,28,32,36],-11,-1,+36),\n", - " Tree([8,11,17,20,22,24,25,26],[2,5,6,7,9,14,23,39],-6,+28,+39),\n", - " Tree([5,7,12,14,20,22,31,35,37],[3,8,10,11,21,24,29,33,39],+35,-4,+29)),\n", - " Tree([11,12,13,14,16,17,22,26,30,33,35,39],[1,2,5,6,8,9,10,15,23,25,34,38],\n", - " Tree([7,13,21,33,34],[2,19,28,32,36],+33,+30,+0),\n", - " Tree([1,3,22,25,29,31,33,38],[6,9,14,16,20,28,30,34],+31,-7,-3),\n", - " Tree([1,2,3,8,9,13,16,22,31,35],[5,6,11,12,17,18,19,27,36,37],-12,+34,-13))))\n" + "(1·2·3·4 ⟘ 9·10·11·12) ➔ \n", + " >:(3·6·11·12 ⟘ 2·8·9·10) ➔ \n", + " >:(2·3·8·9 ⟘ 1·5·6·7) ➔ >:+3 =:-10 <:-9) \n", + " =:(2·4·8·9 ⟘ 6·10·11·12) ➔ >:+4 =:+1 <:0) \n", + " <:(1·6·11 ⟘ 3·7·12) ➔ >:-12 =:+2 <:-11)) \n", + " =:(1·5·7·10 ⟘ 2·4·8·11) ➔ \n", + " >:(1·7·11 ⟘ 5·9·10) ➔ >:+7 =:-8 <:+5) \n", + " =:(1·6·7·12 ⟘ 3·4·9·11) ➔ >:+6 =:0 <:-6) \n", + " <:(3·6·9 ⟘ 2·7·8) ➔ >:-7 =:-5 <:+8)) \n", + " <:(3·7·10·11 ⟘ 4·8·9·12) ➔ \n", + " >:(2·7·8 ⟘ 4·6·10) ➔ >:-4 =:+11 <:+10) \n", + " =:(3·11·12 ⟘ 1·6·8) ➔ >:-1 =:-2 <:0) \n", + " <:(1·3·12 ⟘ 4·5·11) ➔ >:+12 =:+9 <:-3)))\n" ] } ], "source": [ - "show(solve(Puzzle(39, 4)))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can get up to 26 balls in 3 weighings if we know that the only possibility is that one is lighter (no ball can be heavier):" + "# 12 balls\n", + "do(p12)" ] }, { @@ -601,31 +557,218 @@ "name": "stdout", "output_type": "stream", "text": [ - "Tree([1,2,3,4,5,6,7,8,9],[18,19,20,21,22,23,24,25,26],\n", - " Tree([6,7,10,11,13,20,22,23],[1,3,4,5,9,18,21,26],\n", - " Tree([1,6,11,13,19,21,22,23],[2,5,7,8,14,17,20,26],-26,-18,-21),\n", - " Tree([1,7,11,15,18,20,21,23,24],[2,3,4,10,13,14,19,22,26],-19,-25,-24),\n", - " Tree([7,8,12,13,18,20,25,26],[1,3,5,6,14,16,21,22],-22,-23,-20)),\n", - " Tree([6,8,13,15,18,20,21,23],[1,4,9,14,16,17,19,22],\n", - " Tree([8,11,16,21],[5,12,17,20],-17,-14,-16),\n", - " Tree([6,10,26],[2,11,16],-11,-12,-10),\n", - " Tree([1,26],[13,18],-13,-15,+0)),\n", - " Tree([5,8,9,10,15,23,24,26],[1,6,7,12,13,18,20,22],\n", - " Tree([1,5,9,16,17,20,24,25],[2,3,6,8,12,13,14,19],-6,-7,-1),\n", - " Tree([1,2,9,10,14,18,22,23,26],[3,6,7,11,12,13,20,21,25],-3,-4,-2),\n", - " Tree([8,14,16,23],[4,5,6,19],-5,-9,-8)))\n" + "(1·2·3·4 ⟘ 9·10·11·12) ➔ \n", + " >:(2·4·7·12 ⟘ 1·3·6·9) ➔ \n", + " >:(4·5·9·10 ⟘ 1·3·6·8) ➔ >:+4 =:+2 <:-9) \n", + " =:(4·5·6·11 ⟘ 1·3·9·10) ➔ >:-10 =:0 <:-11) \n", + " <:(1·5·9·11 ⟘ 3·4·8·10) ➔ >:+1 =:-12 <:+3)) \n", + " =:(1·5·6·7 ⟘ 2·4·11·12) ➔ \n", + " >:(2·4·5·11 ⟘ 1·7·8·9) ➔ >:+5 =:+6 <:+7) \n", + " =:(1·4·8·12 ⟘ 5·7·10·11) ➔ >:+8 =:0 <:-8) \n", + " <:(5·10 ⟘ 3·7) ➔ >:-7 =:-6 <:-5)) \n", + " <:(2·7·8·9 ⟘ 1·3·6·10) ➔ \n", + " >:(6·10 ⟘ 3·9) ➔ >:-3 =:-1 <:+9) \n", + " =:(1·6·8 ⟘ 3·4·11) ➔ >:-4 =:+12 <:+11) \n", + " <:(3 ⟘ 10) ➔ >:0 =:-2 <:+10)))\n" ] } ], "source": [ - "show(solve(Puzzle(26, 3, {-1})))" + "# A different random solution\n", + "do(p12)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "What other puzzles can you solve?" + "We can solve much larger puzzles. We can do 39 balls with 4 weighings:" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(1·2·3·4·5·6·7·8·9·10·11·12·13 ⟘ 27·28·29·30·31·32·33·34·35·36·37·38·39) ➔ \n", + " >:(3·5·8·9·14·16·18·26·34·36·37·39 ⟘ 2·4·10·12·13·17·19·22·27·30·31·32) ➔ \n", + " >:(4·6·7·14·19·20·23·24·28·34·35·37 ⟘ 3·9·12·15·17·18·22·26·30·31·32·38) ➔ \n", + " >:(6·9·12·14·17·19·21·22·32·36 ⟘ 8·13·15·16·20·23·30·33·35·38) ➔ >:-30 =:-31 <:-32) \n", + " =:(7·8·11·16·17·19·21·24·27·34·36 ⟘ 2·6·12·14·15·18·26·30·33·37·39) ➔ >:+8 =:+5 <:-27) \n", + " <:(9·12·19·29·30 ⟘ 3·8·15·23·28) ➔ >:+9 =:0 <:+3)) \n", + " =:(8·9·13·20·26·27·29·30·34·35 ⟘ 1·5·10·18·19·25·28·32·33·38) ➔ \n", + " >:(3·7·14·16·20·25·27·29·30·33·34 ⟘ 2·4·5·8·12·13·24·31·35·38·39) ➔ >:-38 =:-28 <:-33) \n", + " =:(8·11·15·36·37·38 ⟘ 2·7·19·30·31·34) ➔ >:+11 =:+6 <:+7) \n", + " <:(1·2·3·9·10·17·26·28·34·35 ⟘ 6·7·12·15·16·22·24·30·32·37) ➔ >:+1 =:-29 <:-35)) \n", + " <:(1·2·3·8·13·21·25·26·29·33·37·38·39 ⟘ 6·7·12·16·17·19·23·24·27·28·30·31·34) ➔ \n", + " >:(3·6·7·16·17·18·20·22·24·25·27·29·39 ⟘ 2·5·9·12·19·23·30·31·33·34·36·37·38) ➔ >:-34 =:+13 <:+2) \n", + " =:(2·4·11·12·14·17·23·27·28·32·33·36 ⟘ 3·5·6·8·15·16·21·25·29·31·34·38) ➔ >:+4 =:+10 <:-36) \n", + " <:(1·6·10·18·19·21·23·27·29·31·33·37 ⟘ 2·3·4·8·13·16·17·20·26·32·34·39) ➔ >:-39 =:+12 <:-37))) \n", + " =:(1·3·6·9·14·16·22·23·28·30·32·39 ⟘ 2·4·8·12·17·19·21·24·26·29·31·33) ➔ \n", + " >:(1·3·13·14·23·24·25·28·32·36·38·39 ⟘ 4·5·9·15·16·22·26·27·29·31·35·37) ➔ \n", + " >:(3·4·7·8·10·14·18·26·29·36·37 ⟘ 1·5·6·9·11·12·13·27·31·34·38) ➔ >:+14 =:+23 <:-26) \n", + " =:(1·3·9·10·11·14·19·32 ⟘ 12·16·17·18·29·34·36·38) ➔ >:-17 =:-21 <:-19) \n", + " <:(7·17·22·23·24·32 ⟘ 6·10·15·20·25·30) ➔ >:+22 =:+16 <:-24)) \n", + " =:(4·11·14·20·32·33·36·39 ⟘ 2·3·8·15·18·19·28·37) ➔ \n", + " >:(7·12·14·18·22·26·27·31·33·34·36·37 ⟘ 2·5·6·8·9·13·15·16·24·28·30·38) ➔ >:-15 =:+20 <:-18) \n", + " =:(2·4·11·12·15·18·23·27·30·34 ⟘ 1·3·8·24·25·26·28·29·33·38) ➔ >:-25 =:0 <:+25) \n", + " <:(6·14·18·20·24·27·34 ⟘ 1·17·23·25·30·37·38) ➔ >:+18 =:+15 <:-20)) \n", + " <:(3·4·12·14·17·18·20·22·26·28·31 ⟘ 2·7·10·19·23·25·27·29·32·38·39) ➔ \n", + " >:(12·30·31·32·35·38 ⟘ 4·7·13·14·23·26) ➔ >:-23 =:+17 <:+26) \n", + " =:(19·25 ⟘ 16·21) ➔ >:-16 =:+24 <:+21) \n", + " <:(1·3·11·19·22·30·32 ⟘ 2·12·16·18·20·25·26) ➔ >:+19 =:-14 <:-22))) \n", + " <:(1·11·12·15·16·17·18·21·25·27·30·37·39 ⟘ 2·5·7·8·13·20·23·28·31·32·34·35·36) ➔ \n", + " >:(2·7·8·9·15·16·18·20·24·30·34·36·39 ⟘ 3·10·11·12·13·17·21·22·25·28·29·31·32) ➔ \n", + " >:(3·11·12·26·27·33 ⟘ 1·8·13·17·18·30) ➔ >:-13 =:+39 <:+30) \n", + " =:(3·8·10·12·16·17·21·24·30·35·36·37 ⟘ 6·7·11·14·15·19·25·26·27·29·38·39) ➔ >:+37 =:-5 <:+27) \n", + " <:(7·15·18·20·22·24·26·27·29·34·36·38 ⟘ 1·2·3·5·10·11·14·16·17·19·25·33) ➔ >:-2 =:-8 <:-7)) \n", + " =:(6·10·12·15·19·30·37 ⟘ 3·4·14·28·34·38·39) ➔ \n", + " >:(1·4·9·10·12·15·21·23·34 ⟘ 8·14·18·24·28·29·31·33·37) ➔ >:0 =:-3 <:-4) \n", + " =:(2·11·18·19·30·32·33 ⟘ 14·17·22·23·25·29·39) ➔ >:+33 =:-9 <:+29) \n", + " <:(2·3·8·14·15·16·24·25·27·29·33·35·37 ⟘ 1·4·10·12·17·19·21·22·23·26·36·38·39) ➔ >:-10 =:-6 <:+38)) \n", + " <:(4·7·12·15·16·20·21·22·27·34·35 ⟘ 1·2·3·24·25·29·30·32·33·36·37) ➔ \n", + " >:(2·3·6·7·14·15·22·24·31·34·36·38 ⟘ 4·5·11·12·19·20·25·26·29·30·33·35) ➔ >:+34 =:-1 <:+35) \n", + " =:(11·28·37 ⟘ 7·22·26) ➔ >:+28 =:+31 <:-11) \n", + " <:(7·10·12·25·26·30·34·36 ⟘ 3·4·19·20·24·31·35·38) ➔ >:+36 =:+32 <:-12))))\n" + ] + } + ], + "source": [ + "do(Puzzle(39, 4))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can do 26 balls in 3 weighings if we know that no ball can be heavier:" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "(1·2·3·4·5·6·7·8·9 ⟘ 18·19·20·21·22·23·24·25·26) ➔ \n", + " >:(3·7·8·9·14·16·20·21·23 ⟘ 2·4·6·10·12·13·18·19·24) ➔ \n", + " >:(3·8·16·19·21·22·25 ⟘ 4·5·9·10·11·20·24) ➔ >:-24 =:-18 <:-19) \n", + " =:(3·15·20·25 ⟘ 6·12·22·24) ➔ >:-22 =:-26 <:-25) \n", + " <:(1·3·7·9·21·22·24 ⟘ 2·8·13·14·17·18·23) ➔ >:-23 =:-20 <:-21)) \n", + " =:(7·11·15·17·21 ⟘ 2·8·10·14·20) ➔ \n", + " >:(4·9·16·18·20·22 ⟘ 2·5·6·8·14·26) ➔ >:-14 =:-10 <:0) \n", + " =:(13·15·19·20·21·22·23·25·26 ⟘ 1·4·7·9·10·12·14·18·24) ➔ >:-12 =:-16 <:-13) \n", + " <:(5·7·9·14·17·19·21 ⟘ 1·4·6·11·13·18·25) ➔ >:-11 =:-15 <:-17)) \n", + " <:(2·5·8·10·12·16·19·24·26 ⟘ 1·3·6·13·15·18·22·23·25) ➔ \n", + " >:(1·2·11·22 ⟘ 3·14·15·19) ➔ >:-3 =:-6 <:-1) \n", + " =:(6·9·12·13·17·21 ⟘ 1·2·4·5·14·16) ➔ >:-4 =:-7 <:-9) \n", + " <:(3·5·9·11·17·22·24·26 ⟘ 7·8·12·15·16·18·19·23) ➔ >:-8 =:-2 <:-5)))\n" + ] + } + ], + "source": [ + "do(Puzzle(26, 3, {-1}))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# What's Next?\n", + "\n", + "- What other puzzles can you solve?\n", + "- Can you make a table of solvable and unsolvable puzzles?\n", + "- What happens when it is a possibility that *no* ball is odd?\n", + "- What happens when it is a possibility that *two* balls are odd?\n", + "- Can you prove which puzzles are unsolvable? Can you modify the `find_tree` function so that it only fails when the puzzle really is unsolvable, and not when it happens to have bad luck with `random`?\n", + "\n", + "It turns out that our solution for the 12-ball problem can handle the case where there is no oddball. It correctly returns 0, meaning there is no oddball, when given 0:" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "follow(solve(p12), oddball=0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Three weighings can theoretically handle 27 possibilities. Can we solve the 13-ball problem, which has 26 possibilities? **No**, because there is no first weighing that partitions the 26 possibilities into 9/9/8; the best we can do is partition them into 8/10/8 or 10/6/10:" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'gt': [1, 2, 3, 4, -10, -11, -12, -13],\n", + " 'eq': [5, -5, 6, -6, 7, -7, 8, -8, 9, -9],\n", + " 'lt': [-1, -2, -3, -4, 10, 11, 12, 13]}" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "p13 = Puzzle(13)\n", + "partition([1, 2, 3, 4], [10, 11, 12, 13], p13.oddballs)" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'gt': [1, 2, 3, 4, 5, -9, -10, -11, -12, -13],\n", + " 'eq': [6, -6, 7, -7, 8, -8],\n", + " 'lt': [-1, -2, -3, -4, -5, 9, 10, 11, 12, 13]}" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "partition([1, 2, 3, 4, 5], [9, 10, 11, 12, 13], p13.oddballs)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "What else can you discover?" ] } ],