LeetCode 2173 - Longest Winning Streak
This problem is asking us to determine the longest sequence of consecutive wins, also called a winning streak, for each player in a match history. The input is a Matches table with three columns: playerid, matchday, and result.
Difficulty: 🔴 Hard
Topics: Database
Solution
Problem Understanding
This problem is asking us to determine the longest sequence of consecutive wins, also called a winning streak, for each player in a match history. The input is a Matches table with three columns: player_id, match_day, and result. Each row represents one match played by a specific player on a given day, with the match result being either Win, Draw, or Lose. The combination (player_id, match_day) is guaranteed to be unique, which ensures that each match is distinct.
The expected output is a table that lists every player_id along with their longest winning streak, which is the maximum number of consecutive matches that ended in a Win without any Draw or Lose interrupting the sequence. If a player has never won a match, their longest streak is 0.
Key constraints and observations include:
- Matches are not guaranteed to be consecutive in dates for a player.
- A streak is only broken by
DraworLose. - The dataset can have multiple players, each with varying numbers of matches.
- Edge cases include players who never win, players with only one match, and players whose wins are scattered with draws or losses.
Understanding these constraints ensures we handle both sparse and dense match histories, and correctly compute consecutive sequences without miscounting.
Approaches
Brute-Force Approach
A straightforward approach would involve iterating through each player’s matches sorted by match_day and maintaining a counter for consecutive wins. Whenever a Draw or Lose is encountered, we reset the counter and update the maximum streak. While this approach is simple and guarantees correct results, it requires sorting matches for each player and iterating through them, which is potentially inefficient for large datasets. The database implementation might require multiple scans or complex subqueries to achieve this.
Optimal Approach
The key insight for an optimal solution is to use SQL window functions, specifically ROW_NUMBER(), to identify contiguous sequences of wins. By assigning a row number to each match in chronological order and then computing a “group identifier” as the difference between the row number and a cumulative count of wins, we can segment winning streaks efficiently. This allows grouping consecutive wins together and counting them in a single query. This approach avoids repeated scans and leverages the database's set-based operations.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n log n) | O(n) | Sort matches per player and iterate to compute streaks |
| Optimal | O(n) | O(n) | Use window functions to group consecutive wins and compute max streak |
Algorithm Walkthrough
- Partition the matches by
player_idand order them bymatch_dayusing theROW_NUMBER()window function. - Create a cumulative count of wins per player using a running sum, treating
Winas 1 and others as 0. - Compute a group identifier for consecutive wins by subtracting the cumulative win count from the row number. This groups consecutive wins together while ignoring
DrawandLose. - Filter only
Winmatches, as only wins contribute to a streak. - Group by
player_idand the calculated group identifier, counting matches in each winning streak. - Compute the maximum streak per player using
MAX. - Ensure players who have never won a match are included with a streak of
0using aLEFT JOINorCOALESCE.
Why it works: The difference between row number and cumulative win count remains constant for consecutive wins. When a non-win occurs, the difference changes, effectively segmenting winning streaks. Counting within these segments produces the correct length for each streak, and taking the maximum per player yields the desired result.
Python Solution
import sqlite3
from typing import List, Tuple
def longest_winning_streak(conn: sqlite3.Connection) -> List[Tuple[int, int]]:
query = """
WITH numbered AS (
SELECT
player_id,
match_day,
result,
ROW_NUMBER() OVER(PARTITION BY player_id ORDER BY match_day) AS rn,
SUM(CASE WHEN result = 'Win' THEN 1 ELSE 0 END) OVER(PARTITION BY player_id ORDER BY match_day ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS win_count
FROM Matches
),
streaks AS (
SELECT
player_id,
rn - win_count AS grp
FROM numbered
WHERE result = 'Win'
)
SELECT
m.player_id,
COALESCE(MAX(COUNT(s.grp)) OVER(PARTITION BY m.player_id), 0) AS longest_streak
FROM Matches m
LEFT JOIN streaks s ON m.player_id = s.player_id
GROUP BY m.player_id
"""
cursor = conn.cursor()
cursor.execute(query)
return cursor.fetchall()
This Python solution executes a single SQL query using SQLite. It uses a common table expression (CTE) to compute row numbers and cumulative wins, then identifies streaks by grouping. Finally, it computes the maximum streak for each player while handling players with no wins using COALESCE.
Go Solution
package main
import (
"database/sql"
_ "github.com/mattn/go-sqlite3"
)
func LongestWinningStreak(db *sql.DB) ([]struct{PlayerID, LongestStreak int}, error) {
query := `
WITH numbered AS (
SELECT
player_id,
match_day,
result,
ROW_NUMBER() OVER(PARTITION BY player_id ORDER BY match_day) AS rn,
SUM(CASE WHEN result = 'Win' THEN 1 ELSE 0 END) OVER(PARTITION BY player_id ORDER BY match_day ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS win_count
FROM Matches
),
streaks AS (
SELECT
player_id,
rn - win_count AS grp
FROM numbered
WHERE result = 'Win'
)
SELECT
m.player_id,
COALESCE(MAX(COUNT(s.grp)) OVER(PARTITION BY m.player_id), 0) AS longest_streak
FROM Matches m
LEFT JOIN streaks s ON m.player_id = s.player_id
GROUP BY m.player_id
`
rows, err := db.Query(query)
if err != nil {
return nil, err
}
defer rows.Close()
var results []struct{ PlayerID, LongestStreak int }
for rows.Next() {
var r struct{ PlayerID, LongestStreak int }
if err := rows.Scan(&r.PlayerID, &r.LongestStreak); err != nil {
return nil, err
}
results = append(results, r)
}
return results, nil
}
In Go, the solution executes the same SQL query and scans results into a slice of structs. Go requires explicit handling of SQL rows and errors, and there is no COALESCE equivalent in Go code, as it is handled in SQL.
Worked Examples
Example 1
For player 1:
| match_day | result | rn | win_count | grp |
|---|---|---|---|---|
| 2022-01-17 | Win | 1 | 1 | 0 |
| 2022-01-18 | Win | 2 | 2 | 0 |
| 2022-01-25 | Win | 3 | 3 | 0 |
| 2022-01-31 | Draw | 4 | 3 | - |
| 2022-02-08 | Win | 5 | 4 | 1 |
The groups for wins are 0 and 1, giving streaks of lengths 3 and 1. Maximum is 3.
Player 2 has no wins, so maximum streak is 0.
Player 3 has one win, so maximum streak is 1.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Each match is processed once; window functions compute row numbers and cumulative sums efficiently |
| Space | O(n) | Storing row numbers, cumulative sums, and grouping requires extra space proportional to number of matches |
The algorithm leverages database-level window functions, avoiding explicit nested loops, making it scalable for large tables.
Test Cases
# test cases
assert longest_winning_streak(conn) == [(1, 3), (2, 0), (3, 1)] # Example 1
# Player with all losses
assert longest_winning_streak(conn_loss_only) == [(1, 0)]
# Player with alternating wins and draws
assert longest_winning_streak(conn_alternating) == [(1, 1)]
# Player with a single match win
assert longest_winning_streak(conn_single_win) == [(1, 1)]
# Multiple players with mixed results
assert longest_winning_streak(conn_multiple) == [(1, 3), (2, 2), (3, 1)]
| Test | Why |
|---|---|
| Example 1 | Validates basic streak calculation |
| All losses | Ensures streak 0 is handled correctly |
| Alternating wins/draws | Tests streak resets on non-win |
| Single match win | Edge case for minimal input |
| Multiple players | Checks correctness with multiple IDs |
Edge Cases
One important edge case is a player who never