Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

1900. The Earliest and Latest Rounds Where Players Compete - Leetcode Solution

Problem Description

Given n players in a knockout tournament, each player is numbered from 1 to n. In each round, the remaining players are paired up in order: the first with the last, the second with the second last, and so on. If there's an odd number of players, the middle one advances automatically. You are given two integers firstPlayer and secondPlayer, representing two players. Your task is to determine the earliest and latest possible rounds in which these two players could face each other, assuming all matches' outcomes can be chosen arbitrarily (except that these two players must win until they meet).

Constraints:

  • 1 <= n <= 30
  • 1 <= firstPlayer < secondPlayer <= n
The solution must consider all possible match outcomes, and the two players must not lose before meeting each other.

Thought Process

At first, you might think to simulate every possible tournament bracket, but that quickly becomes infeasible as n grows, due to the exponential number of possible match outcomes. However, since we only care about when two specific players meet, we can focus on their positions and how the pairing works, rather than simulating every player's path.

The key insight is:

  • In each round, the order of remaining players determines the matchups.
  • We can control (by choosing match outcomes) who advances, except that firstPlayer and secondPlayer must always win until they face each other.
  • We need to find both the minimal and maximal number of rounds before they meet, considering all possible ways to arrange the bracket through match outcomes.
This suggests a recursive or memoized search, focusing only on the relative positions of our two players as the rounds progress.

Solution Approach

Let's break down the approach step by step:

  1. Model the Tournament: Represent the list of remaining players by their positions. In each round, players are paired up from the outside in.
  2. Recursive Search: Use a recursive function (with memoization to avoid recomputation) that, given the current state (number of players, positions of firstPlayer and secondPlayer), returns the earliest and latest round in which they can meet.
  3. Base Case: If the two players are paired in the current round (i.e., their positions sum to n+1 or are the same), return the current round as both earliest and latest.
  4. Simulate All Outcomes: For each possible combination of winners (except that firstPlayer and secondPlayer must survive), recursively compute the next round's positions.
  5. Aggregate Results: Track the minimum and maximum rounds across all possible paths.
  6. Optimization: Use memoization (caching) to avoid redundant calculations, as the state is determined by the set of players and their positions.

By focusing only on the positions of firstPlayer and secondPlayer in each round, and not the full bracket, we can solve the problem efficiently.

Example Walkthrough

Let's consider n = 8, firstPlayer = 2, secondPlayer = 4.

  • Round 1: Players: [1, 2, 3, 4, 5, 6, 7, 8]. Pairings: (1,8), (2,7), (3,6), (4,5).
    • firstPlayer (2) plays 7, secondPlayer (4) plays 5. They are not matched yet.
  • Round 2: Winners advance. The order and matchups depend on who advances from other matches. By choosing different winners, we can affect the positions of 2 and 4 in the next round.
    • We continue simulating rounds, always ensuring 2 and 4 win, and arranging other match outcomes to try to make them meet as early or as late as possible.
  • Meeting: By exploring all possible outcomes, we find the earliest round they can meet is 2, and the latest is 3.

The recursive/memoized approach efficiently explores all these possibilities.

Time and Space Complexity

Brute-force Approach: Simulating every possible tournament bracket would take exponential time: O(2^n), which is infeasible for n up to 30.

Optimized Approach: By focusing only on the positions of firstPlayer and secondPlayer and using memoization, the number of unique states is much smaller. For each state (n, position of firstPlayer, position of secondPlayer), we memoize results. The total number of such states is O(n^3), and each state takes O(n^2) time to process (since we may need to consider all possible combinations of winners for the other players), making the overall runtime feasible for n ≤ 30.

Space Complexity: The memoization table uses O(n^3) space.

Summary

The key insight is to focus on the positions of the two players of interest and use recursion with memoization to efficiently explore all possible ways they could be paired in the tournament. By simulating only the necessary parts of the tournament (the paths of firstPlayer and secondPlayer), we avoid the combinatorial explosion of simulating all matches. The approach is elegant because it reduces a seemingly intractable problem to a manageable state space, using dynamic programming principles.

Code Implementation

from functools import lru_cache

class Solution:
    def earliestAndLatest(self, n: int, firstPlayer: int, secondPlayer: int) -> [int, int]:
        @lru_cache(None)
        def dfs(players_tuple):
            players = list(players_tuple)
            l = len(players)
            # Find positions of two players
            idx1 = players.index(firstPlayer)
            idx2 = players.index(secondPlayer)
            # If they play each other this round
            if idx1 + idx2 == l - 1:
                return (1, 1)
            if idx1 > idx2:
                idx1, idx2 = idx2, idx1
            min_round, max_round = float('inf'), 0
            # Each pair: (i, l-1-i)
            def gen_next(players, winners):
                nxt = []
                for i in range((len(players)+1)//2):
                    if i == len(players)-1-i:
                        nxt.append(players[i])
                    else:
                        nxt.append(winners[i])
                return tuple(nxt)
            # For all possible winners for each pair (except for pairs containing firstPlayer or secondPlayer)
            def helper(pos, win):
                nonlocal min_round, max_round
                if pos >= l//2:
                    nxt = []
                    for i in range(l//2):
                        nxt.append(win[i])
                    if l % 2 == 1:
                        nxt.append(players[l//2])
                    # Only keep firstPlayer and secondPlayer if they survived
                    if firstPlayer in nxt and secondPlayer in nxt:
                        e, la = dfs(tuple(nxt))
                        min_round = min(min_round, e+1)
                        max_round = max(max_round, la+1)
                    return
                i, j = pos, l-1-pos
                a, b = players[i], players[j]
                # If (a,b) contains firstPlayer or secondPlayer, they must win
                if a == firstPlayer or a == secondPlayer:
                    helper(pos+1, win+[a])
                elif b == firstPlayer or b == secondPlayer:
                    helper(pos+1, win+[b])
                else:
                    helper(pos+1, win+[a])
                    helper(pos+1, win+[b])
            helper(0, [])
            return (min_round, max_round)
        players = tuple(range(1, n+1))
        return list(dfs(players))
      
#include <vector>
#include <algorithm>
#include <unordered_map>
using namespace std;

class Solution {
public:
    unordered_map<string, pair<int,int>> memo;
    vector<int> players;
    int fP, sP;
    
    pair<int,int> dfs(vector<int> curr) {
        int n = curr.size();
        int idx1 = find(curr.begin(), curr.end(), fP) - curr.begin();
        int idx2 = find(curr.begin(), curr.end(), sP) - curr.begin();
        if (idx1 + idx2 == n - 1)
            return {1, 1};
        if (idx1 > idx2)
            swap(idx1, idx2);
        string key;
        for (int x : curr) key += to_string(x) + ",";
        if (memo.count(key)) return memo[key];
        int minr = 100, maxr = 0;
        int half = n/2;
        vector<int> win;
        function<void(int, vector<int>&)> helper = [&](int pos, vector<int>& win){
            if (pos >= half) {
                vector<int> nxt = win;
                if (n%2==1) nxt.push_back(curr[n/2]);
                if (find(nxt.begin(), nxt.end(), fP) != nxt.end() &&
                    find(nxt.begin(), nxt.end(), sP) != nxt.end()) {
                    auto pr = dfs(nxt);
                    minr = min(minr, pr.first+1);
                    maxr = max(maxr, pr.second+1);
                }
                return;
            }
            int a = curr[pos], b = curr[n-1-pos];
            if (a == fP || a == sP) {
                win.push_back(a);
                helper(pos+1, win);
                win.pop_back();
            } else if (b == fP || b == sP) {
                win.push_back(b);
                helper(pos+1, win);
                win.pop_back();
            } else {
                win.push_back(a);
                helper(pos+1, win);
                win.pop_back();
                win.push_back(b);
                helper(pos+1, win);
                win.pop_back();
            }
        };
        vector<int> tmp;
        helper(0, tmp);
        return memo[key] = {minr, maxr};
    }
    vector<int> earliestAndLatest(int n, int firstPlayer, int secondPlayer) {
        fP = firstPlayer; sP = secondPlayer;
        vector<int> arr(n);
        for (int i=0; i<n; ++i) arr[i]=i+1;
        auto pr = dfs(arr);
        return {pr.first, pr.second};
    }
};
      
import java.util.*;

class Solution {
    Map<String, int[]> memo = new HashMap<>();
    int fP, sP;
    public int[] earliestAndLatest(int n, int firstPlayer, int secondPlayer) {
        fP = firstPlayer; sP = secondPlayer;
        List<Integer> arr = new ArrayList<>();
        for (int i=1; i<=n; ++i) arr.add(i);
        int[] ans = dfs(arr);
        return ans;
    }
    int[] dfs(List<Integer> curr) {
        int n = curr.size();
        int idx1 = curr.indexOf(fP);
        int idx2 = curr.indexOf(sP);
        if (idx1 + idx2 == n - 1) return new int[]{1,1};
        if (idx1 > idx2) { int t=idx1; idx1=idx2; idx2=t; }
        StringBuilder sb = new StringBuilder();
        for (int x: curr) sb.append(x).append(",");
        String key = sb.toString();
        if (memo.containsKey(key)) return memo.get(key);
        int minr = 100, maxr = 0;
        int half = n/2;
        List<Integer> win = new ArrayList<>();
        helper(curr, 0, win, minr, maxr, half, n, memo, key);
        return memo.get(key);
    }
    void helper(List<Integer> curr, int pos, List<Integer> win, int minr, int maxr, int half, int n, Map<String, int[]> memo, String key) {
        if (pos >= half) {
            List<Integer> nxt = new ArrayList<>(win);
            if (n%2==1) nxt.add(curr.get(n/2));
            if (nxt.contains(fP) && nxt.contains(sP)) {
                int[] pr = dfs(nxt);
                int minx = Math.min(memo.containsKey(key)?memo.get(key)[0]:100, pr[0]+1);
                int maxx = Math.max(memo.containsKey(key)?memo.get(key)[1]:0, pr[1]+1);
                memo.put(key, new int[]{minx, maxx});
            }
            return;
        }
        int a = curr.get(pos), b = curr.get(n-1-pos);
        if (a == fP || a == sP) {
            win.add(a);
            helper(curr, pos+1, win, minr, maxr, half, n, memo, key);
            win.remove(win.size()-1);
        } else if (b == fP || b == sP) {
            win.add(b);
            helper(curr, pos+1, win, minr, maxr, half, n, memo, key);
            win.remove(win.size()-1);
        } else {
            win.add(a);
            helper(curr, pos+1, win, minr, maxr, half, n, memo, key);
            win.remove(win.size()-1);
            win.add(b);
            helper(curr, pos+1, win, minr, maxr, half, n, memo, key);
            win.remove(win.size()-1);
        }
    }
}
      
var earliestAndLatest = function(n, firstPlayer, secondPlayer) {
    let memo = new Map();
    function dfs(players) {
        let l = players.length;
        let idx1 = players.indexOf(firstPlayer);
        let idx2 = players.indexOf(secondPlayer);
        if (idx1 + idx2 === l - 1) return [1,1];
        if (idx1 > idx2) [idx1, idx2] = [idx2, idx1];
        let key = players.join(',');
        if (memo.has(key)) return memo.get(key);
        let minr = Infinity, maxr = 0;
        function helper(pos, win) {
            if (pos >= Math.floor(l/2)) {
                let nxt = win.slice();
                if (l % 2 === 1) nxt.push(players[Math.floor(l/2)]);
                if (nxt.includes(firstPlayer) && nxt.includes(secondPlayer)) {
                    let [e, la] = dfs(nxt);
                    minr = Math.min(minr, e+1);
                    maxr = Math.max(maxr, la+1);
                }
                return;
            }
            let a = players[pos], b = players[l-1-pos];
            if (a === firstPlayer || a === secondPlayer) {
                win.push(a);
                helper(pos+1, win);
                win.pop();
            } else if (b === firstPlayer || b === secondPlayer) {
                win.push(b);
                helper(pos+1, win);
                win.pop();
            } else {
                win.push(a);
                helper(pos+1, win);
                win.pop();
                win.push(b);
                helper(pos+1, win);
                win.pop();
            }
        }
        helper(0, []);
        memo.set(key, [minr, maxr]);
        return [minr, maxr];
    }
    let arr = [];
    for (let i = 1; i <= n; ++i) arr.push(i);
    return dfs(arr);
};