Alessandro's Portfolio

Analysis of a Rodeo-tournament maker algorithm

Introduction and motivation

Padel is a game inspired from tennis, from which inherits most of its rules. The variation is in that the court is surrounded by see-through walls, where the ball can do its second bounce. The game is typically played in doubles.

Rodeo is a type of tournament, and it is apparently a very popular formula in the Padel environment. Rodeo has no official set of rules, but in general matches are quick (e.g., one-game matches), teams all play the same number of matches and in general the team with most points/wins, wins the tournament. Interesting variations have players switch teams, and the player with most points wins. Some add a semi-final and final round.

Below the kind of Rodeo I am going to focus on:

  • Teams all play the same numbers of matches.
  • Matches last one game.
  • Winner is the team with the most points.

A draw is handled via a final match (if between two teams) or round-robin round.

Here we focus on the first part of the tournament, and more specifically on match-making. In the remainder of this report I first describe the problem and then show an algorithm that does the match-making for a Rodeo tournament.

The Rodeo-match-making problem

These are the set of constraints I have to follow:

  • Teams all play the same number of matches throughout the tournament.
  • Teams play at most a match per round.
  • They never play against each other more than once.

Note that by round I mean a set of matches that are all played during the same timeframe. A round ends when all matches in the same round end.

The input parameters are:

  • Teams, let $t$ be the number of teams.
  • The number of rounds $r$.
  • Number of padel courts available, $p$.

The output is a list of matches split between rounds. Match-making can be split in three subproblems:

  1. Given the number of teams $t$, rounds $r$, padel courts $p$, I want to know the maximum number of matches per team, $m$.
  2. Given the matches per team I compute the pairings.
  3. I assign a set of matches to each round.

Let’s dive in!

Number of matches per team

Given the number of teams $t$, rounds $r$, courts available $p$, we want to compute the highest possible number of matches per team, $m$. The algorithm below tries to find via a brute force approach a solution. First, it assumes there is a solution where each team plays once every round, that is, the situation where there are at least $t / 2$ padel courts available. Then, reduces iteratively by one the candidate matches per team, until it either finds a solution or reaches zero. Zero means no solution is possible. This is a situation that can happen with ill-formed inputs, such as 0 courts available or an input that does not allow every team to play at least once.

The algorithm tries to find a total number of matches that are even, and if that number leads to a number of matches per turn that is lower than $p$, we found a solution.

Note that we do not care in this step whether matches_per_turn is integer, we will deal with it later when actually filling the rounds.

The algorithm finally returns the total matches, matches per turn and matches per team.

def get_matches_per_team(t, r, p):
    
    matches_per_team = r

    while matches_per_team > 0:
        total_participations = t * matches_per_team
        total_matches = total_participations / 2

        if total_matches % 1 == 0:
            matches_per_turn = total_matches / r

            if matches_per_turn <= p:
                return total_matches, matches_per_turn, matches_per_team

        matches_per_team -= 1

    return 0, 0, 0

Matchmaking

For each team, we need to find $m$ matches. It is convenient to use a graph data structure where nodes are teams, and edges are the matches. In fact, we are not building any graph, but a k-regular one! We have a set of nodes (teams), and each node must have a degree $k = m$.

The preconditions for building a k-regular graph are that the cardinality is even and that the number of nodes is larger than the number of edges, i.e., $(n * k) % 2 == 0\wedge n > k$. The use-case satisfies the conditions:

  • $(n * k)$ is the number of total participations which must be even (each match counts as two participations).
  • The number of teams is larger than the number of matches per team, since teams play at most once against each other.
def make_matches(teams, matches_per_team):
    n = len(teams)
    k = matches_per_team

    if k % 2 == 0:
        return k_regular_even(teams, k)

    return k_regular_odd(teams, k)

The idea is the following, I arrange the nodes in a circle, and:

  • If k is even, I select $k/2$ nodes to the left and right of each node.
  • If k is odd, I do the same and for each node $n$ I draw an additional edge that, in the imaginary circle, is on the opposite side of $n$.
def k_regular_even(teams, k):
    n = len(teams)

    for i in range(0, n):
        for j in range(i - 1, i - 1 - k // 2, -1):
            res.add({teams[j % n], teams[i]})

        for j in range(i + 1, i + 1 + k // 2):
            res.add({teams[j % n], teams[i]})

    return res

def k_regular_odd(teams: List[int], k: int) -> Set[FrozenSet[int]]:
    n = len(teams)
    res = k_regular_even(teams, k - 1)

    for i in range(0, n):
        res.add(frozenset({teams[i], teams[(i + n // 2) % n]}))

    return res

Assigning matches to rounds

I lean into the graph abstraction. I have a set of matches (edges), and teams (nodes) and I want to find a way to separate all the matches into different rounds in such a way that in each round no team plays twice. In other words, each round is a matching 1, and I want to create at most matchings as there are rounds. This is an edge-coloring problem 2. The algorithm below will add matches to rounds, without using more than $p$ courts and maximizing the number of matches per round. If $m$ is not whole, the algorithm will always try to compute $\top{m}$ matches per round, until the matches are exhausted: there are situations where the number of matchings found is less than the required rounds, which I consider acceptable.

Given a k-regular graph, split the edges into $n$ sets of edges, where every set contains non-adjacent edges (a set of non-adjacent edges is an independent-set, or a matching). I need to find $n$ matchings where $n$ is the number of rounds, and the cardinality of the sets is the number of matches for each round. Edge-coloring is a well-studied, NP-hard problem. Luckily, the input is a k-regular graph which makes the implementation fairly straight forward. The intuition is: just pick one edge per vertex to build a round.

Ops! This is wrong, because it can lead to a situation where it is impossible to build the last round: adjacent edges may remain.

I settled on a brute-force solution: a better solution would not be polynomial anyway, unless I misunderstood the assumptions. Intuitively, a brute-force algorithm would guess at each step in which round a specific match goes. It builds a decision tree, and backtracks whenever it realizes it took the wrong path. The stop condition is that all matches have been assigned to a round. Some quick napkin math: the tree of choices this brute-force algorithm may go through is huge! The complexity is exponential and should be around $O(\prod_{i}^{K} ​(E - i * $m$))$ where $E$ is the total number of matches during the tournament (and the depth of the recursion tree), $K$ the number of rounds (where to place the match), $m$ the number of matches per round. Each level in the decision tree we have $m$ fewer matches to choose between.

Below the backtracking algorithm. I use the following variables:

  • remaining_edges, matches not yet scheduled.
  • buckets, matches scheduled for each round.
  • used_nodes, for each round, the teams already scheduled.
  • total_matchings, the number of rounds.
  • matching_max_size, the maximum number of matches per round.
def find_matchings(remaining_edges, buckets, used_nodes, total_matchings, matching_max_size):

    if len(remaining_edges) == 0:
        return buckets

    current_edge = remaining_edges.pop()
    p1, p2 = current_edge

    for i in range(0, total_matchings - 1):
        
        if (p1 not in used_nodes[i]) and 
           (p2 not in used_nodes[i]) and 
           (size(buckets[i]) < matching_max_size):

            buckets[i].add(current_edge)
            used_nodes[i].add(p1)
            used_nodes[i].add(p2)

            result = find_matchings(
                remaining_edges,
                buckets,
                used_nodes,
                total_matchings,
                matching_max_size
            )

            if result:
                return result

            buckets[i].remove(current_edge)
            used_nodes[i].remove(p1)
            used_nodes[i].remove(p2)

    remaining_edges.push(current_edge)
    return false

Does this exponential algorithm work in practice? Let me use the following assumptions:

  • Building a state in the decision tree takes only 1 CPU cycle.
  • I have a 4 GHz CPU, this means I can do roughly $4*10^9$ operations per second.

For example, if were to build a tournament with 15 teams, 5 courts available and 8 rounds, thus, $K = 8$ and $E = 30$ and $m = 5$, it should take approximately $0.003$ seconds to compute the whole solution space, nice! Hopefully the algorithm converges to a path to the correct solution fast!

Conclusion

This was hard! Tournament making (not only in the case of Rodeo tournaments) is a difficult problem that require complex and often slow algorithms. We found that this particular tournament was just an instance of edge-coloring. There is already a lot of literature that try to solve this problem on graphs that satisfy certain properties, and provide algorithms. Still, edge-coloring is classified to be NP-hard and thus only exponential algorithms are known to be able to solve the general case. I provided a backtracking algorithm that builds a solution. Improvements to the algorithm could be made by adding strategies to prune the search space earlier, for example by choosing the edges according to some order. Higher level optimizations might also be interesting to add:

  • I could apply a heuristic first, and only if it does not work, use the brute-force algorithm.
  • I could add some form of randomness and run instances of the algorithm in parallel, and wait for the first one to finish.