Prerequisites

Before attempting this problem, you should be comfortable with:

  • Hash Maps - Storing and updating player scores with O(1) access
  • Heaps / Priority Queues - Efficiently finding the top K elements without sorting everything
  • Sorted Maps / TreeMaps - Maintaining scores in sorted order for efficient range queries
  • Sorting - Understanding how to sort values in descending order for the brute force approach

1. Brute Force

Intuition

The simplest approach stores player scores in a hash map. Adding a score or resetting is straightforward with hash map operations. To find the sum of the top K scores, we extract all scores, sort them in descending order, and sum the first K values. While easy to implement, sorting all scores for every top query is inefficient when there are many players.

Algorithm

  1. Use a hash map scores to store the mapping from playerId to their score.
  2. For addScore(playerId, score): if the player exists, add to their score; otherwise, initialize and add.
  3. For top(K): extract all score values, sort them in descending order, and return the sum of the first K scores.
  4. For reset(playerId): set the player's score to 0 in the hash map.
class Leaderboard:

    def __init__(self):
        self.scores = defaultdict()

    def addScore(self, playerId: int, score: int) -> None:
        if playerId not in self.scores:
            self.scores[playerId] = 0
        self.scores[playerId] += score

    def top(self, K: int) -> int:
        values = [v for _, v in sorted(self.scores.items(), key=lambda item: item[1])]
        values.sort(reverse=True)
        total, i = 0, 0
        while i < K:
            total += values[i]
            i += 1
        
        return total

    def reset(self, playerId: int) -> None:
        self.scores[playerId] = 0

Time & Space Complexity

  • Time complexity:

    • O(1)O(1) for addScore
    • O(1)O(1) for reset
    • O(NlogN)O(N \log N) for top
  • Space complexity: O(N)O(N)

Where NN is the total number of players in the leaderboard.


2. Heap for top-K

Intuition

Instead of sorting all N scores, we can use a min-heap of size K to find the top K scores more efficiently. As we iterate through all scores, we maintain a heap containing the K largest scores seen so far. When the heap exceeds size K, we remove the smallest element. After processing all scores, the heap contains exactly the top K scores, and we sum them up.

Algorithm

  1. Use a hash map scores to store the mapping from playerId to their score.
  2. For addScore(playerId, score): if the player exists, add to their score; otherwise, initialize and add.
  3. For top(K):
    • Create a min-heap.
    • For each score value, push it onto the heap. If the heap size exceeds K, pop the minimum.
    • After processing all scores, sum up all elements remaining in the heap.
  4. For reset(playerId): set the player's score to 0 in the hash map.
class Leaderboard:

    def __init__(self):
        self.scores = {}

    def addScore(self, playerId: int, score: int) -> None:
        if playerId not in self.scores:
            self.scores[playerId] = 0
        self.scores[playerId] += score

    def top(self, K: int) -> int:
    
        # This is a min-heap by default in Python.
        heap = []
        for x in self.scores.values():
            heapq.heappush(heap, x)
            if len(heap) > K:
                heapq.heappop(heap)
        res = 0
        while heap:
            res += heapq.heappop(heap)
        return res

    def reset(self, playerId: int) -> None:
        self.scores[playerId] = 0

Time & Space Complexity

  • Time complexity:

    • O(1)O(1) for addScore
    • O(1)O(1) for reset
    • O(NlogK)O(N \log K) for top
  • Space complexity: O(N+K)O(N + K)

Where NN is the total number of players in the leaderboard, and KK is the number of top-scoring players.


3. Using a TreeMap / SortedMap

Intuition

A TreeMap (or sorted dictionary) keeps scores in sorted order, allowing us to iterate from highest to lowest efficiently. The key insight is to track how many players share each score rather than storing individual player entries. When a score changes, we decrement the count for the old score and increment for the new one. For top(K), we iterate through scores in descending order, accumulating until we reach K players.

Algorithm

  1. Use two structures:
    • scores: a hash map from playerId to their score.
    • sortedScores: a TreeMap from score to the count of players with that score, sorted in descending order.
  2. For addScore(playerId, score):
    • If the player is new, add their score to both maps.
    • If the player exists, decrement the count for their old score in sortedScores (removing the entry if count becomes 0), update scores, and increment the count for the new score.
  3. For top(K):
    • Iterate through sortedScores in descending order.
    • For each score, add it to the sum as many times as there are players with that score, until K players have been counted.
  4. For reset(playerId): decrement the count for the player's score in sortedScores (removing if 0), and remove the player from scores.
from sortedcontainers import SortedDict

class Leaderboard:

    def __init__(self):
        self.scores = {}
        self.sortedScores = SortedDict()

    def addScore(self, playerId: int, score: int) -> None:

        # The scores dictionary simply contains the mapping from the
        # playerId to their score. The sortedScores contain a BST with 
        # key as the score and value as the number of players that have
        # that score.     
        if playerId not in self.scores:
            self.scores[playerId] = score
            self.sortedScores[-score] = self.sortedScores.get(-score, 0) + 1
        else:
            preScore = self.scores[playerId]
            val = self.sortedScores.get(-preScore)
            if val == 1:
                del self.sortedScores[-preScore]
            else:
                self.sortedScores[-preScore] = val - 1    
            
            newScore = preScore + score
            self.scores[playerId] = newScore
            self.sortedScores[-newScore] = self.sortedScores.get(-newScore, 0) + 1
        
    def top(self, K: int) -> int:
        count, total = 0, 0

        for key, value in self.sortedScores.items():
            times = self.sortedScores.get(key)
            for _ in range(times): 
                total += -key
                count += 1
                
                # Found top-K scores, break.
                if count == K:
                    break
                
            # Found top-K scores, break.
            if count == K:
                break
        
        return total

    def reset(self, playerId: int) -> None:
        preScore = self.scores[playerId]
        if self.sortedScores[-preScore] == 1:
            del self.sortedScores[-preScore]
        else:
            self.sortedScores[-preScore] -= 1
        del self.scores[playerId]

Time & Space Complexity

  • Time complexity:

    • O(logN)O(\log N) for addScore

    • O(logN)O(\log N) for reset. Note that this complexity is in the case when every player always maintains a unique score.

    • O(K)O(K) for top. Note that if the data structure doesn't provide a natural iterator, then we can simply get a list of all the key-value pairs and they will naturally be sorted due to the nature of this data structure. In that case, the complexity would be O(N)O(N) since we would be forming a new list.

  • Space complexity: O(N)O(N) used by the scores dictionary. Also, if you obtain all the key-value pairs in a new list in the top function, then an additional O(N)O(N) would be used.

Where NN is the total number of players in the leaderboard, and KK is the number of top-scoring players.

Common Pitfalls

Using Reset to Delete vs. Set to Zero

The reset function should remove the player's score entirely or set it to zero, depending on the problem requirements. Using the wrong approach affects how the top(K) calculation handles that player.

# Approach 1: Set score to 0 (player still exists with 0 score)
def reset(self, playerId):
    self.scores[playerId] = 0  # Will be included in calculations

# Approach 2: Remove player entirely (cleaner for TreeMap approach)
def reset(self, playerId):
    del self.scores[playerId]  # Player no longer counted

Forgetting to Handle Duplicate Scores in TreeMap

When using a TreeMap to track scores, multiple players can have the same score. You must store a count of players per score, not just a single player. Otherwise, adding a second player with the same score will overwrite the first.

# Wrong: Only stores one player per score
self.sortedScores[score] = playerId  # Overwrites previous player!

# Correct: Store count of players with each score
self.sortedScores[score] = self.sortedScores.get(score, 0) + 1

Not Updating Both Data Structures When Score Changes

When a player's score changes via addScore, you must update both the player-to-score mapping and the sorted scores structure. Forgetting to remove the old score from the sorted structure leads to stale data.

# Wrong: Only updates player score
def addScore(self, playerId, score):
    self.scores[playerId] = self.scores.get(playerId, 0) + score
    # Sorted structure still has old score!

# Correct: Update both structures
def addScore(self, playerId, score):
    if playerId in self.scores:
        oldScore = self.scores[playerId]
        # Remove old score from sorted structure
        self.sortedScores[oldScore] -= 1
        if self.sortedScores[oldScore] == 0:
            del self.sortedScores[oldScore]
    newScore = self.scores.get(playerId, 0) + score
    self.scores[playerId] = newScore
    # Add new score to sorted structure
    self.sortedScores[newScore] = self.sortedScores.get(newScore, 0) + 1