\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. [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 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",
"\n",
"# Imports and Vocabulary\n",
"\n",
"Let's start with some imports and some choices for basic types:"
]
},
{
"cell_type": "code",
"execution_count": 1,
"metadata": {},
"outputs": [],
"source": [
"from collections import Counter\n",
"from itertools import combinations\n",
"from typing import List, Tuple, Set\n",
"import random\n",
"import math\n",
"random.seed(42)\n",
"\n",
"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": [
"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",
" 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": {},
"outputs": [],
"source": [
"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": {},
"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": [
"[(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"all_pairs(4)"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[(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",
" (3, 6),\n",
" (4, 5),\n",
" (4, 6),\n",
" (5, 6)]"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"all_pairs(6)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"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",
"\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": {},
"outputs": [],
"source": [
"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": {},
"outputs": [
{
"data": {
"text/plain": [
"[[(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,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"games_from_pairs(all_pairs(6))"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[[(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,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"games_from_pairs(all_pairs(8))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"That looks good. \n",
"\n",
"# Scheduling Games into Rounds on Courts\n",
"\n",
"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": {},
"outputs": [],
"source": [
"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 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 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": {},
"outputs": [
{
"data": {
"text/plain": [
"[[[(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,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"tournament(6, 1)"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"[[[(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": 11,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"tournament(8, 2)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"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": {},
"outputs": [],
"source": [
"def report(sched):\n",
" \"Print information about this schedule.\"\n",
" for i, round in enumerate(sched, 1):\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(name, people), ' Games')\n",
" print('--+' + '--' * P + ' -----')\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 str_from_round(round) -> str:\n",
" \"A string representing a round of games.\"\n",
" return ' | '.join(f'{name(a)},{name(b)} vs {name(c)},{name(d)}'\n",
" for [(a, b), (c, d)] in round)\n",
" \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 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": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"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",
" | 1 2 3 4 5 6 7 8 Games\n",
"--+---------------- -----\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(tournament(8, 2))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"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",
"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",
"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",
"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": {},
"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": [
"report(tournament2(4, 1, 10))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 8 Player Tournament\n",
"\n",
"Let's try to answer Steve's request for an 8-player tournament:"
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"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",
" | 1 2 3 4 5 6 7 8 Games\n",
"--+---------------- -----\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": [
"report(tournament2(8, 2))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"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": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"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",
" | 1 2 3 4 5 6 7 8 Games\n",
"--+---------------- -----\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": [
"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)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 9 Player Tournament\n",
"\n",
"For 9 players, I can fit the 18 games into 9 rounds, but some players play each other 1 or 3 times. I'll report the results of a previous run:"
]
},
{
"cell_type": "code",
"execution_count": 18,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"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,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",
" | 1 2 3 4 5 6 7 8 9 Games\n",
"--+------------------ -----\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": [
"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)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# 16 Player Tournament\n",
"\n",
"Let's jump to 16 players on 4 courts:"
]
},
{
"cell_type": "code",
"execution_count": 19,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"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",
" | 1 2 3 4 5 6 7 8 9 A B C D E F G Games\n",
"--+-------------------------------- -----\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(tournament2(P=16, C=4))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"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."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Addendum: Counting Schedules\n",
"\n",
"A reader asked \"*couldn't you have tried all possible schedules?*\" That's a great question! As [Ken Thompson says](https://users.ece.utexas.edu/~adnan/pike.html), \"when in doubt, use brute force.\" How many possible schedules are there? \n",
"\n",
"- Assume a schedule with *R* rounds on *C* courts, with every court filled on every round.\n",
"- That means there are *G* = *CR* games and 2*G* slots in the schedule for pairs to fill.\n",
"- We can fill those slots with pairs in (2*G*)! ways.\n",
"- But that over-counts, because order doesn't matter in the following three ways:\n",
" - The order of pairs within a game doesn't matter, so divide by 2*G*.\n",
" - The order of games within a round doesn't matter, so divide by *C*!*R*.\n",
" - The order of rounds within the schedule doesn't matter, so divide by *R*!.\n",
"\n",
"That gives us:"
]
},
{
"cell_type": "code",
"execution_count": 20,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{4: 15.0,\n",
" 5: 945.0,\n",
" 8: 2.8845653137679503e+19,\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": 20,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"from math import factorial\n",
"\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",
"\n",
"{P: count_schedules(P) \n",
" for P in range(4, 18) if P % 4 in (0, 1)}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"We see that it would have been infeasible to try every schedule, even for *P*=8, let alone 9 or 16."
]
}
],
"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.6"
}
},
"nbformat": 4,
"nbformat_minor": 1
}