Skip to content
← All posts

Earliest and Latest Rounds Where Players Compete: Tournament Simulation

5 min read
leetcodeproblemharddynamic-programming

Tournament brackets have a hidden structure that makes this problem fascinating. You are given n players in a line, and in each round player i fights player n+1-i (1-indexed, counting from the outside in). Losers are eliminated, survivors keep their relative order, and the process repeats. Given two specific players, you need to find the earliest and latest round in which they could possibly meet.

The problem

LeetCode 1900: The Earliest and Latest Rounds Where Players Compete. You have n players in a tournament. In each round, the first player fights the last, the second fights the second-to-last, and so on. If there is an odd number of players, the middle one advances automatically. Winners maintain their relative order and form the lineup for the next round. Given firstPlayer and secondPlayer (1-indexed positions), return an array [earliest, latest] representing the earliest and latest rounds where these two players can compete against each other.

Round 1P1P2P3P4P5P6P7vsvsvsbyefirstPlayersecondPlayer
7 players in Round 1. Player i fights player n+1-i from the outside in. The middle player gets a bye.

The key insight: recursive state is just three numbers

The brute-force idea of tracking every player's identity through every round is too expensive. Instead, notice that the only things that matter are: the position of firstPlayer, the position of secondPlayer, and the total number of remaining players. You do not need to know who the other players are, only how many sit between and around your two targets.

In each round, the two target players either fight each other (if their positions mirror, meaning left + right == n + 1) or they do not. If they fight, that is the current round. If not, you need to figure out what positions they land in after this round. The key is that the other matches have flexible outcomes. Any non-target player can win or lose, and each combination produces a different arrangement for the next round.

So you enumerate all valid outcomes for the other matches. For each outcome, you compute the new positions of the two target players and the new total count, then recurse. Across all branches, you track the minimum round number (earliest) and maximum round number (latest).

Visual walkthrough

Example: n=6, firstPlayer=2, secondPlayer=4

R1123456vsvsvs

6 players line up. Player 2 (blue) and Player 4 (yellow) are the ones we track.

Round 1 pairings: 1v6, 2v5, 3v4

R1123456vsvsvs

Player i fights player n+1-i. Players 2 and 4 are not paired, so they cannot meet yet.

Earliest: after Round 1, both survive. 3 players remain.

R2246vs

If 2 beats 5 and 4 beats 3, they advance. The surviving lineup determines Round 2 pairings.

Earliest meeting: Round 2. Positions 1 and 3 pair up.

R2624vs

With 3 survivors [2, 4, 6], player 2 is at position 1 and fights position 3 (player 6). Player 4 gets a bye. But with [6, 2, 4], players 2 and 4 meet. Earliest = 2.

Latest: delay the meeting by choosing other outcomes

State: (left, right, total)left = position of firstPlayerright = position of secondPlayer, total = players remaining

The DP explores all possible winner combinations in each round. By choosing which other players win, we control when the two targets finally collide.

Key insight: only positions and count matter

solve(left=2, right=4, n=6)solve(1, 2, 3)solve(1, 3, 4)

We do not need to track every player. The state (left, right, n) fully determines possible outcomes. Memoize on this triple.

The code

from functools import lru_cache

def earliestAndLatest(n, firstPlayer, secondPlayer):
    @lru_cache(maxsize=None)
    def solve(left, right, total):
        if left + right == total + 1:
            return 1, 1
        if left > right:
            return solve(right, left, total)

        half = total // 2
        nxt = (total + 1) // 2
        mn, mx = total, 0

        for i in range(1, left + 1):
            lo = max(i + 1, right - half + i) if right > half else i + 1
            hi = min(nxt, i + right - left) if right <= half else min(nxt, right - half + i)

            for j in range(lo, hi + 1):
                e, l = solve(i, j, nxt)
                mn = min(mn, e + 1)
                mx = max(mx, l + 1)

        return mn, mx

    return list(solve(firstPlayer, secondPlayer, n))

The function solve(left, right, total) returns a tuple of (earliest round, latest round) where left and right are the 1-indexed positions of the two target players, and total is the number of players remaining.

The base case fires when left + right == total + 1. This means the two target players are mirrors of each other in the current lineup, so they fight this round. Both earliest and latest are 1.

If left > right, we swap them so that left is always the smaller position. This normalization halves the state space and is critical for correct memoization. Without it, solve(3, 1, 6) and solve(1, 3, 6) would be stored as separate entries even though they represent the same situation.

For the recursive case, we enumerate all valid next-round positions (i, j) for the two target players. The variable i ranges from 1 to left because the first target can only end up at a position less than or equal to its current one (players before it might get eliminated, shifting it forward). The bounds for j depend on whether right falls in the first or second half of the lineup. If right is in the second half, its mirror opponent is in the first half, so we need to account for how many survivors come from each side. The lo and hi bounds encode these constraints tightly so we never enumerate an impossible configuration.

Complexity analysis

ComplexityWhy
TimeO(n^4)The state space is O(n^3) triples of (left, right, total). For each state, we iterate over O(n) possible next positions.
SpaceO(n^3)Memoization table stores one entry per unique (left, right, total) triple.

With n at most 28 (per LeetCode constraints), the total number of states is small enough that memoized recursion runs well within time limits. The enumeration loop at each state is bounded by n/2, keeping the overall work manageable.

The building blocks

  1. Recursive simulation with memoization. Instead of simulating every possible tournament bracket, you define a compact state (left, right, total) and cache results. This is the same idea behind game-theory DP problems like Predict the Winner, but applied to a tournament elimination structure.

  2. Outside-in pairing logic. The matching rule (player i vs player n+1-i) means you need to reason about positions relative to the midpoint. Understanding which half each target sits in determines the valid ranges for their next-round positions.

  3. State normalization. Swapping left and right when left > right ensures each state is visited only once. This is a common trick in symmetric DP problems to cut the search space in half.

Edge cases

  • Players are already paired. If firstPlayer + secondPlayer == n + 1 in the initial round, they fight immediately. Answer is [1, 1].
  • Adjacent players. When firstPlayer and secondPlayer are next to each other, they still might not be paired if they are not mirrors.
  • n = 2. Only two players, so they must fight in Round 1. Answer is always [1, 1].
  • Odd number of players. The middle player gets a bye, which changes the next-round count to (n+1)//2 instead of n//2.
  • Target players on the same side. If both targets are in the first half (or both in the second half after mirroring), the enumeration bounds differ from the case where they straddle the midpoint.

From understanding to recall

This problem packs a lot of subtlety into a small state space. The pairing rule, the enumeration of valid next-round positions, and the normalization trick all need to come together correctly. Reading through the solution once gives you the general idea, but reproducing it under time pressure is a different skill entirely.

Spaced repetition bridges that gap. You revisit the three-variable state definition, write the enumeration bounds from memory, and after a few reps the pattern becomes automatic. The "recursive tournament simulation" pattern is niche, but the underlying technique of memoized state exploration shows up in many hard DP problems.

The building blocks here (memoized recursion over a compact state, state normalization, and careful bound computation) are transferable to dozens of other problems. Drilling them individually is more effective than solving random hard problems and hoping the patterns stick.

Related posts