Add files via upload

This commit is contained in:
Peter Norvig 2019-08-18 13:18:09 -07:00 committed by GitHub
parent e473c800d1
commit 0f093492e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

653
ipynb/TwelveBalls.ipynb Normal file
View File

@ -0,0 +1,653 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"<div align=\"right\"><i>Peter Norvig<br>2012; updated 18 August 2019</i></div>\n",
"\n",
"# Twelve Balls and 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",
"\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",
"- **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",
"- **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&mdash;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",
"\n",
"\n",
"# Implementation\n",
"\n",
"Let's start implementing:"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"from collections import namedtuple\n",
"import random\n",
"\n",
"#### Types\n",
"\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",
" 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",
" \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",
" \"Weigh balls L against balls R, given the oddball; return 'gt', 'eq', or 'lt'.\"\n",
" diff = sum(weight(b, oddball) for b in L) - sum(weight(b, oddball) for b in R)\n",
" return ('gt' if diff > 0 else\n",
" 'lt' if diff < 0 else\n",
" 'eq')\n",
"\n",
"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",
" tree = find_tree(puzzle, puzzle.oddballs, puzzle.weighings)\n",
" assert valid(tree, puzzle)\n",
" return tree\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",
" if isinstance(tree, Oddball):\n",
" return tree\n",
" else:\n",
" result = weigh(tree.L, tree.R, oddball)\n",
" return follow(getattr(tree, result), oddball)\n",
" \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",
" all(follow(tree, oddball) == oddball for oddball in puzzle.oddballs))\n",
"\n",
"def depth(tree) -> int:\n",
" \"Maximum depth of a strategy tree.\"\n",
" return (0 if isinstance(tree, Oddball) else \n",
" 1 + max(depth(tree.gt), depth(tree.eq), depth(tree.lt)))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's try out these functions:"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"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"
]
}
],
"source": [
"p12 = Puzzle(12) # The original puzzle with 12 balls\n",
"\n",
"print(p12.weighings)\n",
"print(p12.balls)\n",
"print(p12.oddballs)"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"'eq'"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# If we weigh balls 1, 2 against 3, 4, and the oddball is that 5 is lighter, the result should be 'eq'\n",
"weigh([1, 2], [3, 4], -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"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"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",
" for odd in oddballs:\n",
" part[weigh(L, R, odd)].append(odd)\n",
" return part"
]
},
{
"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."
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"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]}"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"partition([1, 2], [11, 12], p12.oddballs)"
]
},
{
"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 &times; 3 = 9 possible oddballs, not 16.\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.)"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"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]}"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"partition([1, 2, 3, 4], [9, 10, 11, 12], p12.oddballs)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"So now we have a viable approach to implementing `find_tree`, which is the core of `solve`:\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 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,
"metadata": {},
"outputs": [],
"source": [
"def find_tree(puzzle, oddballs, weighings):\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",
" \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",
" 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",
" return L, R, part\n",
" raise ValueError('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",
" 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:])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Here we see that `good_partition` does its job:"
]
},
{
"cell_type": "code",
"execution_count": 8,
"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]})"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"good_partition(p12, p12.oddballs, 2)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"But it uses `random`, so it won't get the same result every time:"
]
},
{
"cell_type": "code",
"execution_count": 9,
"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]})"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"good_partition(p12, p12.oddballs, 2)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now we're ready to solve puzzles!"
]
},
{
"cell_type": "code",
"execution_count": 10,
"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)))"
]
},
"execution_count": 10,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"solve(p12)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"OK, that's hard to read&mdash;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}'"
]
},
{
"cell_type": "code",
"execution_count": 13,
"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"
]
}
],
"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))"
]
},
{
"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."
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {},
"outputs": [],
"source": [
"def show(tree):\n",
" \"Print a condensed representation tree.\"\n",
" print(pp(tree).replace(', ', ','))\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",
" B = random.choice(range(1, (len(puzzle.balls) - 1) // 3 + 2))\n",
" if oddballs == puzzle.oddballs:\n",
" puzzle.balls.sort()\n",
" else:\n",
" random.shuffle(puzzle.balls) \n",
" return sorted(puzzle.balls[:B]), sorted(puzzle.balls[-B:])"
]
},
{
"cell_type": "code",
"execution_count": 17,
"metadata": {},
"outputs": [
{
"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"
]
}
],
"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):"
]
},
{
"cell_type": "code",
"execution_count": 18,
"metadata": {},
"outputs": [
{
"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"
]
}
],
"source": [
"show(solve(Puzzle(26, 3, {-1})))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"What other puzzles can you solve?"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.2"
}
},
"nbformat": 4,
"nbformat_minor": 2
}