I am organizing a tournament where 12 players are going to meet each other on 10 board games.
I want each player to play at least one time with the 11 others players over the 10 board games.
For example :
BoardGame1 - Match1 - Player1 + Player2 + Player3
BoardGame1 - Match2 - Player4 + Player5 + Player6
BoardGame1 - Match3 - Player7 + Player8 + Player9
BoardGame1 - Match4 - Player10 + Player11 + Player12
[...]
BoardGame10 - Match1 - Player1 + Player11 + Player9
BoardGame10 - Match2 - Player4 + Player2 + Player12
BoardGame10 - Match3 - Player7 + Player5 + Player3
BoardGame10 - Match4 - Player10 + Player8 + Player6
How do you create an algorithm where you distribute the players evenly?
I'd like to do it with TDD approach, so I need to predict the expected result (meaning no random distribution).
If all players play each other exactly once, then the resulting object
would be a Kirkman Triple System. There is no KTS with the parameters
you want, but since each player has twenty opponent slots and only
eleven potential opponents, it should be easy to find a suitable
schedule.
The code below generates the (11 choose 2) × (8 choose 2) × (5 choose 2)
= 15400 possibilities for one game and repeatedly greedily chooses the
one that spreads the pairings in the fairest manner.
import collections
import itertools
import pprint
def partitions(players, k):
players = set(players)
assert len(players) % k == 0
if len(players) == 0:
yield []
else:
players = players.copy()
leader = min(players)
players.remove(leader)
for comb in itertools.combinations(sorted(players), k - 1):
group = {leader} | set(comb)
for part in partitions(players - group, k):
yield [group] + part
def update_pair_counts(pair_counts, game):
for match in game:
pair_counts.update(itertools.combinations(sorted(match), 2))
def evaluate_game(pair_counts, game):
pair_counts = pair_counts.copy()
update_pair_counts(pair_counts, game)
objective = [0] * max(pair_counts.values())
for count in pair_counts.values():
objective[count - 1] += 1
total = 0
for i in range(len(objective) - 1, -1, -1):
total += objective[i]
objective[i] = total
return objective
def schedule(n_groups, n_players_per_group, n_games):
games = list(partitions(range(n_groups * n_players_per_group), n_players_per_group))
pair_counts = collections.Counter()
for i in range(n_games):
game = max(games, key=lambda game: evaluate_game(pair_counts, game))
yield game
update_pair_counts(pair_counts, game)
def main():
pair_counts = collections.Counter()
for game in schedule(4, 3, 10):
pprint.pprint(game)
update_pair_counts(pair_counts, game)
print()
pprint.pprint(pair_counts)
if __name__ == "__main__":
main()
Sample output:
[{0, 1, 2}, {3, 4, 5}, {8, 6, 7}, {9, 10, 11}]
[{0, 3, 6}, {1, 4, 9}, {2, 10, 7}, {8, 11, 5}]
[{0, 4, 7}, {3, 1, 11}, {8, 9, 2}, {10, 5, 6}]
[{0, 1, 5}, {2, 11, 6}, {9, 3, 7}, {8, 10, 4}]
[{0, 8, 3}, {1, 10, 6}, {2, 11, 4}, {9, 5, 7}]
[{0, 10, 11}, {8, 1, 7}, {2, 3, 5}, {9, 4, 6}]
[{0, 9, 2}, {1, 10, 3}, {8, 4, 5}, {11, 6, 7}]
[{0, 4, 6}, {1, 2, 5}, {10, 3, 7}, {8, 9, 11}]
[{0, 5, 7}, {1, 11, 4}, {8, 2, 10}, {9, 3, 6}]
[{0, 3, 11}, {8, 1, 6}, {2, 4, 7}, {9, 10, 5}]
Counter({(0, 3): 3,
(0, 1): 2,
(0, 2): 2,
(1, 2): 2,
(3, 5): 2,
(4, 5): 2,
(6, 7): 2,
(6, 8): 2,
(7, 8): 2,
(9, 10): 2,
(9, 11): 2,
(10, 11): 2,
(0, 6): 2,
(3, 6): 2,
(1, 4): 2,
(4, 9): 2,
(2, 7): 2,
(2, 10): 2,
(7, 10): 2,
(5, 8): 2,
(8, 11): 2,
(0, 4): 2,
(0, 7): 2,
(4, 7): 2,
(1, 3): 2,
(1, 11): 2,
(3, 11): 2,
(2, 8): 2,
(2, 9): 2,
(8, 9): 2,
(5, 10): 2,
(6, 10): 2,
(0, 5): 2,
(1, 5): 2,
(2, 11): 2,
(6, 11): 2,
(3, 7): 2,
(3, 9): 2,
(7, 9): 2,
(4, 8): 2,
(8, 10): 2,
(1, 6): 2,
(1, 10): 2,
(2, 4): 2,
(4, 11): 2,
(5, 7): 2,
(5, 9): 2,
(0, 11): 2,
(1, 8): 2,
(2, 5): 2,
(4, 6): 2,
(6, 9): 2,
(3, 10): 2,
(3, 4): 1,
(1, 9): 1,
(5, 11): 1,
(5, 6): 1,
(2, 6): 1,
(4, 10): 1,
(0, 8): 1,
(3, 8): 1,
(0, 10): 1,
(1, 7): 1,
(2, 3): 1,
(0, 9): 1,
(7, 11): 1})
How do you create an algorithm where you distribute the players evenly? I'd like to do it with TDD approach, so I need to predict the expected result (meaning no random distribution).
TDD tends to be successful when your problem is that you know what the computer should do, and you know how to make the computer do that, but you don't know the best way to write the code.
When you don't know how to make the computer do what you want, TDD is a lot harder. There are two typical approaches taken here.
The first is to perform a "spike" - sit down and hack things until you understand how to make the computer do what you want. The key feature of spikes is that you don't get to keep the code changes at the end; instead, you discard your spiked code, keep what you have learned in your head, and start over by writing the tests that you need.
The second approach is to sort of sneak up on it - you do TDD for the very simple cases that you do understand, and keep adding tests that are just a little bit harder than what you have already done. See Robert Martin's Craftsman series for an example of this approach.
For this problem, you might begin by first thinking of an interface that you might use for accessing the algorithm. For instance, you might consider a design that accepts as input a number of players and a number of games, and returns you a sequence of tuples, where each tuple represents a single match.
Typically, this version of the interface will look like general purpose data structures as inputs (in this example: numbers), and general purpose data structures as outputs (the list of tuples).
Most commonly, we'll verify the behavior in each test by figuring out what the answer should be for a given set of inputs, and asserting that the actual data structure exactly matches the expected. For a list of tuples, that would look something like:
assert len(expected) == len(actual)
for x in range(actual):
assert len(expected[x]) == len(actual[x])
for y in range(actual[x]):
assert expected[x][y] == actual[x][y]
Although of course you could refactor that into something that looks nicer
assert expected == actual
Another possibility is to think about the properties that a solution should have, and verify that the actual result is consistent with those properties. Here, you seem to have two properties that are required for every solution:
Each pair of players should appear exactly once in the list of matches
Every player, boardgame pair should appear exactly once in the list of matches
In this case, the answer is easy enough to check (iterate through all of the matches, count each pair, assert count equals one).
The test themselves we introduce by starting with the easiest example we can think of. Here, that might be the case where we have 2 players and 1 board, and our answer should be
BoardGame1 - Match1 - Player1 + Player2
And so we write that test (RED), and hard code this specific answer (GREEN), and then (REFACTOR) the code so that it is clear to the reader why this is the correct answer for these inputs.
And when you are happy with that code, you look for the next example - an example where the current implementation returns the wrong answer, but the change that you need to make to get it to return the write answer is small/easy.
Often, what will happen is that we "pass" the next test using a branch:
if special_case(inputs):
return answer_for_special_case
else:
# ... real implementation here ...
return answer_for_general_case
And then refactor the code until the two blocks are the same, then finally remove the if clause.
It will sometimes happen that the new test is too big, and we can't figure out how to extend the algorithm to cover the new case. Usually the play is to revert any changes we've made (keeping the tests passing), and use what we have learned to find a different test that might be easier to introduce to the code.
And you keep iterating on this process until you have solved "all" of the problem.
Here is a resolvable triple system due to Haim Hanani (“On resolvable
balanced incomplete block designs”, 1974), which provides a schedule for
11 games (drop one). Unfortunately it repeats matches.
import collections
import itertools
from pprint import pprint
def Match(a, b, c):
return tuple(sorted([a, b, c]))
games = []
for j in range(4):
games.append(
[
Match(0 ^ j, 4 ^ j, 8 ^ j),
Match(1 ^ j, 2 ^ j, 3 ^ j),
Match(5 ^ j, 6 ^ j, 7 ^ j),
Match(9 ^ j, 10 ^ j, 11 ^ j),
]
)
games.append([Match(1 ^ j, 6 ^ j, 11 ^ j) for j in range(4)])
games.append([Match(2 ^ j, 7 ^ j, 9 ^ j) for j in range(4)])
games.append([Match(3 ^ j, 5 ^ j, 10 ^ j) for j in range(4)])
for j in range(4):
games.append(
[
Match(0 ^ j, 4 ^ j, 8 ^ j),
Match(1 ^ j, 6 ^ j, 11 ^ j),
Match(2 ^ j, 7 ^ j, 9 ^ j),
Match(3 ^ j, 5 ^ j, 10 ^ j),
]
)
for game in games:
game.sort()
pprint(len(games))
pprint(games)
pair_counts = collections.Counter()
for game in games:
for triple in game:
pair_counts.update(itertools.combinations(sorted(triple), 2))
pprint(max(pair_counts.values()))
Output:
11
[[(0, 4, 8), (1, 2, 3), (5, 6, 7), (9, 10, 11)],
[(0, 2, 3), (1, 5, 9), (4, 6, 7), (8, 10, 11)],
[(0, 1, 3), (2, 6, 10), (4, 5, 7), (8, 9, 11)],
[(0, 1, 2), (3, 7, 11), (4, 5, 6), (8, 9, 10)],
[(0, 7, 10), (1, 6, 11), (2, 5, 8), (3, 4, 9)],
[(0, 5, 11), (1, 4, 10), (2, 7, 9), (3, 6, 8)],
[(0, 6, 9), (1, 7, 8), (2, 4, 11), (3, 5, 10)],
[(0, 4, 8), (1, 6, 11), (2, 7, 9), (3, 5, 10)],
[(0, 7, 10), (1, 5, 9), (2, 4, 11), (3, 6, 8)],
[(0, 5, 11), (1, 7, 8), (2, 6, 10), (3, 4, 9)],
[(0, 6, 9), (1, 4, 10), (2, 5, 8), (3, 7, 11)]]
2
Combinatorial optimization is another possibility. This one doesn’t
scale super well but can handle 12 players/10 games.
import collections
import itertools
from pprint import pprint
def partitions(V):
if not V:
yield []
return
a = min(V)
V.remove(a)
for b, c in itertools.combinations(sorted(V), 2):
for part in partitions(V - {b, c}):
yield [(a, b, c)] + part
parts = list(partitions(set(range(12))))
from ortools.sat.python import cp_model
model = cp_model.CpModel()
vars = [model.NewBoolVar("") for part in parts]
model.Add(sum(vars) == 10)
pairs = collections.defaultdict(list)
for part, var in zip(parts, vars):
for (a, b, c) in part:
pairs[(a, b)].append(var)
pairs[(a, c)].append(var)
pairs[(b, c)].append(var)
for clique in pairs.values():
total = sum(clique)
model.Add(1 <= total)
model.Add(total <= 2)
solver = cp_model.CpSolver()
status = solver.Solve(model)
print(solver.StatusName(status))
schedule = []
for part, var in zip(parts, vars):
if solver.Value(var):
schedule.append(part)
pprint(schedule)
Sample output:
OPTIMAL
[[(0, 1, 6), (2, 4, 9), (3, 8, 11), (5, 7, 10)],
[(0, 1, 10), (2, 3, 5), (4, 8, 9), (6, 7, 11)],
[(0, 2, 4), (1, 8, 10), (3, 7, 11), (5, 6, 9)],
[(0, 2, 8), (1, 4, 11), (3, 7, 9), (5, 6, 10)],
[(0, 3, 6), (1, 4, 7), (2, 5, 8), (9, 10, 11)],
[(0, 3, 8), (1, 5, 11), (2, 6, 9), (4, 7, 10)],
[(0, 4, 5), (1, 2, 7), (3, 6, 10), (8, 9, 11)],
[(0, 5, 11), (1, 3, 9), (2, 6, 7), (4, 8, 10)],
[(0, 7, 9), (1, 6, 8), (2, 10, 11), (3, 4, 5)],
[(0, 9, 10), (1, 2, 3), (4, 6, 11), (5, 7, 8)]]
I got quite standard DP problem - board nxn with integers, all positive. I want to start somewhere in the first row, end somewhere in the last row and accumulate as much sum as possible. From field (i,j) I can go to fields (i+1, j-1), (i+1, j), (i+1, j+1).
That's quite standard DP problem. But we add one thing - there can be an asterisk on the field, instead of the number. If we meet the asterisk, then we got 0 points from it, but we increase multiplier by 1. All numbers we collect later during our traversal are multiplied by multiplier.
I can't find out how to solve this problem with that multiplier thing. I assume that's still a DP problem - but how to get the equations right for it?
Thanks for any help.
You can still use DP, but you have to keep track of two values: The "base" value, i.e. without any multipliers applied to it, and the "effective" value, i.e. with multipliers. You work your way backwards through the grid, starting in the previous-to-last row, get the three "adjacent" cells in the row after that (the possible "next" cells on the path), and just pick the one with the highest value.
If the current cell is a *, you get the cell where base + effective is maximal, otherwise you just get the one where the effective score is highest.
Here's an example implementation in Python. Note that instead of * I'm just using 0 for multipliers, and I'm looping the grid in order instead of in reverse, just because it's more convenient.
import random
size = 5
grid = [[random.randint(0, 5) for _ in range(size)] for _ in range(size)]
print(*grid, sep="\n")
# first value is base score, second is effective score (with multiplier)
solution = [[(x, x) for x in row] for row in grid]
for i in range(1, size):
for k in range(size):
# the 2 or 3 values in the previous line
prev_values = solution[i-1][max(0, k-1):k+2]
val = grid[i][k]
if val == 0:
# multiply
base, mult = max(prev_values, key=lambda t: t[0] + t[1])
solution[i][k] = (base, base + mult)
else:
# add
base, mult = max(prev_values, key=lambda t: t[1])
solution[i][k] = (val + base, val + mult)
print(*solution, sep="\n")
print(max(solution[-1], key=lambda t: t[1]))
Example: The random 5x5 grid, with 0 corresponding to *:
[4, 4, 1, 2, 1]
[2, 0, 3, 2, 0]
[5, 1, 3, 4, 5]
[0, 0, 2, 4, 1]
[1, 0, 5, 2, 0]
The final solution grid with base values and effective values:
[( 4, 4), ( 4, 4), ( 1, 1), ( 2, 2), ( 1, 1)]
[( 6, 6), ( 4, 8), ( 7, 7), ( 4, 4), ( 2, 4)]
[( 9, 13), ( 5, 9), ( 7, 11), (11, 11), ( 9, 9)]
[( 9, 22), ( 9, 22), ( 9, 13), (11, 15), (12, 12)]
[(10, 23), ( 9, 31), (14, 27), (13, 17), (11, 26)]
Thus, the best solution for this grid is 31 from (9, 31). Working backwards through the grid solution grid, this corresponds to the path 0-0-5-0-4, i.e. 3*5 + 4*4 = 31, as there are 2 * before the 5, and 3 * before the 4.
Say I have the following ranges, in some list:
{ (1, 4), (6, 8), (2, 5), (1, 3) }
(1, 4) represents days 1, 2, 3, 4. (6, 8) represents days 6, 7, 8, and so on.
The goal is to find the total number of days that are listed in the collection of ranges -- for instance, in the above example, the answer would be 8, because days 1, 2, 3, 4, 6, 7, 8, and 5 are contained within the ranges.
This problem can be solved trivially by iterating through the days in each range and putting them in a HashSet, then returning the size of the HashSet. But is there any way to do it in O(n) time with respect to the number of range pairs? How about in O(n) time and with constant space? Thanks.
Sort the ranges in ascending order by their lower limits. You can probably do this in linear time since you're dealing with integers.
The rest is easy. Loop through the ranges once keeping track of numDays (initialized to zero) and largestDay (initialized to -INF). On reaching each interval (a, b):
if b > largestDay then
numDays <- numDays + b-max(a - 1, largestDay)
largestDay <- max(largestDay, b)
else nothing.
So, after sorting we have (1,4), (1,3), (2,5), (6,8)
(1,4): numDays <- 0 + (4 - max(1 - 1, -INF)) = 4, largestDay <- max(-INF, 4) = 4
(1,3): b < largestDay, so no change.
(2,5): numDays <- 4 + (5 - max(2 - 1, 4)) = 5, largestDay <- 5
(6,8): numDays <- 5 + (8 - max(6-1, 5)) = 8, largestDay <- 8
The complexity of the following algorithm is O(n log n) where n is the number of ranges.
Sort the ranges (a, b) lexicographically by increasing a then by decreasing b.
Before: { (1, 4), (6, 8), (2, 5), (1, 3) }
After: { (1, 4), (1, 3), (2, 5), (6, 8) }
Collapse the sorted sequence of ranges into a potentially-shorter sequence of ranges, repeatedly merging consecutive (a, b) and (c, d) into (a, max(b, d)) if b >= c.
Before: { (1, 4), (1, 3), (2, 5), (6, 8) }
{ (1, 4), (2, 5), (6, 8) }
After: { (1, 5), (6, 8) }
Map the sequence of ranges to their sizes.
Before: { (1, 5), (6, 8) }
After: { 5, 3 }
Sum the sizes to arrive at the total number of days.
8