Facebook Pixel

911. Online Election

MediumDesignArrayHash TableBinary Search
Leetcode Link

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 candidate persons[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 time t

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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):

  1. Data Structures Setup:

    • cnt = Counter(): A counter dictionary to track the vote count for each candidate
    • self.times = times: Store the times array for later binary search
    • self.wins = []: An array to store the winner after each vote
    • cur = 0: Variable to track the current leading candidate
  2. Processing Each Vote:

    • For each person in the persons array:
      • cnt[p] += 1: Increment the vote count for candidate p
      • 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)
      • cur = p: Update the current leader if necessary
      • self.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]), making p the new leader due to being the most recent

Query Method (q method):

  1. Binary Search:

    • bisect_right(self.times, t): Finds the insertion point for t in the sorted times 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 time t
  2. Return Winner:

    • return self.wins[i]: Return the precomputed winner at index i

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) where n 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 Evaluator

Example 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 = []

  1. Time 2, Vote for candidate 0:

    • cnt = {0: 1}, candidate 0 has 1 vote
    • Check: cnt[0] (0) <= cnt[0] (1)? Yes, so cur = 0
    • wins = [0] (candidate 0 leads)
  2. 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), so cur = 1 (candidate 1 wins tie)
    • wins = [0, 1] (candidate 1 now leads)
  3. Time 6, Vote for candidate 0:

    • cnt = {0: 2, 1: 1}, candidate 0 has 2 votes
    • Check: cnt[1] (1) <= cnt[0] (2)? Yes, so cur = 0
    • wins = [0, 1, 0] (candidate 0 leads)
  4. 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), so cur = 1 (candidate 1 wins tie)
    • wins = [0, 1, 0, 1] (candidate 1 leads)
  5. 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), where n is the length of the persons and times arrays. The constructor iterates through all n 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), where n is the length of the times array. The bisect_right function performs a binary search on the sorted times array to find the appropriate position, which takes logarithmic time. After finding the index, accessing the element in self.wins is O(1).

Space Complexity:

  • O(n), where n 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 is O(n)
    • self.wins: stores the winning candidate at each time point, which is O(n)
    • cnt (Counter): in the worst case stores up to n unique persons, which is O(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.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which of the following array represent a max heap?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More