Add files via upload

This commit is contained in:
Peter Norvig 2017-05-15 01:01:54 -07:00 committed by GitHub
parent 9717145ad9
commit 8cfde5f40a

936
Golomb.ipynb Normal file
View File

@ -0,0 +1,936 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# OLD VERSION: Sol Golombs Rectangle Puzzle"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This problem was presented by Gary Antonik in his 14/4/14 New York Times [Numberplay column](http://wordplay.blogs.nytimes.com/2014/04/14/rectangle). \n",
"\n",
">Say youre given the following challenge: create a set of five rectangles that have sides of length 1, 2, 3, 4, 5, 6, 7, 8, 9 and 10 units. You can combine sides in a variety of ways: for example, you could create a set of rectangles with dimensions 1 x 3, 2 x 4, 5 x 7, 6 x 8 and 9 x 10.\n",
">\n",
">1. How many different sets of five rectangles are possible?\n",
">\n",
">2. What are the maximum and minimum values for the total areas of the five rectangles?\n",
"<p><b>Bonus Challenges</b><p>\n",
">\n",
">3. What other values for the total areas of the five rectangles are possible?\n",
">\n",
">4. Which sets of rectangles may be assembled to form a square?\n",
"\n",
"To me, these are interesting questions because, first, I have personal connections to Solomon Golomb (my former colleague at USC) and to puzzle author Nelson Blachman (the father of my colleague Nancy Blachman)and to Antonik (a fun guy I've met with), and second, I find it interesting that the problems span the range from mathematical to computational. I wonder how the way I look at the problems compares to others who have a different mix of these two disciplines."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 1. How many different sets of five rectangles are possible?"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This is a basic [combinatorics](http://en.wikipedia.org/wiki/Combinatorics) or counting problem. I will present *three* methods to count the sets. If all goes right, they will give the same answer. The example set of rectangles given in the problem was\n",
"\n",
"> {1 &times; 3, 2 &times; 4, 5 &times; 7, 6 &times; 8, 9 &times; 10}\n",
" \n",
"and in general it would be\n",
"\n",
"> {A &times; B, C &times; D, E &times; F, G &times; H, I &times; J}\n",
"\n",
"The question is: how many distinct ways can we assign the numbers 1 through 10 to the variables A through J?\n",
" \n",
"**Method 1: Count all permutations and divide by repetitions:** There are 10 variables to be filled, so there are 10! = 3,628,800 permutations. But if we fill the first two variables with 1 &times; 3, that is the same rectangle as 3 &times; 1. So divide 10! by 2<sup>5</sup> to account for the fact that each of 5 rectangles can appear 2 ways. Similarly, if we fill A and B with 1 &times; 3, that yields the same set as if we filled C and D with 1 &times; 3. So divide again by 5! (the number of permutations of 5 things) to account for this.\n",
"That gives us <a href=\"https://www.google.com/search?ie=UTF-8#q=10!+%2F+2%5E5+/+5!\">10! / 2<sup>5</sup> / 5!</a> = 945. (It is always gratifying when this \"count and divide\" method comes out to a whole number.)\n",
"\n",
"**Method 2: Count without repetitions**: The example set is presented in [lexicographical order](http://en.wikipedia.org/wiki/Lexicographical_order): in each rectangle the smaller component is listed first, and in the set, the rectangles with smaller first components are listed first. An alternate to \"count and divide\" is to count directly how many sets there are in lexicographical order. We'll work from left to right. How many choices are there for variable A? Only one: A must always be 1, because we agreed that the smallest number comes first. Then, given A, there are 9 remaining choices for B. For C, given A and B, there is again only one choice: C must be the smallest of the remaining 8 numbers (it will be 3 if the first rectangle was 1 &times; 2; otherwise it will be 2, but either way there is only one choice). That leaves 7 choices for D, 5 for F, 3 for H and 1 for J. And 9 &times; 7 &times; 5 &times; 3 &times; 1 = 945. (It is always gratifying when two methods give the same answer.)\n",
" \n",
"**Method 3: Write a program to enumerate the sets:** We'll represent the 1 &times; 3 rectangle as the tuple `(1, 3)` and the example set of rectangles as the set\n",
"\n",
"> {(1, 3), (2, 4), (5, 7), (6, 8), (9, 10)}\n",
"\n",
"To generate all the sets of rectangles, we'll follow method 2: given a set of sides, first make all possible rectangles `(A, B)` where `A` is the shortest side, and `B` ranges over all the other sides. For each of `(A, B)` rectangle, consider all possible `(C, D)` rectangles, and so on. This is easy to write out if we know there will be exactly 5 rectangles in a set:"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"def sets_of_rectangles(sides):\n",
" \"Given exactly 10 sides, yield all sets of rectangles that can be made from them.\"\n",
" for (A, B) in each_rectangle(sides):\n",
" for (C, D) in each_rectangle(sides - {A, B}):\n",
" for (E, F) in each_rectangle(sides - {A, B, C, D}):\n",
" for (G, H) in each_rectangle(sides - {A, B, C, D, E, F}):\n",
" for (I, J) in each_rectangle(sides - {A, B, C, D, E, F, G, H}):\n",
" yield {(A, B), (C, D), (E, F), (G, H), (I, J)}\n",
" \n",
"def each_rectangle(sides):\n",
" \"Yield all (A, B) pairs, where A is minimum of sides, and B ranges over all other sides.\"\n",
" A = min(sides)\n",
" for B in (sides - {A}):\n",
" yield (A, B) "
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"def sets_of_rectangles(sides):\n",
" \"Given exactly 10 sides, yield all sets of rectangles that can be made from them.\"\n",
" return [{(A, B), (C, D), (E, F), (G, H), (I, J)}\n",
" for (A, B) in each_rectangle(sides)\n",
" for (C, D) in each_rectangle(sides - {A, B})\n",
" for (E, F) in each_rectangle(sides - {A, B, C, D})\n",
" for (G, H) in each_rectangle(sides - {A, B, C, D, E, F})\n",
" for (I, J) in each_rectangle(sides - {A, B, C, D, E, F, G, H})]\n",
" \n",
"def each_rectangle(sides):\n",
" \"Yield all (A, B) pairs, where A is minimum of sides, and B ranges over all other sides.\"\n",
" A = min(sides)\n",
" return [(A, B)\n",
" for B in sides if B is not A] "
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"945"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"sides = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}\n",
"sets = sets_of_rectangles(sides)\n",
"len(sets)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"(Gratifying that once again we get the same answer.) But I don't like the fact that we are limited to exactly 10 sides, and that some of the code is [repeated](http://en.wikipedia.org/wiki/Don't_repeat_yourself) five times. So let's convert the nested-iterated algorithm into a *recursive* algorithm that chooses the first rectangle (in the same way as before), but then recursively solves for the remaining sides and unions the set of rectangles from the remaining sides with the first rectangle. (Note we have to also correctly handle the case when there are no sides; then there is one way to make a set of rectangles: the empty set.)"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"def sets_of_rectangles(sides):\n",
" \"Given a set of sides, yield all sets of rectangles that can be made from sides.\"\n",
" if sides:\n",
" A = min(sides)\n",
" return [{(A, B)}.union(rest)\n",
" for B in (sides - {A})\n",
" for rest in sets_of_rectangles(sides - {A, B})]\n",
" else:\n",
" return [set()]"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"[{(1, 2), (3, 4), (5, 6)},\n",
" {(1, 2), (3, 5), (4, 6)},\n",
" {(1, 2), (3, 6), (4, 5)},\n",
" {(1, 3), (2, 4), (5, 6)},\n",
" {(1, 3), (2, 5), (4, 6)},\n",
" {(1, 3), (2, 6), (4, 5)},\n",
" {(1, 4), (2, 3), (5, 6)},\n",
" {(1, 4), (2, 5), (3, 6)},\n",
" {(1, 4), (2, 6), (3, 5)},\n",
" {(1, 5), (2, 3), (4, 6)},\n",
" {(1, 5), (2, 4), (3, 6)},\n",
" {(1, 5), (2, 6), (3, 4)},\n",
" {(1, 6), (2, 3), (4, 5)},\n",
" {(1, 6), (2, 4), (3, 5)},\n",
" {(1, 6), (2, 5), (3, 4)}]"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# For example, with 6 sides:\n",
"sets_of_rectangles({1, 2, 3, 4, 5, 6})"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"945"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"# Reaffirm the answer with 10 sides\n",
"len(sets_of_rectangles(sides))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"I don't want to print all 945 sets, but let's peek at every 100th one:"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"[{(1, 2), (3, 4), (5, 8), (6, 9), (7, 10)},\n",
" {(1, 2), (3, 10), (4, 6), (5, 9), (7, 8)},\n",
" {(1, 3), (2, 10), (4, 9), (5, 7), (6, 8)},\n",
" {(1, 4), (2, 10), (3, 8), (5, 9), (6, 7)},\n",
" {(1, 5), (2, 9), (3, 6), (4, 10), (7, 8)},\n",
" {(1, 6), (2, 9), (3, 10), (4, 7), (5, 8)},\n",
" {(1, 7), (2, 9), (3, 8), (4, 10), (5, 6)},\n",
" {(1, 8), (2, 7), (3, 5), (4, 10), (6, 9)},\n",
" {(1, 9), (2, 7), (3, 10), (4, 6), (5, 8)},\n",
" {(1, 10), (2, 7), (3, 8), (4, 9), (5, 6)}]"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"sets_of_rectangles(sides)[::100]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 2. What are the maximum and minimum values for the total areas of the five rectangles?"
]
},
{
"cell_type": "markdown",
"metadata": {
"slideshow": {
"slide_type": "-"
}
},
"source": [
"I think I know this one, but I'm not completely sure. I know that a rectangle with a fixed perimeter has maximum area when it is a square. My guess is that the maximum total area occurs when each rectangle is *almost* square: a 9 &times; 10 rectangle; 7 &times; 8; and so on. So that would give us a maximum area of:"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"190"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"(1 * 2) + (3 * 4) + (5 * 6) + (7 * 8) + (9 * 10)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"And the minimum area should be when the rectangles deviate the most from squares: a 1 &times; 10, and so on:"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"110"
]
},
"execution_count": 10,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"(1 * 10) + (2 * 9) + (3 * 8) + (4 * 7) + (5 * 6)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Since I am not sure, I will double check by computing the set of all possible areas and then asking for the min and max of the set:"
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"def total_area(rectangles): \n",
" \"The total area of a set of rectangles.\"\n",
" return sum((w * h) for (w, h) in rectangles)\n",
"\n",
"areas = [total_area(s) for s in sets]"
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"190"
]
},
"execution_count": 16,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"max(areas)"
]
},
{
"cell_type": "code",
"execution_count": 17,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"110"
]
},
"execution_count": 17,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"min(areas)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This verifies that the maximum and minimum are what I thought they were. But I still don't think I completely understand the situation. For example, suppose there are *N* sides that are not necessarily consecutive integers. Will the maximum total area always be formed by combining the two biggest sides, and so on?"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 3. What other values for the total areas of the five rectangles are possible?"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"I have no idea how to figure this out mathematically from first principles, but it is easy to compute with the code we already have:"
]
},
{
"cell_type": "code",
"execution_count": 20,
"metadata": {
"collapsed": false
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 177, 178, 179, 180, 181, 182, 183, 184, 186, 187, 190]\n"
]
}
],
"source": [
"print(sorted(set(areas)))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Is there a more succint way to describe these values?"
]
},
{
"cell_type": "code",
"execution_count": 21,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"{176, 185, 188, 189}"
]
},
"execution_count": 21,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"set(range(110, 191)) - set(areas)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Yes: All the integers between 110 and 190 inclusive, except 176, 185, 188, and 189."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## 4. Which sets of rectangles may be assembled to form a square?"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The only way I can think about this is to write a program; I don't see any way to work it out by hand. I do know that the total area will have to be a perfect square, and that in the range of areas (110 to 190) the only perfect squares are 11<sup>2</sup> = 121, 12<sup>2</sup> = 144, and 13<sup>2</sup> = 169. We can find the rectangle sets that have a perfect square as their total area:"
]
},
{
"cell_type": "code",
"execution_count": 22,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"[{(1, 2), (3, 5), (4, 9), (6, 10), (7, 8)},\n",
" {(1, 2), (3, 7), (4, 6), (5, 10), (8, 9)},\n",
" {(1, 2), (3, 7), (4, 9), (5, 6), (8, 10)},\n",
" {(1, 2), (3, 8), (4, 5), (6, 10), (7, 9)},\n",
" {(1, 3), (2, 5), (4, 8), (6, 9), (7, 10)},\n",
" {(1, 3), (2, 7), (4, 5), (6, 10), (8, 9)},\n",
" {(1, 3), (2, 7), (4, 8), (5, 6), (9, 10)},\n",
" {(1, 3), (2, 9), (4, 10), (5, 7), (6, 8)},\n",
" {(1, 3), (2, 10), (4, 7), (5, 9), (6, 8)},\n",
" {(1, 3), (2, 10), (4, 8), (5, 7), (6, 9)},\n",
" {(1, 4), (2, 5), (3, 7), (6, 9), (8, 10)},\n",
" {(1, 5), (2, 3), (4, 9), (6, 7), (8, 10)},\n",
" {(1, 5), (2, 4), (3, 8), (6, 7), (9, 10)},\n",
" {(1, 5), (2, 6), (3, 8), (4, 10), (7, 9)},\n",
" {(1, 5), (2, 7), (3, 4), (6, 8), (9, 10)},\n",
" {(1, 6), (2, 3), (4, 8), (5, 7), (9, 10)},\n",
" {(1, 6), (2, 9), (3, 10), (4, 8), (5, 7)},\n",
" {(1, 6), (2, 10), (3, 8), (4, 9), (5, 7)},\n",
" {(1, 6), (2, 10), (3, 9), (4, 7), (5, 8)},\n",
" {(1, 7), (2, 4), (3, 8), (5, 9), (6, 10)},\n",
" {(1, 7), (2, 9), (3, 5), (4, 6), (8, 10)},\n",
" {(1, 7), (2, 10), (3, 6), (4, 9), (5, 8)},\n",
" {(1, 8), (2, 6), (3, 10), (4, 9), (5, 7)},\n",
" {(1, 8), (2, 7), (3, 10), (4, 6), (5, 9)},\n",
" {(1, 8), (2, 9), (3, 7), (4, 6), (5, 10)},\n",
" {(1, 8), (2, 10), (3, 5), (4, 9), (6, 7)},\n",
" {(1, 9), (2, 5), (3, 7), (4, 6), (8, 10)},\n",
" {(1, 9), (2, 6), (3, 5), (4, 7), (8, 10)},\n",
" {(1, 9), (2, 7), (3, 6), (4, 10), (5, 8)},\n",
" {(1, 9), (2, 7), (3, 8), (4, 6), (5, 10)},\n",
" {(1, 9), (2, 7), (3, 10), (4, 5), (6, 8)},\n",
" {(1, 9), (2, 8), (3, 6), (4, 7), (5, 10)},\n",
" {(1, 10), (2, 4), (3, 5), (6, 8), (7, 9)},\n",
" {(1, 10), (2, 5), (3, 9), (4, 8), (6, 7)},\n",
" {(1, 10), (2, 8), (3, 7), (4, 5), (6, 9)}]"
]
},
"execution_count": 22,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"[s for s in sets if total_area(s) in {121, 144, 169}]"
]
},
{
"cell_type": "code",
"execution_count": 23,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"35"
]
},
"execution_count": 23,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"len(_)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"So 35 out of 945 rectangle sets *might* be assembled into a square; we don't know yet. I would like to see *how* the rectangles are packed into the square, not just *which* sets can be. To start, I'll represent a *Board* as a two-dimensional array: a list of rows, each of which is a list of cells, with the idea that each cell will be covered by a rectangle. We'll initialize each cell to be `empty`:"
]
},
{
"cell_type": "code",
"execution_count": 24,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"empty = (0, 0)\n",
"\n",
"def Board(width, height):\n",
" \"Make an empty board of size width by height.\"\n",
" return [[empty for x in range(width)] for y in range(height)]\n",
"\n",
"def Square(size): return Board(size, size) "
]
},
{
"cell_type": "code",
"execution_count": 25,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"[[(0, 0), (0, 0), (0, 0), (0, 0), (0, 0)],\n",
" [(0, 0), (0, 0), (0, 0), (0, 0), (0, 0)],\n",
" [(0, 0), (0, 0), (0, 0), (0, 0), (0, 0)],\n",
" [(0, 0), (0, 0), (0, 0), (0, 0), (0, 0)],\n",
" [(0, 0), (0, 0), (0, 0), (0, 0), (0, 0)]]"
]
},
"execution_count": 25,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"Square(5)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now we need to be able to place rectangles onto a board. The function `place_rectangle_at` places a single rectangle of width *w* and height *h* onto a board at position (x<sub>0</sub>, y<sub>0</sub>). If the rectangle overlaps a non-empty cell, or goes off the board, we return `None` to indicate that this is not a legal packing. If the rectangle does fit, we return a new board with any old rectangles plus the new one (note we do not modify the old board):"
]
},
{
"cell_type": "code",
"execution_count": 28,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"def place_rectangle_at(rect, board, pos):\n",
" \"\"\"Place the rectangle of size (w, h) onto board at position (x0, y0).\n",
" Return a new board, or None if the rectangle cannot be placed.\"\"\"\n",
" (w, h) = rect\n",
" (x0, y0) = pos\n",
" newboard = [list(row) for row in board] # copy old board\n",
" try:\n",
" for x in range(x0, x0+w):\n",
" for y in range(y0, y0+h):\n",
" if newboard[y][x] is not empty:\n",
" return None \n",
" newboard[y][x] = rect\n",
" return newboard\n",
" except IndexError: # went off the board\n",
" return None"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now we place a 3 &times; 4 rectangle in the upper left corner of the 5 &times; 5 square:"
]
},
{
"cell_type": "code",
"execution_count": 29,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/plain": [
"[[(3, 4), (3, 4), (3, 4), (0, 0), (0, 0)],\n",
" [(3, 4), (3, 4), (3, 4), (0, 0), (0, 0)],\n",
" [(3, 4), (3, 4), (3, 4), (0, 0), (0, 0)],\n",
" [(3, 4), (3, 4), (3, 4), (0, 0), (0, 0)],\n",
" [(0, 0), (0, 0), (0, 0), (0, 0), (0, 0)]]"
]
},
"execution_count": 29,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"place_rectangle_at((3, 4), Square(5), (0, 0))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"That looks right, but it is a bit difficult to read. Let's figure out how to display a board with pretty multi-colored rectangles. I will use some APIs that are specific to IPython, and some HTML and CSS, so feel free to skip these details if you want; all you have to know is that `display_board(board)` causes the board to be displayed.\n",
"\n",
"(If you *are* interested in the details, note that we are creating an HTML table, where each cell (each `<td>` entry) has its own color. We use the color `#f0f0f0`, a light grey, for empty, and for non-empty rectangles, we make the color less red the wider it is, and less green the taller it is (which has the effect that big rectangles are mostly blue). In Python `0xf0` is the hex notation for the integer `240`.)"
]
},
{
"cell_type": "code",
"execution_count": 32,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"from IPython.display import HTML, display\n",
"\n",
"def display_board(board):\n",
" \"Display a representation of this board.\"\n",
" display('Not a legal board' if not board else\n",
" HTML('<table style=\"border: solid black 4px;\">' +\n",
" cat(html_row(r) for r in board) +\n",
" '</table>'))\n",
" \n",
"def color(w, h):\n",
" return \"#{:2x}{:2x}{:2x}\".format(0xf0-w*22, 0xf0-h*22, 0xf0)\n",
" \n",
"def html_row(row): \n",
" return '\\n<tr>' + cat(html_cell(rect) for rect in row)\n",
" \n",
"def html_cell(rect): \n",
" (w, h) = rect\n",
" return ('<td style=\"text-align:center; width:1.3em; height:1.3em;' +\n",
" 'background-color:{}\">{}{}</td>'.format(color(w, h), w%10, h%10))\n",
"\n",
"cat = ''.join"
]
},
{
"cell_type": "code",
"execution_count": 31,
"metadata": {
"collapsed": false
},
"outputs": [
{
"data": {
"text/html": [
"<table style=\"border: solid black 4px;\">\n",
"<tr><td style=\"text-align:center; width:1.3em; height:1.3em;background-color:#ae98f0\">34</td><td style=\"text-align:center; width:1.3em; height:1.3em;background-color:#ae98f0\">34</td><td style=\"text-align:center; width:1.3em; height:1.3em;background-color:#ae98f0\">34</td><td style=\"text-align:center; width:1.3em; height:1.3em;background-color:#f0f0f0\">00</td><td style=\"text-align:center; width:1.3em; height:1.3em;background-color:#f0f0f0\">00</td>\n",
"<tr><td style=\"text-align:center; width:1.3em; height:1.3em;background-color:#ae98f0\">34</td><td style=\"text-align:center; width:1.3em; height:1.3em;background-color:#ae98f0\">34</td><td style=\"text-align:center; width:1.3em; height:1.3em;background-color:#ae98f0\">34</td><td style=\"text-align:center; width:1.3em; height:1.3em;background-color:#f0f0f0\">00</td><td style=\"text-align:center; width:1.3em; height:1.3em;background-color:#f0f0f0\">00</td>\n",
"<tr><td style=\"text-align:center; width:1.3em; height:1.3em;background-color:#ae98f0\">34</td><td style=\"text-align:center; width:1.3em; height:1.3em;background-color:#ae98f0\">34</td><td style=\"text-align:center; width:1.3em; height:1.3em;background-color:#ae98f0\">34</td><td style=\"text-align:center; width:1.3em; height:1.3em;background-color:#f0f0f0\">00</td><td style=\"text-align:center; width:1.3em; height:1.3em;background-color:#f0f0f0\">00</td>\n",
"<tr><td style=\"text-align:center; width:1.3em; height:1.3em;background-color:#ae98f0\">34</td><td style=\"text-align:center; width:1.3em; height:1.3em;background-color:#ae98f0\">34</td><td style=\"text-align:center; width:1.3em; height:1.3em;background-color:#ae98f0\">34</td><td style=\"text-align:center; width:1.3em; height:1.3em;background-color:#f0f0f0\">00</td><td style=\"text-align:center; width:1.3em; height:1.3em;background-color:#f0f0f0\">00</td>\n",
"<tr><td style=\"text-align:center; width:1.3em; height:1.3em;background-color:#f0f0f0\">00</td><td style=\"text-align:center; width:1.3em; height:1.3em;background-color:#f0f0f0\">00</td><td style=\"text-align:center; width:1.3em; height:1.3em;background-color:#f0f0f0\">00</td><td style=\"text-align:center; width:1.3em; height:1.3em;background-color:#f0f0f0\">00</td><td style=\"text-align:center; width:1.3em; height:1.3em;background-color:#f0f0f0\">00</td></table>"
],
"text/plain": [
"<IPython.core.display.HTML object>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"display_board(place_rectangle_at((3, 4), Square(5), (0, 0)))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Much prettier! Here is how to place more than rectangle:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"board1 = Square(10)\n",
"board2 = place_rectangle_at((5, 7), board1, (0, 0))\n",
"board3 = place_rectangle_at((4, 9), board2, (5, 1))\n",
"display_board(board3)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"# Now try to place a third rectangle that does not fit; should fail\n",
"display_board(place_rectangle_at((8, 2), board3, (0, 5)))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now we need a strategy for packing a set of rectangles onto a board. I know that many variants of [bin packing problems](http://en.wikipedia.org/wiki/Bin_packing_problem) are NP-hard, but we only have 5 rectangles, so it should be easy: just exhaustively try each rectangle in each possible position in both possible orientations (horizontal and vertical). We can do this two ways:\n",
"\n",
"> Way 1: Considering the rectangles in a fixed order, try every possible position for each rectangle.\n",
"\n",
"or\n",
"\n",
"> Way 2: Considering the positions in a fixed order, try every possible rectangle for each position.\n",
"\n",
"In Way 1, we could pre-sort the rectangles (say, biggest first). Then we try to put the biggest rectangle in all possible positions on the board, and for each position that fits, try putting the second biggest rectangle in all remaining positions, and so on. \n",
"\n",
"In Way 2, we consider the positions in some fixed order; say top-to-bottom, left-to right. Take the first empty position (the upper left corner). Try putting each of the rectangles there, and for each one that fits, try the next empty position, and consider all possible rectangles to go there, and so on.\n",
"\n",
"Which approach is better? I'm not sure; let's compare. As a wild guess, assume there are on average about 10 ways to place a rectangle on a board. Then Way 1 will look at 10<sup>5</sup> = 100,000 combinations. For Way 2, there are only 5! = 120 permutations of rectangles, and each rectangle can go either horizontaly or verticaly, so that's 5! &times; 2<sup>5</sup> = 3840. Since 3840 &lt; 100,000, I'll go with Way 2. Here is a more precise description:\n",
"\n",
"> Way 2: To **pack** a set of rectangles onto a board, find the first empty cell on the board. Try in turn to place each rectangle (in either orientation) at that position. For each one that fits, try to recursively **pack** the remaining rectangles, and return the resulting board if one of these recursive calls succeeds. If none succeeds, return None. *Details:* If there are no rectangles to pack, the solution is the original board. If the board is illegal, we can't place anything on it, so return None. The order we choose for the first empty cell is arbitrary; we will go top-to-bottom, left-to-right."
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"def pack(rectangles, board):\n",
" \"\"\"Find a way to pack all rectangles onto board and return the packed board,\n",
" or return None if not possible.\"\"\"\n",
" if not rectangles:\n",
" return board # Placing no rectangles give you the original board\n",
" elif not board:\n",
" return None # Can't put rectangles on an illegal board\n",
" else:\n",
" pos = first_empty_cell(board)\n",
" for (w, h) in rectangles: # for each rectangle\n",
" for rect in [(w, h), (h, w)]: # in each orientation\n",
" board2 = place_rectangle_at(rect, board, pos)\n",
" solution = pack(rectangles - {(w, h)}, board2)\n",
" if solution:\n",
" return solution\n",
" return None\n",
" \n",
"def first_empty_cell(board):\n",
" \"The uppermost, leftmost empty cell position on board.\"\n",
" for (y, row) in enumerate(board):\n",
" for (x, cell) in enumerate(row):\n",
" if cell is empty:\n",
" return (x, y)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Let's give it a try:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"display_board(pack({(5, 7), (4, 9)}, Square(10)))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Looks good! We see that the first rectangle was placed in the upper left, then the second was placed at the first empty cell after the first rectangle. \n",
"\n",
"Now we will define the function `display_packable` which takes a collection of sets of rectangles and displays the ones that can be packed into a square. Most of the work is done by `pack`:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": [
"perfect = {i**2:i for i in range(100)}\n",
"\n",
"def packable(sets):\n",
" \"Go through all sets of rectangles and display those that can be packed into a square.\"\n",
" for rectangles in sets:\n",
" A = total_area(rectangles)\n",
" if A in perfect:\n",
" board = pack(rectangles, Square(perfect[A]))\n",
" if board:\n",
" yield board"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Finally, we can answer question 4:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"map(display_board, packable(sets))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"So we see that out of the 945 sets of rectangles, and the 35 whose total area is a perfect square, there are only **four** that are packable. (I had a couple of ideas for how to make this faster, but I won't bother because it is plenty fast enough&mdash;that took less than a second.)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": true
},
"outputs": [],
"source": []
}
],
"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.5.1"
}
},
"nbformat": 4,
"nbformat_minor": 0
}