911. Online Election
Problem Description
You are given information about an election where votes are cast at different times. The election data consists of two arrays:
persons[i]
represents that the i-th vote was cast for candidatepersons[i]
times[i]
represents the time when the i-th vote was cast
Your task is to implement a system that can answer queries about who was leading the election at any given time t
. When querying for time t
, all votes cast at or before time t
should be counted.
The rules for determining the leader are:
- The candidate with the most votes is the leader
- In case of a tie (multiple candidates have the same highest vote count), the candidate who most recently received a vote wins
You need to implement a TopVotedCandidate
class with:
- A constructor
TopVotedCandidate(int[] persons, int[] times)
that initializes the election data - A method
q(int t)
that returns the candidate number who was leading at timet
For example, if persons = [0, 1, 1, 0, 0, 1, 0]
and times = [0, 5, 10, 15, 20, 25, 30]
:
- At time 5: candidate 0 has 1 vote, candidate 1 has 1 vote (tie, but 1 voted more recently, so 1 leads)
- At time 15: candidate 0 has 2 votes, candidate 1 has 2 votes (tie, but 0 voted more recently at time 15, so 0 leads)
- At time 30: candidate 0 has 4 votes, candidate 1 has 3 votes (0 leads with more votes)
The solution precomputes the leader at each voting time during initialization, then uses binary search to efficiently answer queries by finding the appropriate time point.
Intuition
The key insight is that the election leader only changes when a new vote is cast. Between any two consecutive votes, the leader remains the same. This means we don't need to calculate the winner for every possible time value - we only need to know who was winning after each vote was counted.
Think about it this way: if we're asked "who was leading at time 17?", and votes were cast at times [0, 5, 10, 15, 20, 25, 30]
, we need to count all votes up to and including time 15 (since 17 is between 15 and 20). The winner at time 17 is the same as the winner right after the vote at time 15 was counted.
This observation leads us to a two-phase approach:
Phase 1 (Preprocessing): When we initialize the system, we can simulate the entire election chronologically. For each vote cast, we:
- Update the vote count for that candidate
- Check if this changes the current leader (either they now have more votes, or they tie with the current leader and become the new leader due to the tie-breaking rule)
- Record who is leading after this vote
Phase 2 (Query): When asked about time t
, we need to find which votes had been cast by that time. Since the times
array is sorted, we can use binary search to find the last vote that occurred at or before time t
. The leader at that point in the election is our answer.
The binary search specifically looks for the rightmost position where times[i] <= t
. This gives us the index of the last vote that should be counted for our query. We can then directly return the precomputed winner at that index.
This approach transforms a potentially expensive query (counting all votes up to time t
for each query) into a simple binary search and array lookup, making each query O(log n)
instead of O(n)
.
Learn more about Binary Search patterns.
Solution Approach
The implementation consists of two main components: initialization and query handling.
Initialization (__init__
method):
-
Data Structures Setup:
cnt = Counter()
: A counter dictionary to track the vote count for each candidateself.times = times
: Store the times array for later binary searchself.wins = []
: An array to store the winner after each votecur = 0
: Variable to track the current leading candidate
-
Processing Each Vote:
- For each person in the
persons
array:cnt[p] += 1
: Increment the vote count for candidatep
if cnt[cur] <= cnt[p]
: Check if the current vote changes the leader- If candidate
p
now has more votes than the current leader, they become the new leader - If candidate
p
ties with the current leader, they also become the new leader (tie-breaking rule: most recent vote wins)
- If candidate
cur = p
: Update the current leader if necessaryself.wins.append(cur)
: Record who is leading after this vote
The condition
cnt[cur] <= cnt[p]
is crucial - it handles both cases:- When
p
has strictly more votes (cnt[cur] < cnt[p]
) - When
p
ties with the current leader (cnt[cur] == cnt[p]
), makingp
the new leader due to being the most recent
- For each person in the
Query Method (q
method):
-
bisect_right(self.times, t)
: Finds the insertion point fort
in the sortedtimes
array- This returns the index where
t
would be inserted to maintain sorted order - Subtracting 1 (
- 1
) gives us the index of the last vote that occurred at or before timet
-
Return Winner:
return self.wins[i]
: Return the precomputed winner at indexi
Example Walkthrough:
If persons = [0, 1, 1, 0]
and times = [5, 10, 15, 20]
:
- After vote at time 5: candidate 0 has 1 vote, leads →
wins[0] = 0
- After vote at time 10: candidate 1 has 1 vote, ties but voted more recently →
wins[1] = 1
- After vote at time 15: candidate 1 has 2 votes, leads →
wins[2] = 1
- After vote at time 20: candidate 0 has 2 votes, ties but voted more recently →
wins[3] = 0
For query q(12)
:
bisect_right([5, 10, 15, 20], 12)
returns 2- Index
2 - 1 = 1
- Return
wins[1] = 1
Time Complexity:
- Initialization:
O(n)
wheren
is the number of votes - Query:
O(log n)
due to binary search
Space Complexity: O(n)
to store the wins
array and vote counts
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with persons = [0, 1, 0, 1, 1]
and times = [2, 4, 6, 8, 10]
.
Initialization Phase - Building the wins array:
Starting with cnt = {}, cur = 0, wins = []
-
Time 2, Vote for candidate 0:
cnt = {0: 1}
, candidate 0 has 1 vote- Check:
cnt[0] (0) <= cnt[0] (1)
? Yes, socur = 0
wins = [0]
(candidate 0 leads)
-
Time 4, Vote for candidate 1:
cnt = {0: 1, 1: 1}
, candidate 1 has 1 vote- Check:
cnt[0] (1) <= cnt[1] (1)
? Yes (tie), socur = 1
(candidate 1 wins tie) wins = [0, 1]
(candidate 1 now leads)
-
Time 6, Vote for candidate 0:
cnt = {0: 2, 1: 1}
, candidate 0 has 2 votes- Check:
cnt[1] (1) <= cnt[0] (2)
? Yes, socur = 0
wins = [0, 1, 0]
(candidate 0 leads)
-
Time 8, Vote for candidate 1:
cnt = {0: 2, 1: 2}
, candidate 1 has 2 votes- Check:
cnt[0] (2) <= cnt[1] (2)
? Yes (tie), socur = 1
(candidate 1 wins tie) wins = [0, 1, 0, 1]
(candidate 1 leads)
-
Time 10, Vote for candidate 1:
cnt = {0: 2, 1: 3}
, candidate 1 has 3 votes- Check:
cnt[1] (3) <= cnt[1] (3)
? Yes,cur
stays 1 wins = [0, 1, 0, 1, 1]
(candidate 1 leads)
Final state: times = [2, 4, 6, 8, 10]
, wins = [0, 1, 0, 1, 1]
Query Phase - Answering queries:
Query q(3)
:
- Binary search:
bisect_right([2, 4, 6, 8, 10], 3)
returns 1 - Index:
1 - 1 = 0
- Return:
wins[0] = 0
- Meaning: At time 3, only the vote at time 2 has been cast, so candidate 0 leads
Query q(7)
:
- Binary search:
bisect_right([2, 4, 6, 8, 10], 7)
returns 3 - Index:
3 - 1 = 2
- Return:
wins[2] = 0
- Meaning: At time 7, votes at times 2, 4, and 6 have been cast, candidate 0 leads with 2 votes
Query q(10)
:
- Binary search:
bisect_right([2, 4, 6, 8, 10], 10)
returns 5 - Index:
5 - 1 = 4
- Return:
wins[4] = 1
- Meaning: At time 10, all votes have been cast, candidate 1 leads with 3 votes
The key insight is that we precompute the leader after each vote during initialization, then use binary search to quickly find which leader to return for any given time query.
Solution Implementation
1from typing import List
2from collections import Counter
3import bisect
4
5class TopVotedCandidate:
6 def __init__(self, persons: List[int], times: List[int]):
7 """
8 Initialize the TopVotedCandidate with voting records.
9
10 Args:
11 persons: List of person IDs who received votes at corresponding times
12 times: List of timestamps when votes were cast (in ascending order)
13 """
14 # Counter to track vote counts for each person
15 vote_counter = Counter()
16
17 # Store the times array for binary search in queries
18 self.times = times
19
20 # List to store the leading candidate at each timestamp
21 self.leading_candidates = []
22
23 # Track the current leader
24 current_leader = 0
25
26 # Process each vote in chronological order
27 for person in persons:
28 # Increment vote count for this person
29 vote_counter[person] += 1
30
31 # Update leader if current person has equal or more votes
32 # (ties go to the most recent vote)
33 if vote_counter[current_leader] <= vote_counter[person]:
34 current_leader = person
35
36 # Record the leader after this vote
37 self.leading_candidates.append(current_leader)
38
39 def q(self, t: int) -> int:
40 """
41 Query the leading candidate at a given time.
42
43 Args:
44 t: The timestamp to query
45
46 Returns:
47 The ID of the person leading at time t
48 """
49 # Find the rightmost vote that occurred at or before time t
50 # bisect_right returns insertion point, so subtract 1 to get the index
51 index = bisect.bisect_right(self.times, t) - 1
52
53 # Return the leader at that point in time
54 return self.leading_candidates[index]
55
56
57# Your TopVotedCandidate object will be instantiated and called as such:
58# obj = TopVotedCandidate(persons, times)
59# param_1 = obj.q(t)
60
1class TopVotedCandidate {
2 // Array to store the timestamp of each vote
3 private int[] times;
4 // Array to store the winner (person with most votes) at each timestamp
5 private int[] winners;
6
7 /**
8 * Constructor to preprocess the voting data
9 * @param persons Array where persons[i] is the person that received a vote at time times[i]
10 * @param times Array of timestamps when votes were cast (monotonically increasing)
11 */
12 public TopVotedCandidate(int[] persons, int[] times) {
13 int n = persons.length;
14 this.winners = new int[n];
15 this.times = times;
16
17 // Array to count votes for each person (person IDs are 0 to n-1)
18 int[] voteCount = new int[n];
19 // Track the current leader (person with most votes)
20 int currentLeader = 0;
21
22 // Process each vote chronologically
23 for (int i = 0; i < n; i++) {
24 int person = persons[i];
25 // Increment vote count for this person
26 voteCount[person]++;
27
28 // Update the leader if current person has equal or more votes
29 // (ties go to the most recent person)
30 if (voteCount[currentLeader] <= voteCount[person]) {
31 currentLeader = person;
32 }
33
34 // Store the winner at this point in time
35 winners[i] = currentLeader;
36 }
37 }
38
39 /**
40 * Query the person who was leading at time t
41 * @param t The query timestamp
42 * @return The person who was leading at time t
43 */
44 public int q(int t) {
45 // Use binary search to find the insertion point for (t + 1)
46 // This helps us find the rightmost vote that occurred at or before time t
47 int index = Arrays.binarySearch(times, t + 1);
48
49 // If (t + 1) is not found, binarySearch returns (-(insertion point) - 1)
50 // Convert this to get the index of the last vote at or before time t
51 if (index < 0) {
52 index = -index - 2; // Get the index just before the insertion point
53 } else {
54 index = index - 1; // If found, we need the previous index
55 }
56
57 return winners[index];
58 }
59}
60
61/**
62 * Your TopVotedCandidate object will be instantiated and called as such:
63 * TopVotedCandidate obj = new TopVotedCandidate(persons, times);
64 * int param_1 = obj.q(t);
65 */
66
1class TopVotedCandidate {
2public:
3 /**
4 * Constructor: Precomputes the winner at each voting time point
5 * @param persons - Array where persons[i] is the person that the i-th vote is cast for
6 * @param times - Array where times[i] is the time at which the i-th vote was cast
7 */
8 TopVotedCandidate(vector<int>& persons, vector<int>& times) {
9 int n = persons.size();
10 this->times = times;
11 winners.resize(n);
12
13 // Track vote count for each person
14 vector<int> voteCount(n);
15 int currentLeader = 0;
16
17 // Process each vote chronologically
18 for (int i = 0; i < n; ++i) {
19 int person = persons[i];
20 ++voteCount[person];
21
22 // Update leader if current person has equal or more votes
23 // (ties go to most recent vote)
24 if (voteCount[currentLeader] <= voteCount[person]) {
25 currentLeader = person;
26 }
27
28 // Store the winner at this time point
29 winners[i] = currentLeader;
30 }
31 }
32
33 /**
34 * Query: Returns the person that was leading at time t
35 * @param t - The query time
36 * @return The person who was leading at time t
37 */
38 int q(int t) {
39 // Find the rightmost vote that occurred at or before time t
40 // upper_bound returns iterator to first element > t
41 // Subtracting 1 gives us the last element <= t
42 int index = upper_bound(times.begin(), times.end(), t) - times.begin() - 1;
43 return winners[index];
44 }
45
46private:
47 vector<int> times; // Stores the original times array
48 vector<int> winners; // winners[i] stores the leader after the i-th vote
49};
50
51/**
52 * Your TopVotedCandidate object will be instantiated and called as such:
53 * TopVotedCandidate* obj = new TopVotedCandidate(persons, times);
54 * int param_1 = obj->q(t);
55 */
56
1// Global variables to store election data
2let electionTimes: number[] = [];
3let electionWinners: number[] = [];
4
5/**
6 * Initialize the election system with persons who voted and their voting times
7 * @param persons - Array where persons[i] is the person that the ith vote was cast for
8 * @param times - Array where times[i] is the time at which the ith vote was cast (non-decreasing order)
9 */
10function initializeTopVotedCandidate(persons: number[], times: number[]): void {
11 const numberOfVotes = persons.length;
12 electionTimes = times;
13 electionWinners = new Array<number>(numberOfVotes).fill(0);
14
15 // Track vote count for each person
16 const voteCount: number[] = new Array<number>(numberOfVotes).fill(0);
17 let currentLeader = 0;
18
19 // Process each vote and determine the leader at each point in time
20 for (let i = 0; i < numberOfVotes; ++i) {
21 const votedPerson = persons[i];
22 voteCount[votedPerson]++;
23
24 // Update leader if current person has equal or more votes (ties go to most recent)
25 if (voteCount[currentLeader] <= voteCount[votedPerson]) {
26 currentLeader = votedPerson;
27 }
28
29 // Store the leader at this point in time
30 electionWinners[i] = currentLeader;
31 }
32}
33
34/**
35 * Query the winner at a given time t
36 * @param t - The time to query for the election winner
37 * @returns The person who was leading at time t
38 */
39function q(t: number): number {
40 // Find the rightmost vote that occurred at or before time t
41 const voteIndex = binarySearchForTime(t) - 1;
42 return electionWinners[voteIndex];
43}
44
45/**
46 * Binary search to find the first time greater than t
47 * @param targetTime - The time to search for
48 * @returns Index of the first element greater than targetTime
49 */
50function binarySearchForTime(targetTime: number): number {
51 let left = 0;
52 let right = electionTimes.length;
53
54 // Binary search for upper bound (first element > targetTime)
55 while (left < right) {
56 const mid = (left + right) >> 1; // Equivalent to Math.floor((left + right) / 2)
57
58 if (electionTimes[mid] > targetTime) {
59 right = mid;
60 } else {
61 left = mid + 1;
62 }
63 }
64
65 return left;
66}
67
Time and Space Complexity
Time Complexity:
-
Initialization (
__init__
):O(n)
, wheren
is the length of thepersons
andtimes
arrays. The constructor iterates through alln
persons exactly once, and for each person, it performs constant-time operations (updating the counter, comparison, and appending to the wins list). -
Query (
q
):O(log n)
, wheren
is the length of thetimes
array. Thebisect_right
function performs a binary search on the sortedtimes
array to find the appropriate position, which takes logarithmic time. After finding the index, accessing the element inself.wins
isO(1)
.
Space Complexity:
-
O(n)
, wheren
is the length of the input arrays. The space is used to store:self.times
: stores a reference to the input times array (or a copy of it), which isO(n)
self.wins
: stores the winning candidate at each time point, which isO(n)
cnt
(Counter): in the worst case stores up ton
unique persons, which isO(n)
The total space complexity is therefore
O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Tie-Breaking Logic
Pitfall: A common mistake is using strict inequality (<
) instead of less-than-or-equal (<=
) when checking if a candidate should become the new leader:
# WRONG - This doesn't handle ties correctly if vote_counter[current_leader] < vote_counter[person]: current_leader = person
This fails because when there's a tie, the most recent voter should win, but strict inequality won't update the leader.
Solution: Always use <=
to ensure ties are broken in favor of the most recent voter:
# CORRECT - Handles both cases: more votes OR tied votes if vote_counter[current_leader] <= vote_counter[person]: current_leader = person
2. Off-by-One Error in Binary Search
Pitfall: Using bisect_left
instead of bisect_right
, or forgetting to subtract 1:
# WRONG - bisect_left might give incorrect results for exact matches index = bisect.bisect_left(self.times, t) # WRONG - forgetting to subtract 1 index = bisect.bisect_right(self.times, t)
Solution: Use bisect_right
and subtract 1 to get the last vote at or before time t
:
# CORRECT index = bisect.bisect_right(self.times, t) - 1
3. Not Handling Edge Cases
Pitfall: Not considering queries for times before the first vote:
def q(self, t: int) -> int:
index = bisect.bisect_right(self.times, t) - 1
# If t < times[0], index becomes -1, causing incorrect array access
return self.leading_candidates[index]
Solution: Add validation or ensure the problem constraints guarantee valid queries:
def q(self, t: int) -> int:
index = bisect.bisect_right(self.times, t) - 1
# Ensure index is valid (though usually guaranteed by problem constraints)
if index < 0:
return -1 # or handle according to requirements
return self.leading_candidates[index]
4. Inefficient Query Implementation
Pitfall: Recalculating the leader for each query instead of precomputing:
# WRONG - O(n) per query
def q(self, t: int) -> int:
vote_count = Counter()
leader = -1
for i in range(len(self.times)):
if self.times[i] <= t:
vote_count[self.persons[i]] += 1
# Recalculate leader...
return leader
Solution: Precompute leaders during initialization and use binary search for O(log n) queries.
Which of the following array represent a max heap?
Recommended Readings
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
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
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!