diff --git a/ipynb/Pickleball.ipynb b/ipynb/Pickleball.ipynb index bf9d444..1f38dcc 100644 --- a/ipynb/Pickleball.ipynb +++ b/ipynb/Pickleball.ipynb @@ -4,37 +4,21 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "
Peter Norvig, Updated Sept 2018
\n", + "
Peter Norvig
Sept 2018
\n", "\n", "# Scheduling a Doubles Pickleball Tournament\n", "\n", - "My friend Steve asked for help in creating a schedule for a round-robin doubles pickleball tournament with 8 or 9 players on 2 courts. (*To clarify:* [Pickleball](https://en.wikipedia.org/wiki/Pickleball) is a paddle/ball/net game played on a court that is smaller than tennis. In this type of tournament a player plays with a different partner in each game.) \n", + "My friend Steve asked for help in creating a schedule for a round-robin doubles pickleball tournament with 8 or 9 players on 2 courts. [Pickleball](https://en.wikipedia.org/wiki/Pickleball) is a tennis-like game played on a smaller court. In this type of tournament a player plays with a different partner in each game. To be precise:\n", "\n", - "> Given *P* players and *C* available courts, create a **schedule**: a list of **rounds** of play, where each round consists of from 1 to *C* **games** played simultaneously. Each game pits one **pair** of players against another pair. The **criteria** for a schedule are:\n", + "> Given *P* players and *C* available courts, create a **schedule**: a list of **rounds** of play, where each round consists of up to *C* **games** played simultaneously. Each game pits one **pair** of players against another pair. The **criteria** for a schedule are:\n", + ">\n", + "> 1. A player cannot be scheduled to play twice in the same round.\n", + "2. Each player should partner with each other player once (or as close to that as possible).\n", + "2. Each player should play against each other player twice (or as close to that as possible).\n", + "4. Each court should be filled each round (or as close to that as possible); in other words, fewer rounds are better.\n", "\n", - "> 1. Each player should partner *with* each other player once (or as close to that as possible).\n", - "2. Each player should play *against* each other player twice (or as close to that as possible).\n", - "3. Each court should be filled each round (or as close to that as possible); in other words, fewer rounds are better.\n", - "4. A player *cannot* be scheduled to play twice in the same round.\n", "\n", - "For example, here's a schedule for *P*=8 players on *C*=2 courts. It says that in the first round, players 4 and 6 partner against 2 and 3 on one court, while 5 and 7 partner against 0 and 1 on the other court.\n", - "\n", - " Round 1: | 4,6 vs 2,3 | 5,7 vs 0,1 |\n", - " Round 2: | 0,2 vs 1,3 | 4,5 vs 6,7 |\n", - " Round 3: | 5,6 vs 0,3 | 1,2 vs 4,7 |\n", - " Round 4: | 0,4 vs 3,6 | 2,7 vs 1,5 |\n", - " Round 5: | 0,5 vs 1,4 | 2,6 vs 3,7 |\n", - " Round 6: | 0,6 vs 2,5 | 1,7 vs 3,4 |\n", - " Round 7: | 3,5 vs 1,6 | 2,4 vs 0,7 |\n", - " \n", - "This is a pretty good schedule—it is optimal according to criteria 1, 3, and 4, but it is not optimal in terms of number of times playing each opponent; for example players 1 and 5 play 3 times, not 2. We will see if we can do better. Our overall strategy is as follows:\n", - "\n", - "- To satisfy criterion 1, we will start with a list of all pairs of players. The function `all_pairs` creates this.\n", - "- We will then call `make_games` to take these pairs and put them together into a list of games, strictly enforcing criterion 4 that a player can't be scheduled into both sides of the net in any one game.\n", - "- Next we call `schedule` to take the list of games and put them into a schedule with up to *C* games played at the same time. We will again strictly enforce criterion 4, not allowing a player to appear on two courts at the same time.\n", - "- If this approach does not result in everybody playing everyone else twice we will randomly swap a pair in one game with a pair in another game, and see if this improves things. Keep swapping *N* times.\n", - "\n", - "# Implementation\n", + "# Imports and Vocabulary\n", "\n", "Let's start with some imports and some choices for basic types:" ] @@ -42,57 +26,94 @@ { "cell_type": "code", "execution_count": 1, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ - "from itertools import combinations\n", "from collections import Counter\n", + "from itertools import combinations\n", + "from typing import List, Tuple, Set\n", "import random\n", - "random.seed('reproducible')\n", + "import math\n", + "random.seed(42)\n", "\n", - "Player = int # A player is an int: `1`\n", - "Pair = tuple # A pair is a tuple of two players who are partners: `(1, 2)`\n", - "Game = list # A game is a list of two pairs: `[(1, 2), (3, 4)]`\n", - "Round = tuple # A round is a tuple of games: `([(1, 2), (3, 4)], [(5, 6), (7, 8)])`\n", - "Schedule = list # A schedule is a list of rounds" + "Player = int # A player is an integer: 1\n", + "Pair = tuple # A pair is a tuple of two players who are partners: (1, 2)\n", + "Game = list # A game is a list of two pairs of players: [(1, 2), (3, 4)]\n", + "Round = list # A round is a list of games that happeen at once: [[(1, 2), (3, 4)], [(5, 6), (7, 8)]]\n", + "Schedule = list # A schedule is a list of rounds that happen one after the other." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "# All Pairs of Players\n", + "Here's a schedule for *P*=8 players on *C*=2 courts. It says that in the first round, players 1 and 2 partner against 3 and 4 on one court, while 5 and 6 partner against 7 and 8 on the other court.\n", "\n", - "We will generate all pairs of players (partners) like this:" + " Round 1: | 1,2 vs 3,4 | 5,6 vs 7,8 |\n", + " Round 2: | 1,3 vs 2,4 | 5,7 vs 6,8 |\n", + " Round 3: | 1,4 vs 2,3 | 5,8 vs 6,7 |\n", + " Round 4: | 1,5 vs 2,6 | 3,7 vs 4,8 |\n", + " Round 5: | 1,6 vs 2,5 | 3,8 vs 4,7 |\n", + " Round 6: | 1,7 vs 2,8 | 3,5 vs 4,6 |\n", + " Round 7: | 1,8 vs 2,7 | 3,6 vs 4,5 |\n", + " \n", + "This schedule is optimal according to criteria 1, 2, and 4, but it is not optimal in terms of number of times playing each opponent; for example players 1 and 2 play each other 6 times. We will see if we can do better. \n", + "\n", + "# Tournament Scheduling Algorithm\n", + "\n", + "Here is a strategy to create a schedule:\n", + "\n", + "- Call `all_pairs(P)` to create a list of player pairs in which each pair appears exactly once (for criterion 2).\n", + "- Call `games_from_pairs` to take these pairs and put them together into a list of games (heeding criterion 1).\n", + "- Call `schedule_games` to take the list of games and put them into rounds with up to *C* games played at the same time (heeding criterion 1 again).\n", + "- This approach might not not completely satisfy criteria 3 and 4, but we'll worry about that alter. \n", + "\n", + "Here's the code:" ] }, { "cell_type": "code", "execution_count": 2, - "metadata": { - "collapsed": true - }, + "metadata": {}, "outputs": [], "source": [ - "def all_pairs(P: int) -> [Pair]: return list(combinations(range(P), 2))" + "def tournament(P: int, C=2) -> Schedule:\n", + " \"\"\"Schedule games for a round-robin tournament for P players on C courts.\"\"\"\n", + " return schedule_games(games_from_pairs(all_pairs(P)), C)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Generating Pairs of Players\n", + "\n", + "Each player should partner with each other player once. `all_pairs` produces that list of partners:" ] }, { "cell_type": "code", "execution_count": 3, - "metadata": { - "collapsed": false - }, + "metadata": {}, + "outputs": [], + "source": [ + "def all_pairs(P: int) -> List[Pair]: \n", + " \"\"\"All ways in which two out of P players can partner.\"\"\"\n", + " return list(combinations(range(1, P + 1), 2))" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]" + "[(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]" ] }, - "execution_count": 3, + "execution_count": 4, "metadata": {}, "output_type": "execute_result" } @@ -103,32 +124,30 @@ }, { "cell_type": "code", - "execution_count": 4, - "metadata": { - "collapsed": false - }, + "execution_count": 5, + "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[(0, 1),\n", - " (0, 2),\n", - " (0, 3),\n", - " (0, 4),\n", - " (0, 5),\n", - " (1, 2),\n", + "[(1, 2),\n", " (1, 3),\n", " (1, 4),\n", " (1, 5),\n", + " (1, 6),\n", " (2, 3),\n", " (2, 4),\n", " (2, 5),\n", + " (2, 6),\n", " (3, 4),\n", " (3, 5),\n", - " (4, 5)]" + " (3, 6),\n", + " (4, 5),\n", + " (4, 6),\n", + " (5, 6)]" ] }, - "execution_count": 4, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } @@ -141,73 +160,49 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "This looks good!\n", + "The astute reader may have noticed that `all_pairs(6)` has 15 pairs, an odd number that cannot be evenly combined into games. We must drop one of the pairs, meaning that two players will never partner with each other, and will end up playing one less game than everyone else. Since there are *P* × (*P*-1) / 2 pairs of *P* players, that means there are *P* × (*P*-1) / 4 games, which is a whole number when either *P* or *P*-1 is divisible by 4.\n", "\n", "# Placing Pairs into Games\n", "\n", - "Now let's take those pairs and place them together into games. We'll choose one pair of players, `A`, to play against another pair `B`, making sure that between the two pairs there are four different players. Then we'll try to make `other_games` out of the remaining pairs. If we can't, we'll make a different choice for `B`. " - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": { - "collapsed": false - }, - "outputs": [], - "source": [ - "def make_games(pairs) -> [Game]:\n", - " \"Combine pairs of players into a list of games.\"\n", - " if len(pairs) < 2:\n", - " return []\n", - " A = pairs[0]\n", - " for B in pairs:\n", - " if len(set(A + B)) == 4:\n", - " game = [A, B]\n", - " other_games = make_games([p for p in pairs if p not in game])\n", - " if other_games is not None:\n", - " return [game] + other_games" + "\n", + "To place pairs into games, we'll choose one pair, `pair1`, to play against another pair `pair2`, making sure that between the two pairs there are four different players. Then we'll try to make `other_games` out of the remaining pairs. If we can't, we'll make a different choice for `pair2`. " ] }, { "cell_type": "code", "execution_count": 6, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "data": { - "text/plain": [ - "[[(0, 1), (2, 3)], [(0, 2), (1, 3)], [(0, 3), (1, 2)]]" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], + "metadata": {}, + "outputs": [], "source": [ - "make_games(all_pairs(4))" + "def games_from_pairs(pairs) -> List[Game]:\n", + " \"Combine pairs of players into a list of games.\"\n", + " if len(pairs) < 2:\n", + " return []\n", + " pair1 = pairs[0]\n", + " for pair2 in pairs:\n", + " if len(set(pair1 + pair2)) == 4:\n", + " game = [pair1, pair2]\n", + " other_games = games_from_pairs([p for p in pairs if p != pair1 and p != pair2])\n", + " if other_games is not None:\n", + " return [game, *other_games]\n", + " return None" ] }, { "cell_type": "code", "execution_count": 7, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[[(0, 1), (2, 3)],\n", - " [(0, 2), (1, 3)],\n", - " [(0, 3), (1, 2)],\n", - " [(0, 4), (1, 5)],\n", - " [(0, 5), (1, 4)],\n", - " [(2, 4), (3, 5)],\n", - " [(2, 5), (3, 4)]]" + "[[(1, 2), (3, 4)],\n", + " [(1, 3), (2, 4)],\n", + " [(1, 4), (2, 3)],\n", + " [(1, 5), (2, 6)],\n", + " [(1, 6), (2, 5)],\n", + " [(3, 5), (4, 6)],\n", + " [(3, 6), (4, 5)]]" ] }, "execution_count": 7, @@ -216,40 +211,31 @@ } ], "source": [ - "make_games(all_pairs(6))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The astute reader may have noticed that `all_pairs(6)` has 15 pairs, and from that we can only make 7 games, not 7.5. We must drop one of the pairs, meaning that two players will never partner with each other, and will end up playing one less game than everyone else. Since there are *P* × (*P*-1) / 2 pairs of *P* players, that means there are *P* × (*P*-1) / 4 games, which is a whole number when either *P* or *P*-1 is divisble by 4." + "games_from_pairs(all_pairs(6))" ] }, { "cell_type": "code", "execution_count": 8, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[[(0, 1), (2, 3)],\n", - " [(0, 2), (1, 3)],\n", - " [(0, 3), (1, 2)],\n", - " [(0, 4), (1, 5)],\n", - " [(0, 5), (1, 4)],\n", - " [(0, 6), (1, 7)],\n", - " [(0, 7), (1, 6)],\n", - " [(2, 4), (3, 5)],\n", - " [(2, 5), (3, 4)],\n", - " [(2, 6), (3, 7)],\n", - " [(2, 7), (3, 6)],\n", - " [(4, 5), (6, 7)],\n", - " [(4, 6), (5, 7)],\n", - " [(4, 7), (5, 6)]]" + "[[(1, 2), (3, 4)],\n", + " [(1, 3), (2, 4)],\n", + " [(1, 4), (2, 3)],\n", + " [(1, 5), (2, 6)],\n", + " [(1, 6), (2, 5)],\n", + " [(1, 7), (2, 8)],\n", + " [(1, 8), (2, 7)],\n", + " [(3, 5), (4, 6)],\n", + " [(3, 6), (4, 5)],\n", + " [(3, 7), (4, 8)],\n", + " [(3, 8), (4, 7)],\n", + " [(5, 6), (7, 8)],\n", + " [(5, 7), (6, 8)],\n", + " [(5, 8), (6, 7)]]" ] }, "execution_count": 8, @@ -258,7 +244,7 @@ } ], "source": [ - "make_games(all_pairs(8))" + "games_from_pairs(all_pairs(8))" ] }, { @@ -267,57 +253,58 @@ "source": [ "That looks good. \n", "\n", - "# Scheduling Games onto Courts\n", + "# Scheduling Games into Rounds on Courts\n", "\n", - "Now we need to schedule games onto courts in rounds, such that no player plays twice in any round, and we take as few rounds as possible. We'll define `schedule(games, courts)` to produce a `list` of rounds, where each round is a tuple of up to `courts` games. We'll use a greedy approach to assigning games to rounds; this does *not* guarantee the shortest possible schedule." + "Now we need to take the games and schedule them such that no player plays twice in any round, and we take as few rounds as possible. We'll define `schedule_games(games, C)` to do this using a greedy approach where we start with an empty schedule (with no rounds), and each game is assigned to the first round where it fits, or if it doesn't fit in any existing round, add a new round. This does *not* guarantee the shortest possible schedule." ] }, { "cell_type": "code", "execution_count": 9, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ - "def schedule(games, courts=2):\n", - " \"Schedule games onto courts; return a list of rounds.\"\n", - " games = list(games) # Don't modify the input\n", - " sched = []\n", - " while games:\n", - " round = []\n", - " # A round gets up to `courts` games, all with disjoint players.\n", - " for game in list(games):\n", - " if len(round) < courts and disjoint(players(round), players(game)):\n", - " round.append(game)\n", - " games.remove(game)\n", - " sched.append(Round(round))\n", - " return sched\n", + "def schedule_games(games, C=2) -> Schedule:\n", + " \"Schedule games onto courts in rounds.\"\n", + " schedule = Schedule() # Start with an empty schedule\n", + " for game in games:\n", + " round = first(round for round in schedule \n", + " if len(round) < C and not (players(round) & players(game)))\n", + " if not round: # Add new round\n", + " round = Round()\n", + " schedule.append(round)\n", + " round.append(game)\n", + " return schedule\n", "\n", - "def disjoint(A, B): return not (A & B)\n", + "def players(x) -> Set[Player]:\n", + " \"The set of players in a Pair, Game, Round, or Schedule.\"\n", + " return set(x) if isinstance(x, Pair) else set().union(*map(players, x))\n", "\n", - "def players(x):\n", - " \"The set of players in a pair, game, round, or schedule.\"\n", - " return ({x} if isinstance(x, Player) else set().union(*map(players, x)))" + "def first(iterable): return next(iter(iterable), None)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Remember that `tournament(P, C)` does `schedule_games(games_from_pairs(all_pairs(P)), C)`." ] }, { "cell_type": "code", "execution_count": 10, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[([(0, 1), (2, 3)], [(4, 5), (6, 7)]),\n", - " ([(0, 2), (1, 3)], [(4, 6), (5, 7)]),\n", - " ([(0, 3), (1, 2)], [(4, 7), (5, 6)]),\n", - " ([(0, 4), (1, 5)], [(2, 6), (3, 7)]),\n", - " ([(0, 5), (1, 4)], [(2, 7), (3, 6)]),\n", - " ([(0, 6), (1, 7)], [(2, 4), (3, 5)]),\n", - " ([(0, 7), (1, 6)], [(2, 5), (3, 4)])]" + "[[[(1, 2), (3, 4)]],\n", + " [[(1, 3), (2, 4)]],\n", + " [[(1, 4), (2, 3)]],\n", + " [[(1, 5), (2, 6)]],\n", + " [[(1, 6), (2, 5)]],\n", + " [[(3, 5), (4, 6)]],\n", + " [[(3, 6), (4, 5)]]]" ] }, "execution_count": 10, @@ -326,243 +313,221 @@ } ], "source": [ - "schedule(make_games(all_pairs(8)))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "That looks pretty good—we fit all the games into the minimum number of rounds. But the opponents are not evenly distributed. For example, player 0 and player 1 play against each other in every round except the first (where they are partners).\n", - "\n", - "How can we improve that? We could try *shuffling* the pairs before we make games. " + "tournament(6, 1)" ] }, { "cell_type": "code", - "execution_count": 22, - "metadata": { - "collapsed": false - }, + "execution_count": 11, + "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "[([(0, 6), (3, 5)],),\n", - " ([(5, 6), (4, 7)],),\n", - " ([(0, 4), (1, 5)],),\n", - " ([(3, 4), (2, 7)],),\n", - " ([(1, 3), (4, 6)], [(0, 2), (5, 7)]),\n", - " ([(6, 7), (1, 2)],),\n", - " ([(3, 7), (1, 4)], [(2, 6), (0, 5)]),\n", - " ([(3, 6), (2, 4)],),\n", - " ([(4, 5), (0, 1)],),\n", - " ([(0, 7), (1, 6)],),\n", - " ([(2, 5), (0, 3)],),\n", - " ([(1, 7), (2, 3)],)]" + "[[[(1, 2), (3, 4)], [(5, 6), (7, 8)]],\n", + " [[(1, 3), (2, 4)], [(5, 7), (6, 8)]],\n", + " [[(1, 4), (2, 3)], [(5, 8), (6, 7)]],\n", + " [[(1, 5), (2, 6)], [(3, 7), (4, 8)]],\n", + " [[(1, 6), (2, 5)], [(3, 8), (4, 7)]],\n", + " [[(1, 7), (2, 8)], [(3, 5), (4, 6)]],\n", + " [[(1, 8), (2, 7)], [(3, 6), (4, 5)]]]" ] }, - "execution_count": 22, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "def shuffled(iterable):\n", - " \"Return a shuffled list of the items in iterable.\"\n", - " items = list(iterable)\n", - " random.shuffle(items)\n", - " return items \n", - "\n", - "schedule(make_games(shuffled(all_pairs(8))))" + "tournament(8, 2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Clearly that made things worse in terms of the number of rounds. But did it even out the distribution of opponents? I'll define a function, `report` to make it easier to see what is going on:" + "Are these good schedules? I'll need some analysis to tell.\n", + "\n", + "# Visualizing a Schedule\n", + "\n", + "I'll define a function, `report` to make it easier to see what is going on:" ] }, { "cell_type": "code", "execution_count": 12, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [], "source": [ "def report(sched):\n", " \"Print information about this schedule.\"\n", " for i, round in enumerate(sched, 1):\n", - " print('Round {:2}: | {} |'.format(i, games_str(round)))\n", - " games = sum(sched, ())\n", - " P = len(players(sched))\n", - " opp = opponents(games)\n", - " fmt = ('{:2X}|' + P * ' {}' + ' {:g}').format\n", + " print(f'Round {i:2}: | {str_from_round(round)} |')\n", + " games = sum(sched, [])\n", + " people = sorted(players(sched))\n", + " P = len(people)\n", + " opp = opponent_counts(games)\n", + " fmt = ('{:2}|' + P * ' {}' + ' {:g}').format\n", " print('\\nNumber of times each player plays against each opponent:\\n')\n", - " print(' |', *map('{:X}'.format, range(P)), ' Games')\n", + " print(' |', *map(name, people), ' Games')\n", " print('--+' + '--' * P + ' -----')\n", - " for row in range(P):\n", - " counts = [opp[pairing(row, col)] for col in range(P)]\n", - " print(fmt(row, *[c or '-' for c in counts], sum(counts) / 2))\n", + " for row in people:\n", + " counts = [opp[pairing(row, col)] for col in people]\n", + " print(fmt(name(row), *[c or '-' for c in counts], sum(counts) / 2))\n", + " print('\\nSummary of counts in table:', \n", + " '; '.join(f'{t}: {c}' for t, c in Counter(opp.values()).most_common()))\n", " \n", - "def games_str(round):\n", + "def str_from_round(round) -> str:\n", " \"A string representing a round of games.\"\n", - " return ' | '.join('{:X},{:X} vs {:X},{:X}'\n", - " .format(a, b, c, d) for ((a, b), (c, d)) in round)\n", + " return ' | '.join(f'{name(a)},{name(b)} vs {name(c)},{name(d)}'\n", + " for [(a, b), (c, d)] in round)\n", " \n", - "def opponents(games):\n", + "def opponent_counts(games) -> Counter:\n", " \"A Counter of {(player, opponent): times_played}.\"\n", " return Counter(pairing(p1, p2) for A, B in games for p1 in A for p2 in B)\n", "\n", - "def pairing(p1, p2): return min(p1, p2), max(p1, p2)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now we can compare the shuffled and non-shuffled versions:" + "def name(player) -> str:\n", + " \"\"\"A one-character string representing the player.\"\"\"\n", + " return '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'[player]\n", + "\n", + "def pairing(p1, p2) -> frozenset: return tuple(sorted((p1, p2)))" ] }, { "cell_type": "code", "execution_count": 13, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Round 1: | 0,1 vs 2,3 | 4,5 vs 6,7 |\n", - "Round 2: | 0,2 vs 1,3 | 4,6 vs 5,7 |\n", - "Round 3: | 0,3 vs 1,2 | 4,7 vs 5,6 |\n", - "Round 4: | 0,4 vs 1,5 | 2,6 vs 3,7 |\n", - "Round 5: | 0,5 vs 1,4 | 2,7 vs 3,6 |\n", - "Round 6: | 0,6 vs 1,7 | 2,4 vs 3,5 |\n", - "Round 7: | 0,7 vs 1,6 | 2,5 vs 3,4 |\n", + "Round 1: | 1,2 vs 3,4 | 5,6 vs 7,8 |\n", + "Round 2: | 1,3 vs 2,4 | 5,7 vs 6,8 |\n", + "Round 3: | 1,4 vs 2,3 | 5,8 vs 6,7 |\n", + "Round 4: | 1,5 vs 2,6 | 3,7 vs 4,8 |\n", + "Round 5: | 1,6 vs 2,5 | 3,8 vs 4,7 |\n", + "Round 6: | 1,7 vs 2,8 | 3,5 vs 4,6 |\n", + "Round 7: | 1,8 vs 2,7 | 3,6 vs 4,5 |\n", "\n", "Number of times each player plays against each opponent:\n", "\n", - " | 0 1 2 3 4 5 6 7 Games\n", + " | 1 2 3 4 5 6 7 8 Games\n", "--+---------------- -----\n", - " 0| - 6 2 2 1 1 1 1 7\n", - " 1| 6 - 2 2 1 1 1 1 7\n", - " 2| 2 2 - 6 1 1 1 1 7\n", - " 3| 2 2 6 - 1 1 1 1 7\n", - " 4| 1 1 1 1 - 6 2 2 7\n", - " 5| 1 1 1 1 6 - 2 2 7\n", - " 6| 1 1 1 1 2 2 - 6 7\n", - " 7| 1 1 1 1 2 2 6 - 7\n" + "1 | - 6 2 2 1 1 1 1 7\n", + "2 | 6 - 2 2 1 1 1 1 7\n", + "3 | 2 2 - 6 1 1 1 1 7\n", + "4 | 2 2 6 - 1 1 1 1 7\n", + "5 | 1 1 1 1 - 6 2 2 7\n", + "6 | 1 1 1 1 6 - 2 2 7\n", + "7 | 1 1 1 1 2 2 - 6 7\n", + "8 | 1 1 1 1 2 2 6 - 7\n", + "\n", + "Summary of counts in table: 1: 16; 2: 8; 6: 4\n" ] } ], "source": [ - "report(schedule(make_games(all_pairs(8))))" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "metadata": { - "collapsed": false - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Round 1: | 1,4 vs 0,5 |\n", - "Round 2: | 5,7 vs 3,4 |\n", - "Round 3: | 3,5 vs 2,6 |\n", - "Round 4: | 1,5 vs 2,7 |\n", - "Round 5: | 3,6 vs 0,1 |\n", - "Round 6: | 0,6 vs 2,3 |\n", - "Round 7: | 6,7 vs 1,3 | 4,5 vs 0,2 |\n", - "Round 8: | 4,6 vs 1,7 |\n", - "Round 9: | 3,7 vs 2,4 |\n", - "Round 10: | 4,7 vs 1,6 |\n", - "Round 11: | 5,6 vs 0,3 |\n", - "Round 12: | 2,5 vs 0,7 |\n", - "Round 13: | 1,2 vs 0,4 |\n", - "\n", - "Number of times each player plays against each opponent:\n", - "\n", - " | 0 1 2 3 4 5 6 7 Games\n", - "--+---------------- -----\n", - " 0| - 2 3 2 2 3 2 - 7\n", - " 1| 2 - 1 1 3 1 3 3 7\n", - " 2| 3 1 - 2 2 3 1 2 7\n", - " 3| 2 1 2 - 1 2 4 2 7\n", - " 4| 2 3 2 1 - 2 1 3 7\n", - " 5| 3 1 3 2 2 - 1 2 7\n", - " 6| 2 3 1 4 1 1 - 2 7\n", - " 7| - 3 2 2 3 2 2 - 7\n" - ] - } - ], - "source": [ - "report(schedule(make_games(shuffled(all_pairs(8)))))" + "report(tournament(8, 2))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "We see that shuffling helps a lot in terms of evening out the opponents, but it does a bad job of filling both courts on each round. \n", + "That looks decent—we fill both courts on every round, and every player plays 7 games. But the opponents are not evenly distributed. For example, player 1 plays against player 2 in 6 of the 7 games, and only plays against players 5 through 8 in one game each.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Improving a Schedule through Hillclimbing\n", "\n", - "# `pickleball`: Improvement through Hillclimbing\n", + "How can I improve on criteria 3 and 4 (having each player play each opponent twice, and minimizing the number of rounds)? My strategy will be to start with a non-optimal list of games, and randomly pick a pair from one game and swap them with a pair from another game. Then see if a better schedule can be made with this altered list of games. If it can, keep the altered games; if not, revert the swap. This is called a **hillclimbing** approach; the analogy is that we start out in a valley, take a step in a random direction, and if that is upward, keep going, otherwise step back and try again. Eventually you reach a peak (although it may not be the global peak).\n", "\n", - "My strategy now is to start with a non-optimal schedule, and repeatedly try to improve it by randomly altering the games and seeing if this results in a better schedule. This is called a **hillclimbing** approach; the analogy is that we start out in a valley, take a step in a random direction, and if that is upward, keep going, otherwise step back and try again. Eventually you reach a peak. \n", + "I measure \"better schedule\" both in terms of minimal variation from the optimal distribution of opponents (as measured by `total_difference(games)`) and in terms of the number of rounds (as measured by `len(sched)`). I keep track of both a list of `games` and a complete schedule, `sched`. I make random changes to the `games`, and then re-schedule the games after each change. By default, I allot 200,000 tries of the hillclimbing process, but I can exit early if an optimal schedule is found." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "def tournament2(P, C=2, tries=200_000):\n", + " \"Schedule games for P players on C courts by randomly swapping game opponents N times.\"\n", + " pairs = all_pairs(P)\n", + " games = games_from_pairs(pairs)\n", + " sched = schedule_games(games, C)\n", + " diff = total_difference(games)\n", + " for _ in range(tries):\n", + " if is_optimal(sched, diff, P, C):\n", + " return sched\n", + " # Randomly swap pairs from two games\n", + " ((i, j), _) = idx = random.sample(range(len(games)), 2), (side(), side())\n", + " swap(games, idx)\n", + " diff2 = total_difference(games)\n", + " # Keep the swap if better (or same); revert if worse\n", + " if (diff2 <= diff and len(players(games[i])) == 4 == len(players(games[j]))\n", + " and len(schedule_games(games, C)) <= len(sched)):\n", + " sched, diff = schedule_games(games, C), diff2\n", + " else:\n", + " swap(games, idx) # Swap them back\n", + " return sched\n", "\n", - "In this case I will be picking two games at random, and swapping one pair of partners in one game with one pair of partners in the other. If the swap makes things worse, discard it; if it makes things better, keep it. Either way, try `N` swaps. I measure \"better\" both in terms of minimal variation from the optimal distribution of opponents (as measured by `opp_difference(games, pairs)`) and in terms of the number of rounds (as measured by `len(sched)`). I keep track of both a list of `games` and a complete schedule, `sched`. I make random changes to the `games`, and then re-schedule the games after each change." + "def side() -> int: \"Random side of the net\"; return random.choice((0, 1))\n", + "\n", + "def swap(games, idx):\n", + " \"Swap the pair at games[g1][s1] with the pair at games[g2][s2].\"\n", + " (g1, g2), (s1, s2) = idx\n", + " games[g1][s1], games[g2][s2] = games[g2][s2], games[g1][s1]\n", + "\n", + "def total_difference(games, optimal=2):\n", + " \"The total difference from an optimal distribution of opponents.\"\n", + " return sum(abs(count - optimal) ** 3 \n", + " for count in opponent_counts(games).values())\n", + "\n", + "def is_optimal(schedule, diff, P, C) -> bool:\n", + " \"\"\"Is this schedule with this diff count optimal for P players on C courts?\"\"\"\n", + " return diff == 0 and len(schedule) == math.ceil(P * (P - 1) / 4 / C)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "I'll check that this works for a simple 4-player tournament:" ] }, { "cell_type": "code", "execution_count": 15, - "metadata": { - "collapsed": false - }, - "outputs": [], + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Round 1: | 1,2 vs 3,4 |\n", + "Round 2: | 1,3 vs 2,4 |\n", + "Round 3: | 1,4 vs 2,3 |\n", + "\n", + "Number of times each player plays against each opponent:\n", + "\n", + " | 1 2 3 4 Games\n", + "--+-------- -----\n", + "1 | - 2 2 2 3\n", + "2 | 2 - 2 2 3\n", + "3 | 2 2 - 2 3\n", + "4 | 2 2 2 - 3\n", + "\n", + "Summary of counts in table: 2: 6\n" + ] + } + ], "source": [ - "def pickleball(P, courts=2, N=100000):\n", - " \"Schedule games for P players on C courts by randomly swapping game opponents N times.\"\n", - " pairs = all_pairs(P)\n", - " games = make_games((pairs))\n", - " diff = opp_difference(games, pairs)\n", - " sched = schedule(games, courts)\n", - " for _ in range(N):\n", - " # Randomly swap pairs from two games\n", - " ((i, j), _) = idx = indexes(games)\n", - " swap(games, idx)\n", - " diff2 = opp_difference(games, pairs)\n", - " # Keep the swap if better (or same); revert if worse\n", - " if (diff2 <= diff and len(schedule(games, courts)) <= len(sched) and\n", - " len(players(games[i])) == 4 == len(players(games[j]))):\n", - " sched, diff = schedule(games, courts), diff2\n", - " else:\n", - " swap(games, idx)\n", - " return sched\n", - "\n", - "def indexes(games):\n", - " \"Random indexes into games, and into sides of the net in each game.\"\n", - " sides = ((0, 0), (1, 1), (0, 1), (1, 0))\n", - " return random.sample(range(len(games)), 2), random.choice(sides)\n", - "\n", - "def swap(games, idx):\n", - " \"Swap the partners at games[g1][a] with games[g2][b].\"\n", - " (g1, g2), (a, b) = idx\n", - " games[g1][a], games[g2][b] = games[g2][b], games[g1][a]\n", - "\n", - "def opp_difference(games, pairs, optimal=2):\n", - " \"The total difference from an optimal distribution of opponents.\"\n", - " opp = opponents(games)\n", - " return sum(abs(opp[pair] - optimal) ** 3\n", - " for pair in pairs)" + "report(tournament2(4, 1, 10))" ] }, { @@ -571,100 +536,100 @@ "source": [ "# 8 Player Tournament\n", "\n", - "Let's create an 8-player tournament:" + "Let's try to answer Steve's request for an 8-player tournament:" ] }, { "cell_type": "code", "execution_count": 16, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Round 1: | 0,1 vs 2,3 | 4,6 vs 5,7 |\n", - "Round 2: | 4,5 vs 1,3 | 0,2 vs 6,7 |\n", - "Round 3: | 0,3 vs 5,6 | 4,7 vs 1,2 |\n", - "Round 4: | 2,7 vs 1,5 | 0,4 vs 3,6 |\n", - "Round 5: | 0,5 vs 1,4 | 2,6 vs 3,7 |\n", - "Round 6: | 1,7 vs 0,6 | 3,4 vs 2,5 |\n", - "Round 7: | 0,7 vs 3,5 | 2,4 vs 1,6 |\n", + "Round 1: | 6,7 vs 3,4 | 1,2 vs 5,8 |\n", + "Round 2: | 5,6 vs 7,8 | 2,4 vs 1,3 |\n", + "Round 3: | 1,4 vs 5,7 | 2,3 vs 6,8 |\n", + "Round 4: | 3,8 vs 1,5 | 4,7 vs 2,6 |\n", + "Round 5: | 3,7 vs 4,8 | 1,6 vs 2,5 |\n", + "Round 6: | 3,6 vs 1,7 | 4,5 vs 2,8 |\n", + "Round 7: | 3,5 vs 2,7 | 4,6 vs 1,8 |\n", "\n", "Number of times each player plays against each opponent:\n", "\n", - " | 0 1 2 3 4 5 6 7 Games\n", + " | 1 2 3 4 5 6 7 8 Games\n", "--+---------------- -----\n", - " 0| - 2 1 3 1 2 3 2 7\n", - " 1| 2 - 3 1 3 2 1 2 7\n", - " 2| 1 3 - 2 2 1 2 3 7\n", - " 3| 3 1 2 - 2 3 2 1 7\n", - " 4| 1 3 2 2 - 3 2 1 7\n", - " 5| 2 2 1 3 3 - 1 2 7\n", - " 6| 3 1 2 2 2 1 - 3 7\n", - " 7| 2 2 3 1 1 2 3 - 7\n", - "CPU times: user 29.4 s, sys: 487 ms, total: 29.9 s\n", - "Wall time: 33 s\n" + "1 | - 2 2 2 3 2 1 2 7\n", + "2 | 2 - 2 2 3 2 1 2 7\n", + "3 | 2 2 - 2 1 2 3 2 7\n", + "4 | 2 2 2 - 1 2 3 2 7\n", + "5 | 3 3 1 1 - 1 2 3 7\n", + "6 | 2 2 2 2 1 - 3 2 7\n", + "7 | 1 1 3 3 2 3 - 1 7\n", + "8 | 2 2 2 2 3 2 1 - 7\n", + "\n", + "Summary of counts in table: 2: 16; 3: 6; 1: 6\n" ] } ], "source": [ - "%time report(pickleball(8, 2))" + "report(tournament2(8, 2))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "That's pretty good, but not perfect. In a previous run I was luckier and achieved a perfect schedule for 8 players (where every player plays each opponent exactly twice): " + "That's good, but not perfect. In a previous run I was luckier and achieved a perfect schedule (where every player plays each opponent exactly twice): " ] }, { "cell_type": "code", "execution_count": 17, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Round 1: | 1,6 vs 2,4 | 3,5 vs 7,0 |\n", - "Round 2: | 1,5 vs 3,6 | 2,0 vs 4,7 |\n", - "Round 3: | 2,3 vs 6,0 | 4,5 vs 1,7 |\n", - "Round 4: | 4,6 vs 3,7 | 1,2 vs 5,0 |\n", - "Round 5: | 1,0 vs 6,7 | 3,4 vs 2,5 |\n", - "Round 6: | 2,6 vs 5,7 | 1,4 vs 3,0 |\n", - "Round 7: | 2,7 vs 1,3 | 4,0 vs 5,6 |\n", + "Round 1: | 1,6 vs 2,4 | 3,5 vs 7,8 |\n", + "Round 2: | 1,5 vs 3,6 | 2,8 vs 4,7 |\n", + "Round 3: | 2,3 vs 6,8 | 4,5 vs 1,7 |\n", + "Round 4: | 4,6 vs 3,7 | 1,2 vs 5,8 |\n", + "Round 5: | 1,8 vs 6,7 | 3,4 vs 2,5 |\n", + "Round 6: | 2,6 vs 5,7 | 1,4 vs 3,8 |\n", + "Round 7: | 2,7 vs 1,3 | 4,8 vs 5,6 |\n", "\n", "Number of times each player plays against each opponent:\n", "\n", - " | 0 1 2 3 4 5 6 7 Games\n", + " | 1 2 3 4 5 6 7 8 Games\n", "--+---------------- -----\n", - " 0| - 2 2 2 2 2 2 2 7\n", - " 1| 2 - 2 2 2 2 2 2 7\n", - " 2| 2 2 - 2 2 2 2 2 7\n", - " 3| 2 2 2 - 2 2 2 2 7\n", - " 4| 2 2 2 2 - 2 2 2 7\n", - " 5| 2 2 2 2 2 - 2 2 7\n", - " 6| 2 2 2 2 2 2 - 2 7\n", - " 7| 2 2 2 2 2 2 2 - 7\n" + "1 | - 2 2 2 2 2 2 2 7\n", + "2 | 2 - 2 2 2 2 2 2 7\n", + "3 | 2 2 - 2 2 2 2 2 7\n", + "4 | 2 2 2 - 2 2 2 2 7\n", + "5 | 2 2 2 2 - 2 2 2 7\n", + "6 | 2 2 2 2 2 - 2 2 7\n", + "7 | 2 2 2 2 2 2 - 2 7\n", + "8 | 2 2 2 2 2 2 2 - 7\n", + "\n", + "Summary of counts in table: 2: 28\n" ] } ], "source": [ - "report([\n", - " ([(1, 6), (2, 4)], [(3, 5), (7, 0)]),\n", - " ([(1, 5), (3, 6)], [(2, 0), (4, 7)]),\n", - " ([(2, 3), (6, 0)], [(4, 5), (1, 7)]),\n", - " ([(4, 6), (3, 7)], [(1, 2), (5, 0)]),\n", - " ([(1, 0), (6, 7)], [(3, 4), (2, 5)]),\n", - " ([(2, 6), (5, 7)], [(1, 4), (3, 0)]),\n", - " ([(2, 7), (1, 3)], [(4, 0), (5, 6)]), \n", - "])" + "perfect8: Schedule = [\n", + " [[(1, 6), (2, 4)], [(3, 5), (7, 8)]],\n", + " [[(1, 5), (3, 6)], [(2, 8), (4, 7)]],\n", + " [[(2, 3), (6, 8)], [(4, 5), (1, 7)]],\n", + " [[(4, 6), (3, 7)], [(1, 2), (5, 8)]],\n", + " [[(1, 8), (6, 7)], [(3, 4), (2, 5)]],\n", + " [[(2, 6), (5, 7)], [(1, 4), (3, 8)]],\n", + " [[(2, 7), (1, 3)], [(4, 8), (5, 6)]], \n", + "]\n", + "\n", + "report(perfect8)" ] }, { @@ -679,51 +644,54 @@ { "cell_type": "code", "execution_count": 18, - "metadata": { - "collapsed": false - }, + "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Round 1: | 1,7 vs 4,0 | 3,5 vs 2,6 |\n", - "Round 2: | 2,7 vs 1,3 | 4,8 vs 6,0 |\n", - "Round 3: | 5,0 vs 1,6 | 7,8 vs 3,4 |\n", - "Round 4: | 7,0 vs 5,8 | 1,2 vs 4,6 |\n", - "Round 5: | 3,8 vs 1,5 | 2,0 vs 6,7 |\n", - "Round 6: | 1,4 vs 2,5 | 3,6 vs 8,0 |\n", + "Round 1: | 1,7 vs 4,9 | 3,5 vs 2,6 |\n", + "Round 2: | 2,7 vs 1,3 | 4,8 vs 6,9 |\n", + "Round 3: | 5,9 vs 1,6 | 7,8 vs 3,4 |\n", + "Round 4: | 7,9 vs 5,8 | 1,2 vs 4,6 |\n", + "Round 5: | 3,8 vs 1,5 | 2,9 vs 6,7 |\n", + "Round 6: | 1,4 vs 2,5 | 3,6 vs 8,9 |\n", "Round 7: | 5,6 vs 4,7 | 1,8 vs 2,3 |\n", - "Round 8: | 1,0 vs 3,7 | 2,8 vs 4,5 |\n", - "Round 9: | 3,0 vs 2,4 | 6,8 vs 5,7 |\n", + "Round 8: | 1,9 vs 3,7 | 2,8 vs 4,5 |\n", + "Round 9: | 3,9 vs 2,4 | 6,8 vs 5,7 |\n", "\n", "Number of times each player plays against each opponent:\n", "\n", - " | 0 1 2 3 4 5 6 7 8 Games\n", + " | 1 2 3 4 5 6 7 8 9 Games\n", "--+------------------ -----\n", - " 0| - 2 1 2 2 1 3 3 2 8\n", - " 1| 2 - 3 3 2 2 1 2 1 8\n", - " 2| 1 3 - 3 3 2 2 1 1 8\n", - " 3| 2 3 3 - 1 1 1 2 3 8\n", - " 4| 2 2 3 1 - 2 2 2 2 8\n", - " 5| 1 2 2 1 2 - 3 2 3 8\n", - " 6| 3 1 2 1 2 3 - 2 2 8\n", - " 7| 3 2 1 2 2 2 2 - 2 8\n", - " 8| 2 1 1 3 2 3 2 2 - 8\n" + "1 | - 3 3 2 2 1 2 1 2 8\n", + "2 | 3 - 3 3 2 2 1 1 1 8\n", + "3 | 3 3 - 1 1 1 2 3 2 8\n", + "4 | 2 3 1 - 2 2 2 2 2 8\n", + "5 | 2 2 1 2 - 3 2 3 1 8\n", + "6 | 1 2 1 2 3 - 2 2 3 8\n", + "7 | 2 1 2 2 2 2 - 2 3 8\n", + "8 | 1 1 3 2 3 2 2 - 2 8\n", + "9 | 2 1 2 2 1 3 3 2 - 8\n", + "\n", + "Summary of counts in table: 2: 18; 3: 9; 1: 9\n" ] } ], "source": [ - "report([\n", - " ([(1, 7), (4, 0)], [(3, 5), (2, 6)]),\n", - " ([(2, 7), (1, 3)], [(4, 8), (6, 0)]),\n", - " ([(5, 0), (1, 6)], [(7, 8), (3, 4)]),\n", - " ([(7, 0), (5, 8)], [(1, 2), (4, 6)]),\n", - " ([(3, 8), (1, 5)], [(2, 0), (6, 7)]),\n", - " ([(1, 4), (2, 5)], [(3, 6), (8, 0)]),\n", - " ([(5, 6), (4, 7)], [(1, 8), (2, 3)]),\n", - " ([(1, 0), (3, 7)], [(2, 8), (4, 5)]),\n", - " ([(3, 0), (2, 4)], [(6, 8), (5, 7)]) ])" + "previous9: Schedule = [\n", + " [[(1, 7), (4, 9)], [(3, 5), (2, 6)]],\n", + " [[(2, 7), (1, 3)], [(4, 8), (6, 9)]],\n", + " [[(5, 9), (1, 6)], [(7, 8), (3, 4)]],\n", + " [[(7, 9), (5, 8)], [(1, 2), (4, 6)]],\n", + " [[(3, 8), (1, 5)], [(2, 9), (6, 7)]],\n", + " [[(1, 4), (2, 5)], [(3, 6), (8, 9)]],\n", + " [[(5, 6), (4, 7)], [(1, 8), (2, 3)]],\n", + " [[(1, 9), (3, 7)], [(2, 8), (4, 5)]],\n", + " [[(3, 9), (2, 4)], [(6, 8), (5, 7)]]\n", + "]\n", + "\n", + "report(previous9)" ] }, { @@ -737,65 +705,65 @@ }, { "cell_type": "code", - "execution_count": 20, - "metadata": { - "collapsed": false - }, + "execution_count": 19, + "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Round 1: | 0,1 vs 2,3 | 4,5 vs 6,7 | 8,9 vs D,E | C,F vs A,B |\n", - "Round 2: | 0,2 vs 5,7 | 3,B vs 4,6 | 8,A vs 1,9 | C,D vs E,F |\n", - "Round 3: | 0,3 vs 1,2 | 4,8 vs 5,9 | 6,A vs 7,B | C,E vs D,F |\n", - "Round 4: | 0,4 vs B,C | 2,6 vs A,D | 8,E vs 1,5 | 3,7 vs 9,F |\n", - "Round 5: | 0,5 vs 1,4 | 2,7 vs B,E | A,F vs 9,D | 8,C vs 3,6 |\n", - "Round 6: | 0,6 vs 8,F | A,E vs 3,4 | 1,7 vs B,D | 2,5 vs 9,C |\n", - "Round 7: | 0,7 vs A,C | B,F vs 3,5 | 8,D vs 2,4 | 1,6 vs 9,E |\n", - "Round 8: | 0,9 vs 6,D | 2,B vs 1,8 | 4,E vs 3,A | 5,F vs 7,C |\n", - "Round 9: | 0,A vs 6,E | 5,C vs 3,9 | 1,B vs 4,D | 7,F vs 2,8 |\n", - "Round 10: | 0,B vs 1,A | 2,9 vs 4,F | 3,8 vs 7,D | 5,E vs 6,C |\n", - "Round 11: | 0,C vs 1,D | 2,E vs 5,A | 4,B vs 3,F | 6,9 vs 7,8 |\n", - "Round 12: | 0,D vs 5,B | 2,F vs 3,E | 6,8 vs 1,C | 4,A vs 7,9 |\n", - "Round 13: | 0,E vs 3,D | 1,F vs 6,B | 4,9 vs 2,C | 5,8 vs 7,A |\n", - "Round 14: | 0,F vs 9,A | 2,D vs 5,6 | 8,B vs 3,C | 4,7 vs 1,E |\n", - "Round 15: | 0,8 vs 4,C | 6,F vs 2,A | 9,B vs 7,E | 1,3 vs 5,D |\n", + "Round 1: | 1,2 vs 3,4 | 5,6 vs 7,8 | D,E vs F,G | 9,A vs B,C |\n", + "Round 2: | 1,3 vs 2,4 | 5,7 vs A,G | C,E vs 6,8 | B,F vs 9,D |\n", + "Round 3: | 1,4 vs 2,3 | C,G vs 6,7 | 5,8 vs 9,F | A,E vs B,D |\n", + "Round 4: | A,D vs 2,6 | 1,5 vs 4,7 | 3,E vs 8,9 | B,G vs C,F |\n", + "Round 5: | 1,6 vs 2,5 | D,F vs 4,8 | 9,B vs 3,7 | A,C vs E,G |\n", + "Round 6: | 9,G vs 2,8 | 3,5 vs C,D | 4,6 vs A,F | B,E vs 1,7 |\n", + "Round 7: | 9,C vs 2,7 | D,G vs 4,5 | 1,8 vs A,B | 3,6 vs E,F |\n", + "Round 8: | 1,9 vs 2,A | 3,G vs 8,B | 5,D vs 6,E | 7,C vs 4,F |\n", + "Round 9: | 4,C vs 2,9 | 6,G vs 1,A | 7,F vs 5,E | 3,B vs 8,D |\n", + "Round 10: | 6,F vs 3,9 | 2,C vs 5,G | 7,D vs 1,B | 4,A vs 8,E |\n", + "Round 11: | 5,F vs 2,B | 3,A vs 1,C | 8,G vs 6,D | 4,9 vs 7,E |\n", + "Round 12: | 6,B vs 2,E | 5,C vs 1,D | 3,F vs 4,G | 7,9 vs 8,A |\n", + "Round 13: | 8,F vs 2,D | 3,C vs 6,A | 5,9 vs 4,B | 7,G vs 1,E |\n", + "Round 14: | 6,9 vs 1,F | 5,A vs 4,E | 2,G vs 3,D | 7,B vs 8,C |\n", + "Round 15: | 5,B vs 2,F | 1,G vs 9,E | 6,C vs 4,D | 7,A vs 3,8 |\n", "\n", "Number of times each player plays against each opponent:\n", "\n", - " | 0 1 2 3 4 5 6 7 8 9 A B C D E F Games\n", + " | 1 2 3 4 5 6 7 8 9 A B C D E F G Games\n", "--+-------------------------------- -----\n", - " 0| - 4 2 2 2 2 2 1 1 1 3 2 3 3 1 1 15\n", - " 1| 4 - 2 2 2 2 2 1 3 1 1 4 1 3 2 - 15\n", - " 2| 2 2 - 2 2 3 2 2 2 2 2 1 1 2 2 3 15\n", - " 3| 2 2 2 - 3 2 1 1 2 1 1 3 2 2 3 3 15\n", - " 4| 2 2 2 3 - 2 1 2 2 3 2 3 2 1 2 1 15\n", - " 5| 2 2 3 2 2 - 2 3 2 2 1 1 3 2 2 1 15\n", - " 6| 2 2 2 1 1 2 - 2 3 2 3 2 2 2 2 2 15\n", - " 7| 1 1 2 1 2 3 2 - 3 3 3 3 1 1 2 2 15\n", - " 8| 1 3 2 2 2 2 3 3 - 3 1 1 3 2 1 1 15\n", - " 9| 1 1 2 1 3 2 2 3 3 - 3 - 2 2 2 3 15\n", - " A| 3 1 2 1 2 1 3 3 1 3 - 2 1 1 3 3 15\n", - " B| 2 4 1 3 3 1 2 3 1 - 2 - 2 2 1 3 15\n", - " C| 3 1 1 2 2 3 2 1 3 2 1 2 - 2 2 3 15\n", - " D| 3 3 2 2 1 2 2 1 2 2 1 2 2 - 3 2 15\n", - " E| 1 2 2 3 2 2 2 2 1 2 3 1 2 3 - 2 15\n", - " F| 1 - 3 3 1 1 2 2 1 3 3 3 3 2 2 - 15\n", - "CPU times: user 2min 4s, sys: 977 ms, total: 2min 5s\n", - "Wall time: 2min 8s\n" + "1 | - 4 3 3 2 2 3 - 2 3 2 1 1 2 - 2 15\n", + "2 | 4 - 3 3 3 2 - 1 3 1 2 2 2 - 2 2 15\n", + "3 | 3 3 - 3 - 2 1 3 2 2 2 2 2 1 2 2 15\n", + "4 | 3 3 3 - 3 1 2 1 2 2 - 2 2 2 3 1 15\n", + "5 | 2 3 - 3 - 2 3 1 1 1 2 2 3 2 3 2 15\n", + "6 | 2 2 2 1 2 - 1 2 1 3 - 3 3 3 3 2 15\n", + "7 | 3 - 1 2 3 1 - 3 3 2 3 3 - 3 1 2 15\n", + "8 | - 1 3 1 1 2 3 - 3 3 3 1 3 2 2 2 15\n", + "9 | 2 3 2 2 1 1 3 3 - 2 3 2 - 2 3 1 15\n", + "A | 3 1 2 2 1 3 2 3 2 - 2 3 1 3 - 2 15\n", + "B | 2 2 2 - 2 - 3 3 3 2 - 2 3 2 3 1 15\n", + "C | 1 2 2 2 2 3 3 1 2 3 2 - 2 1 1 3 15\n", + "D | 1 2 2 2 3 3 - 3 - 1 3 2 - 2 3 3 15\n", + "E | 2 - 1 2 2 3 3 2 2 3 2 1 2 - 2 3 15\n", + "F | - 2 2 3 3 3 1 2 3 - 3 1 3 2 - 2 15\n", + "G | 2 2 2 1 2 2 2 2 1 2 1 3 3 3 2 - 15\n", + "\n", + "Summary of counts in table: 2: 49; 3: 39; 1: 21; 4: 1\n", + "CPU times: user 47 s, sys: 20.8 ms, total: 47 s\n", + "Wall time: 47 s\n" ] } ], "source": [ - "%time report(pickleball(P=16, courts=4))" + "%time report(tournament2(P=16, C=4))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "That's a good schedule! It takes the minimum 15 rounds, and although not all counts are 2, most are in the 1 to 3 range." + "That's a pretty good schedule! It takes the minimum 15 rounds, and although not all counts are 2, most are in the 1 to 3 range." ] }, { @@ -819,10 +787,8 @@ }, { "cell_type": "code", - "execution_count": 67, - "metadata": { - "collapsed": false - }, + "execution_count": 20, + "metadata": {}, "outputs": [ { "data": { @@ -830,12 +796,14 @@ "{4: 15.0,\n", " 5: 945.0,\n", " 8: 2.8845653137679503e+19,\n", - " 9: 7.637693625347176e+27,\n", + " 9: 7.637693625347175e+27,\n", + " 12: 4.375874524269406e+66,\n", + " 13: 2.5327611850776306e+83,\n", " 16: 8.78872489906208e+147,\n", " 17: 1.1985831550364023e+174}" ] }, - "execution_count": 67, + "execution_count": 20, "metadata": {}, "output_type": "execute_result" } @@ -843,15 +811,15 @@ "source": [ "from math import factorial\n", "\n", - "def schedules(P):\n", + "def count_schedules(P):\n", " \"Number of possible schedules for P players with all courts full.\"\n", " G = P * (P - 1) // 4 # Number of games\n", " C = P // 4 # Number of courts\n", " R = G // C # Number of rounds\n", - " return factorial(2 * G) / 2 ** G / factorial(C) ** R / factorial(R)\n", + " return factorial(2 * G) / (2 ** G * factorial(C) ** R * factorial(R))\n", "\n", - "{P: schedules(P) \n", - " for P in (4, 5, 8, 9, 16, 17)}" + "{P: count_schedules(P) \n", + " for P in range(4, 18) if P % 4 in (0, 1)}" ] }, { @@ -878,7 +846,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.0" + "version": "3.7.6" } }, "nbformat": 4, diff --git a/ipynb/SpellingBee.ipynb b/ipynb/SpellingBee.ipynb index 4a199ee..85751a8 100644 --- a/ipynb/SpellingBee.ipynb +++ b/ipynb/SpellingBee.ipynb @@ -361,20 +361,20 @@ { "data": { "text/plain": [ - "[Honeycomb('ACEIORT', 'A'),\n", - " Honeycomb('ACEIORT', 'C'),\n", - " Honeycomb('ACEIORT', 'E'),\n", - " Honeycomb('ACEIORT', 'I'),\n", - " Honeycomb('ACEIORT', 'O'),\n", - " Honeycomb('ACEIORT', 'R'),\n", - " Honeycomb('ACEIORT', 'T'),\n", - " Honeycomb('AEGLMPX', 'A'),\n", + "[Honeycomb('AEGLMPX', 'A'),\n", " Honeycomb('AEGLMPX', 'E'),\n", " Honeycomb('AEGLMPX', 'G'),\n", " Honeycomb('AEGLMPX', 'L'),\n", " Honeycomb('AEGLMPX', 'M'),\n", " Honeycomb('AEGLMPX', 'P'),\n", - " Honeycomb('AEGLMPX', 'X')]" + " Honeycomb('AEGLMPX', 'X'),\n", + " Honeycomb('ACEIORT', 'A'),\n", + " Honeycomb('ACEIORT', 'C'),\n", + " Honeycomb('ACEIORT', 'E'),\n", + " Honeycomb('ACEIORT', 'I'),\n", + " Honeycomb('ACEIORT', 'O'),\n", + " Honeycomb('ACEIORT', 'R'),\n", + " Honeycomb('ACEIORT', 'T')]" ] }, "execution_count": 13, @@ -541,8 +541,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 9.34 ms, sys: 31 µs, total: 9.37 ms\n", - "Wall time: 9.36 ms\n" + "CPU times: user 9.35 ms, sys: 82 µs, total: 9.43 ms\n", + "Wall time: 9.42 ms\n" ] }, { @@ -762,8 +762,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 1.71 s, sys: 1.2 ms, total: 1.71 s\n", - "Wall time: 1.71 s\n" + "CPU times: user 1.75 s, sys: 2.03 ms, total: 1.75 s\n", + "Wall time: 1.75 s\n" ] } ], @@ -877,8 +877,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "CPU times: user 393 ms, sys: 1.31 ms, total: 395 ms\n", - "Wall time: 394 ms\n" + "CPU times: user 382 ms, sys: 864 µs, total: 383 ms\n", + "Wall time: 383 ms\n" ] }, { @@ -1371,10 +1371,10 @@ " \"\"\"The pareto frontier that minimizes word counts while maximizing points.\n", " Returns a list of (wcount, points, honeycomb, points/wcount) entries\n", " such that there is no other entry that has fewer words and more points.\"\"\"\n", - " return [(w, p, h, round(p/w, 2))\n", - " for w, (p, h) in sorted(table.items())\n", + " return sorted((w, p, h, round(p/w, 2))\n", + " for w, (p, h) in table.items()\n", " if not any(h2 != h and w2 <= w and p2 >= p\n", - " for w2, (p2, h2) in table.items())]" + " for w2, (p2, h2) in table.items()))" ] }, { @@ -1402,13 +1402,15 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "So there are 108 (out of 55,902) honeycombs on the Pareto frontier. We can see the first ten (sorted by word count), and every tenth one after that:" + "So there are 108 (out of 55,902) honeycombs on the Pareto frontier. Here they are:" ] }, { "cell_type": "code", "execution_count": 44, - "metadata": {}, + "metadata": { + "scrolled": false + }, "outputs": [ { "data": { @@ -1422,7 +1424,105 @@ " (7, 62, Honeycomb('BGILNOX', 'X'), 8.86),\n", " (8, 67, Honeycomb('DGINOXZ', 'X'), 8.38),\n", " (9, 70, Honeycomb('EFNQRTU', 'Q'), 7.78),\n", - " (10, 84, Honeycomb('CENOQRU', 'Q'), 8.4)]" + " (10, 84, Honeycomb('CENOQRU', 'Q'), 8.4),\n", + " (11, 86, Honeycomb('GINOTUV', 'V'), 7.82),\n", + " (12, 100, Honeycomb('GILMNUZ', 'Z'), 8.33),\n", + " (13, 108, Honeycomb('GINOQTU', 'Q'), 8.31),\n", + " (14, 113, Honeycomb('CINOTXY', 'X'), 8.07),\n", + " (15, 115, Honeycomb('DGINOXZ', 'Z'), 7.67),\n", + " (16, 116, Honeycomb('DEIOPXZ', 'Z'), 7.25),\n", + " (17, 124, Honeycomb('CGINOUV', 'V'), 7.29),\n", + " (18, 136, Honeycomb('GHORTUW', 'W'), 7.56),\n", + " (19, 157, Honeycomb('DEIORXZ', 'X'), 8.26),\n", + " (22, 172, Honeycomb('DEGINPZ', 'Z'), 7.82),\n", + " (23, 184, Honeycomb('ACELQRU', 'Q'), 8.0),\n", + " (25, 189, Honeycomb('DELOPRX', 'X'), 7.56),\n", + " (26, 198, Honeycomb('AILNOTZ', 'Z'), 7.62),\n", + " (28, 224, Honeycomb('DEGINRZ', 'Z'), 8.0),\n", + " (33, 238, Honeycomb('DEIORXZ', 'Z'), 7.21),\n", + " (37, 243, Honeycomb('CGILNYZ', 'G'), 6.57),\n", + " (38, 275, Honeycomb('AGINOTZ', 'Z'), 7.24),\n", + " (41, 279, Honeycomb('CFGHILN', 'G'), 6.8),\n", + " (43, 324, Honeycomb('ACGINTV', 'V'), 7.53),\n", + " (45, 374, Honeycomb('ACINOTV', 'V'), 8.31),\n", + " (55, 385, Honeycomb('ACINOTU', 'U'), 7.0),\n", + " (59, 392, Honeycomb('GHINOTU', 'U'), 6.64),\n", + " (60, 396, Honeycomb('ACGILNZ', 'C'), 6.6),\n", + " (61, 404, Honeycomb('CGHINTW', 'G'), 6.62),\n", + " (62, 424, Honeycomb('CGHINRU', 'G'), 6.84),\n", + " (63, 426, Honeycomb('GINOPRU', 'U'), 6.76),\n", + " (65, 470, Honeycomb('CEINOTV', 'V'), 7.23),\n", + " (69, 485, Honeycomb('ACGINTV', 'C'), 7.03),\n", + " (70, 493, Honeycomb('CEGILNR', 'C'), 7.04),\n", + " (72, 495, Honeycomb('ACGHINR', 'H'), 6.88),\n", + " (73, 496, Honeycomb('CENORTV', 'V'), 6.79),\n", + " (74, 526, Honeycomb('ACILRTU', 'U'), 7.11),\n", + " (79, 530, Honeycomb('FGHILNT', 'G'), 6.71),\n", + " (81, 537, Honeycomb('BEFGINT', 'G'), 6.63),\n", + " (83, 552, Honeycomb('EGIMNPT', 'G'), 6.65),\n", + " (84, 571, Honeycomb('ACGIKNT', 'G'), 6.8),\n", + " (86, 606, Honeycomb('ACGINTV', 'G'), 7.05),\n", + " (89, 615, Honeycomb('ACILNTY', 'Y'), 6.91),\n", + " (92, 617, Honeycomb('CGINOTU', 'G'), 6.71),\n", + " (94, 646, Honeycomb('DELOPRV', 'V'), 6.87),\n", + " (98, 651, Honeycomb('FGILNPU', 'N'), 6.64),\n", + " (99, 674, Honeycomb('ACGINTV', 'T'), 6.81),\n", + " (102, 739, Honeycomb('GINORTU', 'U'), 7.25),\n", + " (110, 746, Honeycomb('CEGILNT', 'G'), 6.78),\n", + " (111, 764, Honeycomb('ACGHINT', 'G'), 6.88),\n", + " (113, 803, Honeycomb('CEGINOT', 'G'), 7.11),\n", + " (120, 823, Honeycomb('ACILRTU', 'C'), 6.86),\n", + " (122, 860, Honeycomb('ACGILNT', 'C'), 7.05),\n", + " (126, 879, Honeycomb('CEGINOT', 'C'), 6.98),\n", + " (131, 882, Honeycomb('CEGINPR', 'G'), 6.73),\n", + " (135, 929, Honeycomb('CEGILNR', 'G'), 6.88),\n", + " (136, 955, Honeycomb('EGINRTV', 'V'), 7.02),\n", + " (140, 994, Honeycomb('CENORTU', 'U'), 7.1),\n", + " (149, 1064, Honeycomb('CEGINRT', 'G'), 7.14),\n", + " (152, 1065, Honeycomb('ACGINOT', 'G'), 7.01),\n", + " (159, 1118, Honeycomb('ACGILNT', 'G'), 7.03),\n", + " (162, 1159, Honeycomb('CENORTU', 'C'), 7.15),\n", + " (166, 1161, Honeycomb('EFGILNR', 'G'), 6.99),\n", + " (169, 1187, Honeycomb('CDEINRT', 'C'), 7.02),\n", + " (172, 1199, Honeycomb('EGINRTW', 'G'), 6.97),\n", + " (180, 1207, Honeycomb('BEGINRT', 'G'), 6.71),\n", + " (181, 1288, Honeycomb('EFGINRT', 'G'), 7.12),\n", + " (183, 1323, Honeycomb('EGINRTV', 'G'), 7.23),\n", + " (188, 1367, Honeycomb('EGIMNRT', 'G'), 7.27),\n", + " (202, 1405, Honeycomb('EGINRTU', 'G'), 6.96),\n", + " (212, 1430, Honeycomb('EGINRTV', 'T'), 6.75),\n", + " (214, 1431, Honeycomb('DEGINRV', 'G'), 6.69),\n", + " (219, 1448, Honeycomb('ACINORT', 'C'), 6.61),\n", + " (221, 1472, Honeycomb('ACILNOT', 'L'), 6.66),\n", + " (223, 1562, Honeycomb('DEFGINR', 'G'), 7.0),\n", + " (230, 1566, Honeycomb('BDEGINR', 'G'), 6.81),\n", + " (231, 1598, Honeycomb('ACGINOT', 'I'), 6.92),\n", + " (234, 1685, Honeycomb('EGILNRT', 'G'), 7.2),\n", + " (238, 1691, Honeycomb('ACILNOT', 'C'), 7.11),\n", + " (244, 1737, Honeycomb('CEINORT', 'C'), 7.12),\n", + " (251, 1774, Honeycomb('ACILNOT', 'I'), 7.07),\n", + " (254, 1787, Honeycomb('ACILNOT', 'N'), 7.04),\n", + " (264, 1788, Honeycomb('DEGILNR', 'N'), 6.77),\n", + " (265, 1830, Honeycomb('DEGINRT', 'G'), 6.91),\n", + " (269, 1847, Honeycomb('CEINORT', 'I'), 6.87),\n", + " (275, 1856, Honeycomb('EGILNRT', 'R'), 6.75),\n", + " (277, 1882, Honeycomb('ACILNOT', 'A'), 6.79),\n", + " (280, 1893, Honeycomb('DEGILNR', 'G'), 6.76),\n", + " (282, 1968, Honeycomb('AGINORT', 'I'), 6.98),\n", + " (283, 1982, Honeycomb('ACEINRT', 'C'), 7.0),\n", + " (288, 2011, Honeycomb('AEGILNT', 'G'), 6.98),\n", + " (297, 2038, Honeycomb('CEINORT', 'N'), 6.86),\n", + " (312, 2117, Honeycomb('AGINORT', 'N'), 6.79),\n", + " (320, 2193, Honeycomb('AEGILNT', 'I'), 6.85),\n", + " (330, 2311, Honeycomb('ACEINRT', 'I'), 7.0),\n", + " (354, 2414, Honeycomb('ACEINRT', 'N'), 6.82),\n", + " (370, 2575, Honeycomb('ADEGINR', 'I'), 6.96),\n", + " (397, 2626, Honeycomb('ADEGINR', 'D'), 6.61),\n", + " (403, 3095, Honeycomb('AEGINRT', 'G'), 7.68),\n", + " (442, 3406, Honeycomb('AEGINRT', 'I'), 7.71),\n", + " (466, 3421, Honeycomb('AEGINRT', 'T'), 7.34),\n", + " (512, 3782, Honeycomb('AEGINRT', 'N'), 7.39),\n", + " (537, 3898, Honeycomb('AEGINRT', 'R'), 7.26)]" ] }, "execution_count": 44, @@ -1431,36 +1531,7 @@ } ], "source": [ - "ph[:10] # (word count, points, honeycomb, points/wcount) " - ] - }, - { - "cell_type": "code", - "execution_count": 45, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[(11, 86, Honeycomb('GINOTUV', 'V'), 7.82),\n", - " (23, 184, Honeycomb('ACELQRU', 'Q'), 8.0),\n", - " (55, 385, Honeycomb('ACINOTU', 'U'), 7.0),\n", - " (73, 496, Honeycomb('CENORTV', 'V'), 6.79),\n", - " (98, 651, Honeycomb('FGILNPU', 'N'), 6.64),\n", - " (135, 929, Honeycomb('CEGILNR', 'G'), 6.88),\n", - " (180, 1207, Honeycomb('BEGINRT', 'G'), 6.71),\n", - " (230, 1566, Honeycomb('BDEGINR', 'G'), 6.81),\n", - " (275, 1856, Honeycomb('EGILNRT', 'R'), 6.75),\n", - " (354, 2414, Honeycomb('ACEINRT', 'N'), 6.82)]" - ] - }, - "execution_count": 45, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "ph[10::10]" + "ph # (word count, points, honeycomb, points/wcount) " ] }, { @@ -1472,7 +1543,7 @@ }, { "cell_type": "code", - "execution_count": 46, + "execution_count": 45, "metadata": {}, "outputs": [ { @@ -1494,7 +1565,7 @@ "def plot(xlabel, X, ylabel, Y): \n", " plt.plot(X, Y, '.'); plt.xlabel(xlabel); plt.ylabel(ylabel); plt.grid(True)\n", " \n", - "plot('Word count', W, 'Points', P, )" + "plot('Word count', W, 'Points', P)" ] }, { @@ -1506,7 +1577,7 @@ }, { "cell_type": "code", - "execution_count": 47, + "execution_count": 46, "metadata": {}, "outputs": [ { @@ -1530,12 +1601,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "We can see all the Pareto optimal honeycombs that score more than, say, 7.6 points per word:" + "So the highest points per word are for honeycombs with very few words. We can see all the Pareto optimal honeycombs that score more than, say, 7.6 points per word:" ] }, { "cell_type": "code", - "execution_count": 48, + "execution_count": 47, "metadata": {}, "outputs": [ { @@ -1566,7 +1637,7 @@ " (442, 3406, Honeycomb('AEGINRT', 'I'), 7.71)]" ] }, - "execution_count": 48, + "execution_count": 47, "metadata": {}, "output_type": "execute_result" } @@ -1579,43 +1650,19 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The last few honeycombs on the right-hand side all rise above the average points/word. We can see that they are all variants of the highest-scoring honeycomb, but with different centers:" - ] - }, - { - "cell_type": "code", - "execution_count": 49, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "[(403, 3095, Honeycomb('AEGINRT', 'G'), 7.68),\n", - " (442, 3406, Honeycomb('AEGINRT', 'I'), 7.71),\n", - " (466, 3421, Honeycomb('AEGINRT', 'T'), 7.34),\n", - " (512, 3782, Honeycomb('AEGINRT', 'N'), 7.39),\n", - " (537, 3898, Honeycomb('AEGINRT', 'R'), 7.26)]" - ] - }, - "execution_count": 49, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "ph[-5:]" + "The last two of these represent our old optimal honeycomb, with two different centers." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Here are reports on what I think are the most interesting low-word-count, higher-score honeycombs. I would have scored zero on the first one, and probably not much better on the second." + "Here are reports on what I think are the most interesting low-word-count, higher-score honeycombs. I would not have been able to find any words at all for the first one, I think:" ] }, { "cell_type": "code", - "execution_count": 50, + "execution_count": 48, "metadata": {}, "outputs": [ { @@ -1638,7 +1685,7 @@ }, { "cell_type": "code", - "execution_count": 51, + "execution_count": 49, "metadata": {}, "outputs": [ { @@ -1670,7 +1717,7 @@ }, { "cell_type": "code", - "execution_count": 52, + "execution_count": 50, "metadata": {}, "outputs": [ { @@ -1710,7 +1757,7 @@ }, { "cell_type": "code", - "execution_count": 53, + "execution_count": 51, "metadata": {}, "outputs": [ { @@ -1766,7 +1813,7 @@ }, { "cell_type": "code", - "execution_count": 54, + "execution_count": 52, "metadata": {}, "outputs": [ { @@ -1775,7 +1822,7 @@ "(98141, 44585)" ] }, - "execution_count": 54, + "execution_count": 52, "metadata": {}, "output_type": "execute_result" } @@ -1796,7 +1843,7 @@ }, { "cell_type": "code", - "execution_count": 55, + "execution_count": 53, "metadata": {}, "outputs": [ {