From eda2a2cc141b36ecd8ff4918496c804ea8923783 Mon Sep 17 00:00:00 2001 From: Peter Norvig Date: Mon, 19 Aug 2019 17:43:00 -0700 Subject: [PATCH] Add files via upload --- ipynb/TwelveBalls.ipynb | 473 ++++++++++++++++++++++++---------------- 1 file changed, 280 insertions(+), 193 deletions(-) diff --git a/ipynb/TwelveBalls.ipynb b/ipynb/TwelveBalls.ipynb index 7477025..ae5196e 100644 --- a/ipynb/TwelveBalls.ipynb +++ b/ipynb/TwelveBalls.ipynb @@ -11,21 +11,22 @@ "> *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 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", + "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, or neither. 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. 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", + "- **oddballs**: Exactly one of the balls is **odd** in its weight. \n", + "I'll represent the situation where ball N is heavier as +N, and where ball N is lighter as -N. (I'll represent the situation where no ball is odd with `0`; that's not needed for the puzzle stated above, but is a nice extension.) With N = 3, I will represent the set of all possible oddballs as `{+1, -1, +2, -2, +3, -3}`. \n", + "- **puzzle**: I'll call `Puzzle(N, W, {-1, +1})` to create a puzzle with N balls, W weighings allowed, and the oddballs allowing for each ball to be lighter (-1) or heavier (+1). The third argument can be any subset of `{-1, 0, +1}`, where the `0` stands for \"allow the possibility that no ball is odd; they all weigh the same.\" \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**: 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", + "- **solution**: Given a puzzle, a solution is a **strategy tree** that can correctly discover the odd ball, whatever it is, in the allowable number of weighings.\n", + "- **strategy tree**: a tree where each node is either a leaf node which is an *oddball* integer, or is an interior node with 5 components: `Tree(L, R, gt, eq, lt)` is a node where `L` and `R` are the collections of balls to be weighed, and `gt`, `eq`, and `lt` are trees for when the result of the weighing is that the total weight of the balls on the left is greater than, equal to, or less than, respectively, the total weight of the balls on the right.\n", + "- **following a path in a tree**: I'll use `follow(tree, oddball)` to say \"follow the path through the tree, doing each weighing under the assumption of the given oddball, and return the leaf node reached by the path—the oddball that the tree predicts. Note that the function `follow` gets to see what the oddball is, but the `tree` never gets direct access to that; the tree has to figure it out by doing weighings.\n", + "- **valid tree**: a tree is a valid solution to a puzzle if no branch uses more than the allowable number of weighings, and if, for every possible oddball in the puzzle, following the path through the tree correct returns that oddball as the answer. I'll use `valid(tree, puzzle)` for this.\n", "\n", "\n", "# Implementation\n", @@ -48,11 +49,10 @@ "\n", "class Puzzle:\n", " \"Represent a specific ball-weighing puzzle.\"\n", - " def __init__(self, N=12, weighings=3, oddities={+1, -1}):\n", - " self.N = N\n", + " def __init__(self, N=12, weighings=3, oddities={-1, +1}):\n", " self.weighings = weighings\n", - " self.balls = list(range(1, N + 1))\n", - " self.oddballs = [b * o for b in self.balls for o in oddities] \n", + " self.balls = list(range(1, N + 1))\n", + " self.oddballs = {b * o for b in self.balls for o in oddities}\n", " \n", "Tree = namedtuple('Tree', 'L, R, gt, eq, lt')\n", "\n", @@ -68,11 +68,10 @@ "def weight(ball, oddball) -> int: \n", " return 101 if +ball == oddball else 99 if -ball == oddball else 100\n", " \n", - "def solve(puzzle) -> Tree:\n", - " \"Return a valid tree; one that solves the puzzle.\"\n", + "def solve(puzzle) -> Tree or None:\n", + " \"Return a valid tree; one that solves the puzzle, or None.\"\n", " tree = find_tree(puzzle, puzzle.oddballs, puzzle.weighings)\n", - " assert valid(tree, puzzle)\n", - " return tree\n", + " return tree if valid(tree, puzzle) else None\n", " \n", "def follow(tree, oddball) -> Oddball:\n", " \"Follow a path through the tree and return the oddball that the tree leads us to.\"\n", @@ -84,7 +83,8 @@ " \n", "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", + " return (tree is not None and\n", + " depth(tree) <= puzzle.weighings and \n", " all(follow(tree, oddball) == oddball \n", " for oddball in puzzle.oddballs))\n", "\n", @@ -112,7 +112,7 @@ "text": [ "3\n", "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]\n", - "[1, -1, 2, -2, 3, -3, 4, -4, 5, -5, 6, -6, 7, -7, 8, -8, 9, -9, 10, -10, 11, -11, 12, -12]\n" + "{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, -12, -11, -1, -9, -8, -7, -6, -5, -4, -3, -2, -10}\n" ] } ], @@ -201,10 +201,10 @@ "outputs": [], "source": [ "def partition(L, R, oddballs) -> dict:\n", - " \"Give all the possible outcomes of weighing L versus R.\"\n", - " part = dict(gt=[], eq=[], lt=[])\n", + " \"Build a dict of the possible outcomes (oddballs) from weighing L versus R.\"\n", + " part = dict(gt=set(), eq=set(), lt=set())\n", " for odd in oddballs:\n", - " part[weigh(L, R, odd)].append(odd)\n", + " part[weigh(L, R, odd)].add(odd)\n", " return part" ] }, @@ -223,9 +223,9 @@ { "data": { "text/plain": [ - "{'gt': [1, 2, -11, -12],\n", - " 'eq': [3, -3, 4, -4, 5, -5, 6, -6, 7, -7, 8, -8, 9, -9, 10, -10],\n", - " 'lt': [-1, -2, 11, 12]}" + "{'gt': {-12, -11, 1, 2},\n", + " 'eq': {-10, -9, -8, -7, -6, -5, -4, -3, 3, 4, 5, 6, 7, 8, 9, 10},\n", + " 'lt': {-2, -1, 11, 12}}" ] }, "execution_count": 7, @@ -254,9 +254,9 @@ { "data": { "text/plain": [ - "{'gt': [1, 2, 3, 4, -9, -10, -11, -12],\n", - " 'eq': [5, -5, 6, -6, 7, -7, 8, -8],\n", - " 'lt': [-1, -2, -3, -4, 9, 10, 11, 12]}" + "{'gt': {-12, -11, -10, -9, 1, 2, 3, 4},\n", + " 'eq': {-8, -7, -6, -5, 5, 6, 7, 8},\n", + " 'lt': {-4, -3, -2, -1, 9, 10, 11, 12}}" ] }, "execution_count": 8, @@ -279,7 +279,7 @@ " - We will then see what partition `L` and `R` gives us, and whether the partition is good or bad.\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", + " (If we don't find a good partition after 1,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." ] }, @@ -292,29 +292,34 @@ "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", + " return oddballs.pop() # One oddball possibility left; we're done: leaf node\n", + " elif len(oddballs) == 0:\n", + " return 0 # No oddball\n", + " elif weighings == 0:\n", + " return None # Can't find a solution in the allowable weighings\n", " else:\n", " L, R, part = find_good_partition(puzzle, oddballs, weighings - 1)\n", + " if not part: \n", + " return None\n", " subtrees = {r: find_tree(puzzle, part[r], weighings - 1) for r in part}\n", - " return Tree(L, R, **subtrees)\n", + " if None not in subtrees.values(): \n", + " return Tree(L, R, **subtrees)\n", " \n", - "def find_good_partition(puzzle, oddballs, weighings) -> tuple:\n", + "def find_good_partition(puzzle, oddballs, weighings) -> (list, list, dict):\n", " \"Return (L, R, partition) such that no partition entry has more than 3**weighings oddballs.\"\n", - " for _ in range(10000): \n", + " for _ in range(1000): \n", " L, R = random_LR(puzzle, oddballs)\n", " part = partition(L, R, oddballs)\n", " if all(len(entry) <= 3 ** weighings for entry in part.values()):\n", " return L, R, part\n", - " raise ValueError('find_good_partition: not found')\n", + " return [], [], {} # Fail\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 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:])" + " return puzzle.balls[:B], puzzle.balls[-B:]" ] }, { @@ -332,11 +337,11 @@ { "data": { "text/plain": [ - "([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]})" + "([9, 3, 2, 1],\n", + " [10, 11, 4, 12],\n", + " {'gt': {-12, -11, -10, -4, 1, 2, 3, 9},\n", + " 'eq': {-8, -7, -6, -5, 5, 6, 7, 8},\n", + " 'lt': {-9, -3, -2, -1, 4, 10, 11, 12}})" ] }, "execution_count": 10, @@ -352,7 +357,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "But it uses `random`, so it won't get the same result every time:" + "It uses `random`, so it won't get the same result every time:" ] }, { @@ -363,11 +368,11 @@ { "data": { "text/plain": [ - "([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]})" + "([8, 12, 10, 4],\n", + " [2, 6, 7, 11],\n", + " {'gt': {-11, -7, -6, -2, 4, 8, 10, 12},\n", + " 'eq': {-9, -5, -3, -1, 1, 3, 5, 9},\n", + " 'lt': {-12, -10, -8, -4, 2, 6, 7, 11}})" ] }, "execution_count": 11, @@ -396,7 +401,7 @@ { "data": { "text/plain": [ - "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)))" + "Tree(L=[6, 2, 3, 5], R=[11, 1, 8, 4], gt=Tree(L=[3, 5, 11], R=[6, 4, 2], gt=Tree(L=[8, 11, 4, 5], R=[10, 9, 6, 7], gt=5, eq=3, lt=-4), eq=Tree(L=[6, 1, 9], R=[7, 12, 4], gt=0, eq=-8, lt=-1), lt=Tree(L=[1, 10, 5], R=[2, 11, 7], gt=-11, eq=6, lt=2)), eq=Tree(L=[12, 2, 3, 4], R=[10, 8, 7, 11], gt=Tree(L=[7, 4], R=[10, 5], gt=-10, eq=12, lt=-7), eq=Tree(L=[8, 1, 10, 11], R=[5, 12, 6, 9], gt=-9, eq=0, lt=9), lt=Tree(L=[12, 4, 8, 7], R=[11, 1, 3, 6], gt=7, eq=10, lt=-12)), lt=Tree(L=[2, 6, 8, 11], R=[10, 9, 5, 4], gt=Tree(L=[12, 11, 10], R=[8, 3, 4], gt=11, eq=-5, lt=8), eq=Tree(L=[8, 5, 1], R=[9, 12, 2], gt=1, eq=-3, lt=0), lt=Tree(L=[5, 10], R=[4, 2], gt=-2, eq=-6, lt=4)))" ] }, "execution_count": 12, @@ -423,7 +428,7 @@ { "data": { "text/plain": [ - "Tree(L=[1], R=[3], gt=-3, eq=-2, lt=-1)" + "Tree(L=[2], R=[1], gt=-1, eq=-3, lt=-2)" ] }, "execution_count": 13, @@ -440,9 +445,18 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "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", + "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. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ "\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." + "\n", + "# Prettier Output\n", + "\n", + "Let's make the output easier to read. I'll use `[1·2·3·4 ⟘ 9·10·11·12] ➔` to mean *\"The result of weighing balls 1,2,3,4 versus 9,10,11,12 is...\"*. I'll indent for each interior node in the tree. 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. Putting `1·2·3·4` on the left is no different than `3·7·9·12`, because each ball is undifferentiated at the start. I'll alter `random_LR` to accomodate that." ] }, { @@ -460,13 +474,13 @@ " 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", + " return f'{indent}{prefix}[{items(tree.L)} ⟘ {items(tree.R)}] ➔ {subtrees})'\n", + " elif tree == 0 or tree == None:\n", + " return f'{prefix}{tree}'\n", " else:\n", " return f'{prefix}{tree:+d}'\n", " \n", - "def items(collection): return '·'.join(map(str, collection))\n", + "def items(collection): return '·'.join(map(str, sorted(collection)))\n", "\n", "def random_LR(puzzle, oddballs):\n", " \"Random choice of balls for L and R side.\"\n", @@ -476,7 +490,7 @@ " puzzle.balls.sort()\n", " else:\n", " random.shuffle(puzzle.balls) \n", - " return sorted(puzzle.balls[:B]), sorted(puzzle.balls[-B:])" + " return puzzle.balls[:B], puzzle.balls[-B:]" ] }, { @@ -488,12 +502,12 @@ "name": "stdout", "output_type": "stream", "text": [ - "(1 ⟘ 3) ➔ >:-3 =:-2 <:-1)\n" + "[1 ⟘ 3] ➔ >:-3 =:-2 <:-1)\n" ] } ], "source": [ - "# 3 balls, 1 weighing, only lighter\n", + "# 3 balls, 1 weighing, only lighter balls possible\n", "do(Puzzle(3, 1, {-1}))" ] }, @@ -506,15 +520,15 @@ "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" + "[1 ⟘ 3] ➔ \n", + " >:[3 ⟘ 2] ➔ >:0 =:+1 <:-3) \n", + " =:[2 ⟘ 3] ➔ >:+2 =:0 <:-2) \n", + " <:[3 ⟘ 2] ➔ >:+3 =:-1 <:0))\n" ] } ], "source": [ - "# 3 balls, 2 weighings, lighter or heavier\n", + "# 3 balls, 2 weighings, lighter or heavier balls possible\n", "do(Puzzle(3, 2))" ] }, @@ -527,55 +541,33 @@ "name": "stdout", "output_type": "stream", "text": [ - "(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" + "[1·2·3·4 ⟘ 9·10·11·12] ➔ \n", + " >:[2·4·5·10 ⟘ 1·3·6·8] ➔ \n", + " >:[5 ⟘ 4] ➔ >:0 =:+2 <:+4) \n", + " =:[1·11 ⟘ 7·12] ➔ >:-12 =:-9 <:-11) \n", + " <:[3·9 ⟘ 1·12] ➔ >:+3 =:-10 <:+1)) \n", + " =:[2·5·10·11 ⟘ 4·6·8·9] ➔ \n", + " >:[8·11 ⟘ 1·6] ➔ >:-6 =:+5 <:-8) \n", + " =:[7·8 ⟘ 1·6] ➔ >:+7 =:0 <:-7) \n", + " <:[7·11·12 ⟘ 5·6·9] ➔ >:-5 =:+8 <:+6)) \n", + " <:[2·10·11 ⟘ 1·7·9] ➔ \n", + " >:[5·7·8·10 ⟘ 3·4·11·12] ➔ >:+10 =:-1 <:+11) \n", + " =:[1·5·8·10 ⟘ 2·3·11·12] ➔ >:-3 =:-4 <:+12) \n", + " <:[4·5·7·12 ⟘ 2·3·9·10] ➔ >:-2 =:0 <:+9)))\n" ] } ], "source": [ - "# 12 balls\n", + "# The original puzzle with 12 balls\n", "do(p12)" ] }, { - "cell_type": "code", - "execution_count": 18, + "cell_type": "markdown", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "(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": [ - "# A different random solution\n", - "do(p12)" + "# Other Puzzles\n", + "\n" ] }, { @@ -587,53 +579,53 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 18, "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" + "[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·4·6·7·8·12·21·27·29·33·34·36·37 ⟘ 10·11·14·16·18·22·23·24·25·26·28·30·35] ➔ \n", + " >:[2·9·12·13·17·18·20·26·31·35·39 ⟘ 1·3·6·10·19·21·24·28·29·30·37] ➔ \n", + " >:[1·3·5·11·13·15·16·20·24·29·35·38 ⟘ 7·8·9·10·12·17·26·27·28·34·36·39] ➔ >:-28 =:-30 <:+12) \n", + " =:[3·8·14·20·22·24·28·36·38 ⟘ 4·9·12·18·19·25·29·30·31] ➔ >:+8 =:+7 <:+4) \n", + " <:[5·11·13·15·18·19·20·26·30·32·34·36 ⟘ 1·4·6·7·16·21·25·27·29·31·35·38] ➔ >:-35 =:+3 <:+6)) \n", + " =:[3·7·12·14·15·19·20·23·25·26·29·34 ⟘ 1·4·5·9·11·18·24·30·31·37·38·39] ➔ \n", + " >:[2·6·16·22·26·31·32·33 ⟘ 1·3·9·11·12·13·37·38] ➔ >:-38 =:-39 <:-31) \n", + " =:[2·6·10·16·19·23·26·28·31·32 ⟘ 5·7·11·12·22·24·30·33·35·37] ➔ >:+2 =:+13 <:-32) \n", + " <:[4·5·10·19·23·31·36 ⟘ 6·8·9·11·33·38·39] ➔ >:+5 =:+1 <:+9)) \n", + " <:[2·5·6·7·11·15·18·22·25·28·29·30 ⟘ 3·9·10·13·17·20·23·26·31·33·35·36] ➔ \n", + " >:[1·2·11·13·17·24·26·27·28·31·35·36 ⟘ 5·6·9·12·14·15·16·22·23·32·34·38] ➔ >:+11 =:-33 <:-36) \n", + " =:[5·6·10·12·14·18·20·21·22·28·35·37·39 ⟘ 1·2·3·4·7·9·16·19·23·31·32·34·38] ➔ >:-34 =:-27 <:-37) \n", + " <:[7·8·10·13·16·24·26·29·32·35·36 ⟘ 1·4·5·9·19·25·27·30·31·33·34] ➔ >:+10 =:0 <:-29))) \n", + " =:[1·4·6·20·22·23·27·30·31·32·33·35·38 ⟘ 2·9·10·13·14·15·16·18·25·26·28·36·39] ➔ \n", + " >:[1·2·4·8·10·15·16·18·20·23·31·34·36 ⟘ 3·5·7·13·14·24·27·29·32·33·37·38·39] ➔ \n", + " >:[1·3·5·14·15·17·21·22·23·31·34·35·36 ⟘ 2·4·6·8·12·13·18·27·29·30·33·37·39] ➔ >:+23 =:+20 <:-14) \n", + " =:[4·5·9·11·15·18·21·27·30·34·38 ⟘ 1·2·10·12·14·17·19·22·25·29·31] ➔ >:-25 =:-26 <:+22) \n", + " <:[6·12·13·18·27·35 ⟘ 1·9·11·14·16·29] ➔ >:-16 =:-15 <:-18)) \n", + " =:[6·8·10·11·19·21·27·28·29·33·35·37 ⟘ 1·14·16·18·20·24·31·32·34·36·38·39] ➔ \n", + " >:[4·7·22·25·30·33 ⟘ 8·9·12·21·24·34] ➔ >:-24 =:+19 <:+21) \n", + " =:[6·15·17·18·19·21·27·33·35·36·39 ⟘ 2·3·5·11·12·16·20·24·28·30·38] ➔ >:+17 =:0 <:-17) \n", + " <:[2·4·5·6·12·16·20·22·23·25·26·32·37 ⟘ 3·7·9·15·17·18·21·24·30·34·35·38·39] ➔ >:-21 =:-19 <:+24)) \n", + " <:[2·10·15·18·21·22·26·29·32·34·37 ⟘ 1·3·4·11·12·14·25·28·30·31·33] ➔ \n", + " >:[6·10·15·20·25·27·33 ⟘ 4·7·9·18·19·29·36] ➔ >:+15 =:+26 <:+18) \n", + " =:[1·3·5·11·13·17·23·25·34·37 ⟘ 2·6·12·18·19·20·22·27·30·36] ➔ >:-20 =:+16 <:-23) \n", + " <:[4·10·11·17·18·19·21·25·26·30·38 ⟘ 1·3·8·13·14·20·31·33·34·35·37] ➔ >:+25 =:-22 <:+14))) \n", + " <:[2·3·6·8·23·29·33·34·35·36·37·39 ⟘ 4·7·18·19·20·24·25·26·27·31·32·38] ➔ \n", + " >:[2·4·19·22·26·27·28·29·32·37·38 ⟘ 1·7·8·13·14·15·23·25·30·33·39] ➔ \n", + " >:[3·11·12·15·19·22·23 ⟘ 6·7·9·14·26·29·39] ➔ >:-7 =:+37 <:+29) \n", + " =:[4·9·13·15·19·20·25·28·31·33·34·37·38 ⟘ 1·11·12·14·17·18·23·24·26·27·29·30·36] ➔ >:+34 =:+35 <:+36) \n", + " <:[6·10·11·13·16·17·18·22·28·36·37·38·39 ⟘ 2·5·8·9·15·19·20·23·24·29·32·33·35] ➔ >:+39 =:-4 <:+33)) \n", + " =:[1·7·15·20·22·24·26·27·29·38 ⟘ 2·9·11·12·17·19·28·30·34·35] ➔ \n", + " >:[2·4·5·6·11·15·17·20·22·23·30·31·34 ⟘ 1·10·12·16·19·24·26·28·29·32·35·36·37] ➔ >:-12 =:-9 <:-11) \n", + " =:[3·8·9·13·19·20·25·27·34 ⟘ 2·5·7·11·18·28·29·35·38] ➔ >:-5 =:-10 <:-13) \n", + " <:[4·7·9·10·11·16·17·19·28·29·34·38·39 ⟘ 3·8·12·14·15·18·21·23·25·27·30·31·36] ➔ >:+28 =:-1 <:+30)) \n", + " <:[1·4·6·21·24·31·32·35·36·39 ⟘ 7·8·11·12·13·15·22·26·29·38] ➔ \n", + " >:[1·2·4·12·15·18·21·22·23·24·34·36·38 ⟘ 7·8·9·10·13·25·28·29·30·32·33·37·39] ➔ >:-8 =:+31 <:+32) \n", + " =:[3·4·14·15·23·25·27 ⟘ 5·12·13·22·33·38·39] ➔ >:+27 =:-2 <:-3) \n", + " <:[4·8·9·15·23·24·35·38 ⟘ 5·10·16·21·22·27·34·37] ➔ >:+38 =:-6 <:0))))\n" ] } ], @@ -645,7 +637,31 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We can do 26 balls in 3 weighings if we know that no ball can be heavier:" + "However, we can't do 40 balls:" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "None\n" + ] + } + ], + "source": [ + "do(Puzzle(40, 4))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can do 12 balls in 3 weighings even if it is possible that none of the balls is odd:" ] }, { @@ -657,39 +673,31 @@ "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" + "[1·2·3·4 ⟘ 9·10·11·12] ➔ \n", + " >:[3·4·9·10 ⟘ 1·6·8·12] ➔ \n", + " >:[4·6·9·12 ⟘ 1·2·5·10] ➔ >:+4 =:+3 <:-12) \n", + " =:[1·5·11·12 ⟘ 4·6·9·10] ➔ >:0 =:+2 <:-11) \n", + " <:[1·9 ⟘ 4·11] ➔ >:+1 =:-10 <:-9)) \n", + " =:[1·7·9·11 ⟘ 5·8·10·12] ➔ \n", + " >:[4·7·8 ⟘ 2·11·12] ➔ >:+7 =:-5 <:-8) \n", + " =:[4·5·7·8 ⟘ 1·2·6·9] ➔ >:-6 =:0 <:+6) \n", + " <:[8·12 ⟘ 3·5] ➔ >:+8 =:-7 <:+5)) \n", + " <:[3·6·8·10 ⟘ 2·4·7·12] ➔ \n", + " >:[5·7·9 ⟘ 4·10·11] ➔ >:-4 =:-2 <:+10) \n", + " =:[5·11 ⟘ 3·9] ➔ >:+11 =:-1 <:+9) \n", + " <:[3·4·9 ⟘ 7·8·11] ➔ >:0 =:+12 <:-3)))\n" ] } ], "source": [ - "do(Puzzle(26, 3, {-1}))" + "do(Puzzle(12, 3, {-1, 0, +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:" + "We can do **26** balls in 3 weighings if we know that no ball can be heavier (and one or none is lighter):" ] }, { @@ -698,25 +706,36 @@ "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "0" - ] - }, - "execution_count": 21, - "metadata": {}, - "output_type": "execute_result" + "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·12·14·15·18·21·25 ⟘ 1·6·8·10·17·20·24·26] ➔ \n", + " >:[3·6·9·10·13·14·17·26 ⟘ 5·7·8·15·21·23·24·25] ➔ >:-24 =:-20 <:-26) \n", + " =:[5·6·7·9·12·13·14·15·23 ⟘ 1·2·3·4·8·16·18·19·25] ➔ >:-19 =:-22 <:-23) \n", + " <:[1·2·7·9·10·12·14·19·25 ⟘ 3·4·5·8·13·15·16·21·22] ➔ >:-21 =:-18 <:-25)) \n", + " =:[10·11·16·19·20·21·22·23·24 ⟘ 1·2·3·4·7·13·14·17·26] ➔ \n", + " >:[2·5·7·14·15·16 ⟘ 4·9·11·17·19·21] ➔ >:-17 =:-13 <:-14) \n", + " =:[5·8·10·13·14·15·23·26 ⟘ 4·7·9·12·18·19·24·25] ➔ >:-12 =:0 <:-15) \n", + " <:[6·10·13·15·19·21·22·23·25 ⟘ 3·4·7·8·9·11·12·17·24] ➔ >:-11 =:-16 <:-10)) \n", + " <:[3·4·8·11·12·16·20 ⟘ 1·2·5·10·13·21·26] ➔ \n", + " >:[1·9·16·18·20·22 ⟘ 4·5·8·13·14·19] ➔ >:-5 =:-2 <:-1) \n", + " =:[1·4·7·13·16·17 ⟘ 5·9·10·12·21·25] ➔ >:-9 =:-6 <:-7) \n", + " <:[4·6·12·14·18 ⟘ 8·11·13·19·20] ➔ >:-8 =:-3 <:-4)))\n" + ] } ], "source": [ - "follow(solve(p12), oddball=0)" + "do(Puzzle(26, 3, {-1, 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:" + "Three weighings can theoretically handle at most 27 possibilities, and we used all 27 in the problem above. \n", + "\n", + "Can we solve the 13-ball, heavier-or-lighter problem, which has 26 possibilities? " ] }, { @@ -725,21 +744,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" + "name": "stdout", + "output_type": "stream", + "text": [ + "None\n" + ] } ], "source": [ - "p13 = Puzzle(13)\n", - "partition([1, 2, 3, 4], [10, 11, 12, 13], p13.oddballs)" + "do(Puzzle(13))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**No**, and the reason is that there is no first weighing that partitions the 26 possibilities into 9/9/8; the best we can do is partition into 8/10/8 or 10/6/10:" ] }, { @@ -750,9 +770,9 @@ { "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]}" + "{'gt': {-13, -12, -11, -10, 1, 2, 3, 4},\n", + " 'eq': {-9, -8, -7, -6, -5, 5, 6, 7, 8, 9},\n", + " 'lt': {-4, -3, -2, -1, 10, 11, 12, 13}}" ] }, "execution_count": 23, @@ -761,14 +781,81 @@ } ], "source": [ - "partition([1, 2, 3, 4, 5], [9, 10, 11, 12, 13], p13.oddballs)" + "partition([1, 2, 3, 4], [10, 11, 12, 13], Puzzle(13).oddballs)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'gt': {-13, -12, -11, -10, -9, 1, 2, 3, 4, 5},\n", + " 'eq': {-8, -7, -6, 6, 7, 8},\n", + " 'lt': {-5, -4, -3, -2, -1, 9, 10, 11, 12, 13}}" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "partition([1, 2, 3, 4, 5], [9, 10, 11, 12, 13], Puzzle(13).oddballs)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "What else can you discover?" + "Here's a puzzle with 25 balls, and the possibilities are that either one of the odd-numbered balls is heavier, or one of the even-numbered balls is lighter, or all the balls weigh the same." + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[1·2·3·4·5·6·7·8·9 ⟘ 17·18·19·20·21·22·23·24·25] ➔ \n", + " >:[1·5·8·9·11·13·18·22 ⟘ 2·6·7·14·16·17·19·25] ➔ \n", + " >:[2·4·9·10·11·13·16·21·22 ⟘ 3·5·8·12·18·19·20·23·24] ➔ >:+9 =:+1 <:+5) \n", + " =:[3·6·8·11·14·21·24 ⟘ 5·7·10·16·18·19·25] ➔ >:+3 =:-20 <:-24) \n", + " <:[1·2·5·10·12·14·19·21·23 ⟘ 6·7·9·11·13·18·20·24·25] ➔ >:-18 =:-22 <:+7)) \n", + " =:[7·9·10·15·17·20·25 ⟘ 2·3·11·12·16·18·23] ➔ \n", + " >:[4·6·11·12·13·19·22·23 ⟘ 9·16·17·18·20·21·24·25] ➔ >:-16 =:+15 <:-12) \n", + " =:[1·2·6·9·15·17·21·23·25 ⟘ 3·8·10·11·12·13·14·19·22] ➔ >:-14 =:0 <:+13) \n", + " <:[11·18·19 ⟘ 3·13·20] ➔ >:+11 =:-10 <:0)) \n", + " <:[2·9·12·13·17·20·23 ⟘ 4·5·15·18·21·22·25] ➔ \n", + " >:[1·2·10·14·16·17·21·25 ⟘ 5·7·15·18·19·22·23·24] ➔ >:+17 =:-4 <:+23) \n", + " =:[1·4·8·11·13·21 ⟘ 2·5·6·18·23·24] ➔ >:-6 =:+19 <:-8) \n", + " <:[6·9·14·17·21·23 ⟘ 5·12·15·18·20·25] ➔ >:+21 =:-2 <:+25)))\n" + ] + } + ], + "source": [ + "p = Puzzle(25, 3)\n", + "p.oddballs = {(+b if b % 2 else -b) for b in p.balls} | {0}\n", + "\n", + "do(p)" + ] + }, + { + "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 *two or more* 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", + "- What else can you discover?\n" ] } ],