LeetCode 1194 - Tournament Winners
The problem asks us to determine the winner in each group of players based on their accumulated points across multiple matches. We are given two tables: Players and Matches.
Difficulty: 🔴 Hard
Topics: Database
Solution
Problem Understanding
The problem asks us to determine the winner in each group of players based on their accumulated points across multiple matches. We are given two tables: Players and Matches. The Players table maps each player to a group, while the Matches table records each match's score between two players, where all matches occur within the same group.
The winner of a group is the player with the highest total points within that group. In case of a tie, the player with the smallest player_id wins. The output is a table showing the winning player_id for each group_id.
Important constraints and observations include that every player belongs to exactly one group, each match is between players of the same group, and ties are resolved deterministically using player_id. Edge cases include players with zero matches, multiple players tying in score, or groups with a single player.
Approaches
The brute-force approach would involve iterating through every player in every group, summing all their scores from the Matches table, and then determining the maximum manually for each group. This is correct but inefficient because it requires repeated full-table scans for each player, resulting in high computational cost.
A more optimal approach leverages SQL aggregation. We can compute total points per player using SUM on scores from the Matches table, then join with the Players table to get their group. Using a window function such as ROW_NUMBER() partitioned by group_id and ordered by total points descending, and player_id ascending, we can efficiently select the top player for each group. This approach avoids repeated scanning and leverages SQL’s built-in optimization for aggregation and sorting.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(P * M) | O(P) | Sum scores per player by scanning the Matches table multiple times, inefficient for large tables |
| Optimal | O(M + P) | O(P) | Aggregate scores per player once, rank within groups, select top player |
Algorithm Walkthrough
- First, compute the total points scored by each player across all matches. This is done by summing the
first_scoreandsecond_scorefor each player depending on whether they were the first or second player in the match. - Next, join this total points table with the
Playerstable to associate each player's total points with theirgroup_id. - Use a window function such as
ROW_NUMBER()partitioned bygroup_idand ordered by total points descending andplayer_idascending. This will assign rank 1 to the player with the most points in the group, breaking ties with the lowestplayer_id. - Finally, select only the players with rank 1 from each group. This gives the winner per group according to the rules.
Why it works: By aggregating each player's points first, we ensure correctness of the total score computation. The window function guarantees that ties are resolved properly by the player_id. Selecting only rank 1 ensures only one winner per group.
Python Solution
import pandas as pd
def tournament_winners(players: pd.DataFrame, matches: pd.DataFrame) -> pd.DataFrame:
# Compute total points for each player
first_points = matches[['first_player', 'first_score']].rename(columns={'first_player': 'player_id', 'first_score': 'points'})
second_points = matches[['second_player', 'second_score']].rename(columns={'second_player': 'player_id', 'second_score': 'points'})
all_points = pd.concat([first_points, second_points])
total_points = all_points.groupby('player_id', as_index=False)['points'].sum()
# Merge with Players table to get group_id
player_groups = players.merge(total_points, on='player_id', how='left').fillna(0)
# Sort by group_id, points descending, player_id ascending
player_groups.sort_values(by=['group_id', 'points', 'player_id'], ascending=[True, False, True], inplace=True)
# Keep the top player per group
winners = player_groups.groupby('group_id').first().reset_index()[['group_id', 'player_id']]
return winners
The code first separates scores for first and second players, concatenates them, and aggregates points per player. After merging with the Players table to get the group, it sorts by group and ranking criteria and selects the top player per group.
Go Solution
package main
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
"fmt"
)
func TournamentWinners(db *sql.DB) ([][2]int, error) {
query := `
WITH player_points AS (
SELECT player_id, SUM(points) as total_points, group_id
FROM (
SELECT first_player as player_id, first_score as points, p.group_id
FROM Matches m
JOIN Players p ON m.first_player = p.player_id
UNION ALL
SELECT second_player, second_score, p.group_id
FROM Matches m
JOIN Players p ON m.second_player = p.player_id
) AS combined
GROUP BY player_id, group_id
),
ranked AS (
SELECT group_id, player_id,
ROW_NUMBER() OVER (PARTITION BY group_id ORDER BY total_points DESC, player_id ASC) AS rn
FROM player_points
)
SELECT group_id, player_id
FROM ranked
WHERE rn = 1;
`
rows, err := db.Query(query)
if err != nil {
return nil, err
}
defer rows.Close()
var results [][2]int
for rows.Next() {
var groupID, playerID int
if err := rows.Scan(&groupID, &playerID); err != nil {
return nil, err
}
results = append(results, [2]int{groupID, playerID})
}
return results, nil
}
The Go version leverages SQL CTEs to compute points and ranks. It then scans the results into a slice of arrays. Go-specific differences include handling SQL query results explicitly with rows.Scan and managing resource cleanup with defer rows.Close().
Worked Examples
Example 1:
After computing total points:
| player_id | group_id | total_points |
|---|---|---|
| 15 | 1 | 3 |
| 25 | 1 | 2 |
| 30 | 1 | 3 |
| 45 | 1 | 0 |
| 10 | 2 | 0 |
| 35 | 2 | 1 |
| 50 | 2 | 1 |
| 20 | 3 | 2 |
| 40 | 3 | 5 |
After ranking within groups:
| group_id | player_id | total_points | rank |
|---|---|---|---|
| 1 | 15 | 3 | 1 |
| 2 | 35 | 1 | 1 |
| 3 | 40 | 5 | 1 |
Selecting rank 1 players gives the correct winners.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(M + P log P) | M for scanning matches to sum points, P log P for sorting players by group |
| Space | O(P) | Storage of aggregated points per player |
The time complexity is dominated by aggregation and sorting. Space complexity is linear in the number of players since we store total points and group associations.
Test Cases
# Example from problem statement
players_df = pd.DataFrame({
'player_id': [15,25,30,45,10,35,50,20,40],
'group_id': [1,1,1,1,2,2,2,3,3]
})
matches_df = pd.DataFrame({
'match_id': [1,2,3,4,5],
'first_player': [15,30,30,40,35],
'second_player': [45,25,15,20,50],
'first_score': [3,1,2,5,1],
'second_score': [0,2,0,2,1]
})
result = tournament_winners(players_df, matches_df)
assert set(map(tuple, result.to_numpy())) == {(1,15),(2,35),(3,40)} # Basic example
# Edge case: player with no matches
players_df2 = pd.DataFrame({'player_id':[1,2],'group_id':[1,1]})
matches_df2 = pd.DataFrame(columns=['match_id','first_player','second_player','first_score','second_score'])
result2 = tournament_winners(players_df2, matches_df2)
assert set(map(tuple, result2.to_numpy())) == {(1,1)} # player_id 1 wins tie-break by ID
# Tie in points
players_df3 = pd.DataFrame({'player_id':[1,2],'group_id':[1,1]})
matches_df3 = pd.DataFrame({'match_id':[1],'first_player':[1],'second_player':[2],'first_score':[2],'second_score':[2]})
result3 = tournament_winners(players_df3, matches_df3)
assert set(map(tuple, result3.to_numpy())) == {(1,1)} # tie broken by lowest