2102. Sequentially Ordinal Rank Tracker
Problem Description
You need to build a tracking system for scenic locations that can:
- Add locations one at a time (each with a unique name and an integer score)
- Query for the i-th best location, where i equals the number of times the system has been queried
Ranking Rules:
- Locations are ranked by score (higher score = better ranking)
- If two locations have equal scores, the one with the lexicographically smaller name ranks higher
System Operations:
The SORTracker
class needs three methods:
SORTracker()
: Initialize an empty tracking systemadd(name, score)
: Add a new scenic location with given name and scoreget()
: Return the i-th best location's name, where i is the cumulative query count
Key Behavior of get()
:
- First call returns the 1st best location
- Second call returns the 2nd best location
- Third call returns the 3rd best location
- And so on...
The problem guarantees that the number of queries will never exceed the number of locations added.
Example Flow: If you add locations A(score=5), B(score=3), C(score=5), D(score=7):
- Ranking would be: D(7) → A(5) → C(5) → B(3)
- Note: A ranks before C because "A" < "C" lexicographically
- First
get()
returns "D" - Second
get()
returns "A" - Third
get()
returns "C" - Fourth
get()
returns "B"
Intuition
The core challenge is maintaining a dynamically sorted collection of locations while efficiently retrieving the i-th best element where i increments with each query.
Since we need to:
- Keep locations sorted by score (descending) and name (ascending for ties)
- Access elements by index position
- Add new locations that automatically get placed in the correct sorted position
A sorted data structure like SortedList
is ideal. It maintains order automatically when elements are added and allows O(log n) insertion and O(1) index access.
The clever trick is storing (-score, name)
tuples instead of (score, name)
. Why? Python's default tuple comparison works left-to-right:
- First compares the first elements
- If equal, compares the second elements
By negating the score, we convert the natural ascending sort into descending order for scores. When scores are equal (negated values are equal), it naturally falls back to comparing names alphabetically, which is exactly what we want.
For tracking which element to return, we maintain a counter i
that starts at -1. Each get()
call increments i
by 1, so:
- First call: i becomes 0 (return 0th element - the best)
- Second call: i becomes 1 (return 1st element - second best)
- And so on...
This approach elegantly handles both the sorting requirements and the sequential access pattern without complex logic or multiple data structures.
Learn more about Data Stream and Heap (Priority Queue) patterns.
Solution Approach
The solution uses a SortedList
data structure to maintain locations in sorted order and a counter variable to track query positions.
Data Structure Setup:
self.sl = SortedList()
: Creates an ordered container that automatically maintains sorted orderself.i = -1
: Initialize query counter to -1 (will become 0 on firstget()
call)
Adding Locations (add
method):
def add(self, name: str, score: int) -> None:
self.sl.add((-score, name))
- Store each location as a tuple
(-score, name)
- Negating the score achieves descending order sorting:
- Location with score 10 becomes
(-10, name)
- Location with score 5 becomes
(-5, name)
- Since -10 < -5, the higher score appears first
- Location with score 10 becomes
- The
SortedList
automatically inserts the tuple in the correct position based on:- First: Negative score (ascending, which gives us descending by original score)
- Second: Name (ascending alphabetically when scores tie)
Retrieving Locations (get
method):
def get(self) -> str:
self.i += 1
return self.sl[self.i][1]
- Increment the counter
i
to track cumulative queries - Access the i-th element using index:
self.sl[self.i]
- Return the name part of the tuple:
[1]
gets the second element (name) from(-score, name)
Time Complexity:
add()
: O(log n) for inserting into sorted positionget()
: O(1) for index-based access
Space Complexity:
- O(n) where n is the number of locations stored
This approach elegantly handles the ranking requirements without manual sorting or complex comparisons, leveraging Python's tuple comparison and the SortedList
's automatic ordering.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate how the solution works:
Initial Setup:
- Create a
SORTracker
with an emptySortedList
andi = -1
Step 1: Add "park" with score 4
- Call:
add("park", 4)
- We store
(-4, "park")
in the SortedList - SortedList contents:
[(-4, "park")]
Step 2: Add "beach" with score 6
- Call:
add("beach", 6)
- We store
(-6, "beach")
in the SortedList - The tuple
(-6, "beach")
is automatically inserted before(-4, "park")
because -6 < -4 - SortedList contents:
[(-6, "beach"), (-4, "park")]
Step 3: First get() call
- Increment
i
from -1 to 0 - Access
self.sl[0]
which is(-6, "beach")
- Return the name part:
"beach"
Step 4: Add "lake" with score 6
- Call:
add("lake", 6)
- We store
(-6, "lake")
in the SortedList - Since -6 == -6 (same negated score), Python compares the names
- "beach" < "lake" alphabetically, so the order is maintained
- SortedList contents:
[(-6, "beach"), (-6, "lake"), (-4, "park")]
Step 5: Second get() call
- Increment
i
from 0 to 1 - Access
self.sl[1]
which is(-6, "lake")
- Return the name part:
"lake"
Step 6: Add "mountain" with score 4
- Call:
add("mountain", 4)
- We store
(-4, "mountain")
in the SortedList - Both
(-4, "mountain")
and(-4, "park")
have the same negated score - "mountain" < "park" alphabetically, so it's inserted before "park"
- SortedList contents:
[(-6, "beach"), (-6, "lake"), (-4, "mountain"), (-4, "park")]
Step 7: Third get() call
- Increment
i
from 1 to 2 - Access
self.sl[2]
which is(-4, "mountain")
- Return the name part:
"mountain"
Final Ranking Explanation:
- "beach" (score 6) - highest score, lexicographically first among score 6
- "lake" (score 6) - highest score, lexicographically second among score 6
- "mountain" (score 4) - lower score, lexicographically first among score 4
- "park" (score 4) - lower score, lexicographically second among score 4
The negation trick converts our "higher score is better" requirement into something that works with ascending sort order, while the tuple comparison automatically handles the lexicographic tiebreaker.
Solution Implementation
1from sortedcontainers import SortedList
2
3class SORTracker:
4 def __init__(self):
5 # Initialize a sorted list to maintain locations in sorted order
6 # Stores tuples of (-score, name) for automatic sorting
7 self.sorted_locations = SortedList()
8
9 # Track the current index for get() queries
10 # Starts at -1 since it's incremented before each retrieval
11 self.current_index = -1
12
13 def add(self, name: str, score: int) -> None:
14 """
15 Add a new location with its score to the tracker.
16
17 Args:
18 name: The name of the location
19 score: The score of the location
20 """
21 # Store as (-score, name) tuple for sorting:
22 # - Negative score ensures higher scores come first
23 # - Name as second element provides lexicographic tiebreaking
24 self.sorted_locations.add((-score, name))
25
26 def get(self) -> str:
27 """
28 Get the next best location based on previous queries.
29
30 Returns:
31 The name of the (i+1)-th best location where i is the number
32 of times get() has been called previously.
33 """
34 # Increment index to get the next best location
35 self.current_index += 1
36
37 # Return the name (second element) of the tuple at current index
38 return self.sorted_locations[self.current_index][1]
39
40
41# Your SORTracker object will be instantiated and called as such:
42# obj = SORTracker()
43# obj.add(name, score)
44# param_2 = obj.get()
45
1import java.util.*;
2
3class SORTracker {
4
5 // Custom class to represent a location with its name and score
6 private static class Location {
7 String name;
8 int score;
9
10 Location(String name, int score) {
11 this.name = name;
12 this.score = score;
13 }
14 }
15
16 // TreeSet to maintain locations sorted by score (descending) and name (ascending)
17 // Using custom comparator for the required ordering
18 private TreeSet<Location> orderedLocations;
19
20 // List to store locations in the order they should be returned by get()
21 // This simulates the order statistics functionality
22 private List<Location> queryResults;
23
24 // Tracks the current query index for the get() method
25 // Initialized to -1, incremented before each query
26 private int currentQueryIndex;
27
28 // Constructor - initializes the tracker with empty data structures
29 public SORTracker() {
30 // Define comparator: sort by score descending, then by name ascending
31 orderedLocations = new TreeSet<>((a, b) -> {
32 if (a.score != b.score) {
33 // Higher score comes first (descending order)
34 return Integer.compare(b.score, a.score);
35 }
36 // If scores are equal, sort by name lexicographically (ascending)
37 return a.name.compareTo(b.name);
38 });
39
40 queryResults = new ArrayList<>();
41 currentQueryIndex = -1;
42 }
43
44 // Add a new location with its name and score to the tracker
45 // Locations are maintained in sorted order by score (descending) and name (ascending)
46 public void add(String name, int score) {
47 Location newLocation = new Location(name, score);
48 orderedLocations.add(newLocation);
49
50 // Update the query results list to maintain order
51 // This ensures O(1) access in get() method
52 queryResults.clear();
53 queryResults.addAll(orderedLocations);
54 }
55
56 // Get the next best location based on the current query count
57 // Returns locations in order: highest score first, then lexicographically by name if scores are equal
58 public String get() {
59 // Increment query index to get the next location
60 currentQueryIndex++;
61
62 // Rebuild query results if needed (after additions)
63 if (queryResults.size() != orderedLocations.size()) {
64 queryResults.clear();
65 queryResults.addAll(orderedLocations);
66 }
67
68 // Return the location name at the current query index
69 return queryResults.get(currentQueryIndex).name;
70 }
71}
72
73/**
74 * Your SORTracker object will be instantiated and called as such:
75 * SORTracker obj = new SORTracker();
76 * obj.add(name,score);
77 * String param_2 = obj.get();
78 */
79
1#include <ext/pb_ds/assoc_container.hpp>
2#include <ext/pb_ds/tree_policy.hpp>
3using namespace __gnu_pbds;
4using namespace std;
5
6// Define an ordered set using GNU PBDS tree structure
7// This provides O(log n) insertion, deletion, and order statistics operations
8template <class T>
9using ordered_set = tree<T, null_type, less<T>, rb_tree_tag, tree_order_statistics_node_update>;
10
11class SORTracker {
12public:
13 // Constructor - initializes the tracker with empty ordered set
14 SORTracker() : currentQueryIndex(-1) {
15 }
16
17 // Add a new location with its name and score to the tracker
18 // Locations are stored by negative score (for descending order) and name (for lexicographic order)
19 void add(string name, int score) {
20 // Store as negative score to maintain descending order by score
21 // When scores are equal, names are compared lexicographically (ascending)
22 orderedLocations.insert({-score, name});
23 }
24
25 // Get the next best location based on the current query count
26 // Returns locations in order: highest score first, then lexicographically by name if scores are equal
27 string get() {
28 // Increment query index to get the next location
29 currentQueryIndex++;
30
31 // find_by_order(k) returns an iterator to the k-th element (0-indexed)
32 // in the sorted order of the set
33 auto resultIterator = orderedLocations.find_by_order(currentQueryIndex);
34
35 // Return the location name (second element of the pair)
36 return resultIterator->second;
37 }
38
39private:
40 // Ordered set to maintain locations sorted by score (descending) and name (ascending)
41 // Pair format: {negative_score, location_name}
42 ordered_set<pair<int, string>> orderedLocations;
43
44 // Tracks the current query index for the get() method
45 // Initialized to -1, incremented before each query
46 int currentQueryIndex;
47};
48
49/**
50 * Your SORTracker object will be instantiated and called as such:
51 * SORTracker* obj = new SORTracker();
52 * obj->add(name,score);
53 * string param_2 = obj->get();
54 */
55
1// Use a sorted array to maintain locations ordered by score (descending) and name (ascending)
2// Each element is a tuple of [score, name]
3let orderedLocations: [number, string][] = [];
4
5// Tracks the current query index for the get() method
6// Initialized to -1, incremented before each query
7let currentQueryIndex: number = -1;
8
9/**
10 * Binary search to find the correct insertion position
11 * Maintains descending order by score, ascending order by name when scores are equal
12 */
13function findInsertPosition(score: number, name: string): number {
14 let left = 0;
15 let right = orderedLocations.length;
16
17 while (left < right) {
18 const mid = Math.floor((left + right) / 2);
19 const [midScore, midName] = orderedLocations[mid];
20
21 // Compare by score first (descending), then by name (ascending)
22 if (midScore > score || (midScore === score && midName < name)) {
23 left = mid + 1;
24 } else {
25 right = mid;
26 }
27 }
28
29 return left;
30}
31
32/**
33 * Constructor - initializes the tracker with empty array
34 */
35function SORTracker(): void {
36 orderedLocations = [];
37 currentQueryIndex = -1;
38}
39
40/**
41 * Add a new location with its name and score to the tracker
42 * Locations are stored by score (descending) and name (ascending) when scores are equal
43 */
44function add(name: string, score: number): void {
45 // Find the correct position to insert the new location
46 const insertPos = findInsertPosition(score, name);
47
48 // Insert the location at the found position
49 orderedLocations.splice(insertPos, 0, [score, name]);
50}
51
52/**
53 * Get the next best location based on the current query count
54 * Returns locations in order: highest score first, then lexicographically by name if scores are equal
55 */
56function get(): string {
57 // Increment query index to get the next location
58 currentQueryIndex++;
59
60 // Return the location name at the current query index
61 // Access the second element (name) of the tuple at currentQueryIndex
62 return orderedLocations[currentQueryIndex][1];
63}
64
Time and Space Complexity
Time Complexity:
__init__()
:O(1)
- Simply initializes an empty SortedList and sets a counter.add(name, score)
:O(log n)
- The SortedList maintains sorted order using a balanced tree structure internally. Adding an element requires finding the correct position and inserting, which takes logarithmic time wheren
is the number of elements already in the list.get()
:O(log n)
- Accessing an element by index in a SortedList takesO(log n)
time due to its tree-based implementation, even though it might appear to beO(1)
like a regular list access.
Space Complexity:
O(n)
- The space complexity is linear with respect to the number of attractions added. The SortedList stores alln
attractions that have been added, where each entry contains a tuple of(-score, name)
. The counter variablei
uses constant additional space.
The code uses a SortedList to maintain attractions sorted by score (in descending order due to negation) and name (lexicographically as a tiebreaker). The negation of the score ensures that higher scores come first when sorted in ascending order.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Score Ordering - Forgetting to Negate
A frequent mistake is storing the score as-is instead of negating it, which results in ascending score order (worst locations first).
Incorrect Implementation:
def add(self, name: str, score: int) -> None:
self.sorted_locations.add((score, name)) # Wrong: sorts in ascending order
Why it fails: With positive scores, the SortedList sorts in ascending order, so a location with score 3 would appear before score 7, giving us the worst locations first.
Correct Solution:
def add(self, name: str, score: int) -> None:
self.sorted_locations.add((-score, name)) # Correct: negating for descending order
2. Index Management Error - Starting at 0
Another common error is initializing the query counter to 0 instead of -1.
Incorrect Implementation:
def __init__(self):
self.sorted_locations = SortedList()
self.current_index = 0 # Wrong: starts at 0
def get(self) -> str:
result = self.sorted_locations[self.current_index][1]
self.current_index += 1 # Incrementing after access
return result
Why it fails: This pattern increments after accessing, which works but is less clean. More critically, if someone forgets to increment or places it incorrectly, it causes off-by-one errors.
Correct Solution:
def __init__(self):
self.current_index = -1 # Start at -1
def get(self) -> str:
self.current_index += 1 # Increment first, then access
return self.sorted_locations[self.current_index][1]
3. Using Standard List with Manual Sorting
Attempting to use a regular list and sorting after each addition is inefficient.
Inefficient Implementation:
def __init__(self):
self.locations = []
self.current_index = -1
def add(self, name: str, score: int) -> None:
self.locations.append((score, name))
self.locations.sort(key=lambda x: (-x[0], x[1])) # O(n log n) every time!
Why it's problematic: This requires O(n log n) time for every addition, whereas SortedList only needs O(log n) for insertion.
Correct Solution: Use SortedList which maintains order automatically with O(log n) insertion.
4. Misunderstanding Lexicographic Ordering with Scores
Some might try complex custom comparisons when the tuple structure handles it automatically.
Overcomplicated Implementation:
def add(self, name: str, score: int) -> None:
# Trying to manually handle comparison logic
position = 0
for i, (s, n) in enumerate(self.sorted_locations):
if score > -s or (score == -s and name < n):
position = i
break
self.sorted_locations.insert(position, (-score, name))
Correct Solution: Trust Python's tuple comparison - it naturally compares element by element in order, handling both score and name comparisons correctly.
Which of the following uses divide and conquer strategy?
Recommended Readings
Median of Data Stream Given a stream of numbers find the median number at any given time accurate to 1 decimal place Example add_number 1 add_number 2 add_number 3 get_median 2 0 add_number 4 get_median 2 5 Try it yourself Explanation Intuition Brute force way is to sort the entire list every time we get a new number This would be O N log N for each add_number operation One step up is to notice that the list is sorted before we add any new number to it So every
https assets algo monster cover_photos heap svg Priority Queue and Heap What is the relationship between priority queue and heap Priority Queue is an Abstract Data Type and Heap is the concrete data structure we use to implement a priority queue Priority Queue A priority queue is a data structure
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!