Skip to content
← All posts

Online Election: Precompute Leaders and Binary Search

7 min read
leetcodeproblemmediumarraysbinary-searchhash-map

LeetCode 911, Online Election, is a medium problem that combines preprocessing with binary search. You are given a list of votes with timestamps, and you need to answer queries asking "who was winning at time t?" The key insight is to separate the work into two phases: an upfront preprocessing step that records the leader at every vote time, and a query step that uses binary search to find the right moment in time.

The problem

You are given two arrays: persons[i] is the candidate who received the i-th vote, and times[i] is the timestamp of that vote. Times are strictly increasing. A candidate is "leading" if they have the most votes at that point. In case of a tie, the candidate who received the most recent vote wins.

You need to implement a class with a method q(t) that returns the leader at time t. It is guaranteed that a vote exists at or before time t.

Input:  persons = [0, 1, 1, 0, 0, 1, 0], times = [0, 5, 10, 15, 20, 25, 30]
Query:  q(3) = 0, q(12) = 1, q(25) = 1, q(15) = 0, q(24) = 0, q(8) = 1
timesvotesleaders05101520253001100100110010q(12)bisect_right(times, 12) - 1 = 2 → leader = 1[0][1][2][3][4][5][6]
Precomputed leaders array from votes. A query at t=12 uses binary search to find the last vote time <= 12, which is index 2 (time 10), giving leader = 1.

Why this problem matters

This problem teaches a powerful design pattern: precompute expensive answers during initialization so that queries become cheap. Instead of recounting votes on every query, you do the work once and store the result. Then each query is just a lookup with binary search.

This pattern appears frequently in system design and real-world applications. Think of leaderboard systems that snapshot rankings at intervals, or time-series databases that precompute aggregations. The combination of hash maps for counting and binary search for time-based queries is a versatile toolkit.

The problem also reinforces how to handle tie-breaking rules cleanly. The >= comparison in the code naturally gives precedence to the most recent voter in a tie, which matches the problem statement.

The brute force approach

The naive way is to recount all votes up to time t for every query:

class TopVotedCandidate:
    def __init__(self, persons, times):
        self.persons = persons
        self.times = times

    def q(self, t):
        counts = {}
        leader = -1
        for i, time in enumerate(self.times):
            if time > t:
                break
            p = self.persons[i]
            counts[p] = counts.get(p, 0) + 1
            if counts[p] >= counts.get(leader, 0):
                leader = p
        return leader

This works but each query scans up to n votes. If you have q queries, the total time is O(n * q), which is too slow when both n and q can be up to 5000.

The key insight

The leader can only change when a vote is cast. Between two consecutive vote times, the leader stays the same. This means you only need to know the leader at each of the n vote times. For any query time t, find the last vote time that is <= t, and return the leader at that moment.

The preprocessing is a single pass through the votes, maintaining a count dictionary and tracking the current leader. The query is a binary search on the sorted times array.

The leader at any time t is the same as the leader at the most recent vote time before or at t. Precomputing leaders at each vote time reduces queries from O(n) to O(log n).

The solution

import bisect

class TopVotedCandidate:
    def __init__(self, persons, times):
        self.times = times
        self.leaders = []
        counts = {}
        leader = -1
        for p in persons:
            counts[p] = counts.get(p, 0) + 1
            if counts[p] >= counts.get(leader, 0):
                leader = p
            self.leaders.append(leader)

    def q(self, t):
        idx = bisect.bisect_right(self.times, t) - 1
        return self.leaders[idx]

Here is what each piece does:

  1. self.times = times stores the vote timestamps for binary search during queries.
  2. self.leaders = [] will hold the precomputed leader at each vote index.
  3. The for loop processes each vote in order. It increments the vote count for person p, then checks if p is now the leader. The >= comparison means that in a tie, the most recent voter wins.
  4. self.leaders.append(leader) records the leader after processing this vote.
  5. bisect.bisect_right(self.times, t) - 1 finds the largest index where times[idx] <= t. This is the last vote that was cast at or before time t.
  6. return self.leaders[idx] returns the precomputed leader at that index.

Walking through it step by step

Phase 1: Preprocessing (build leaders array)

Process each vote in order, track counts, and record the leader at each timestamp.

Preprocessing Step 1: Vote at t=0, person 0 votes.

t=0, p=0t=5, p=1t=10, p=1t=15, p=0t=20, p=0t=25, p=1t=30, p=0counts: {0:1}leader = 0 → leaders[0] = 0

Person 0 has 1 vote. They lead. leaders[0] = 0.

Preprocessing Step 2: Vote at t=5, person 1 votes.

t=0, p=0t=5, p=1t=10, p=1t=15, p=0t=20, p=0t=25, p=1t=30, p=0counts: {0:1, 1:1}leader = 1 → leaders[1] = 1

Person 1 has 1 vote, tied with person 0. Ties go to the most recent vote, so leader becomes 1.

Preprocessing Step 3: Vote at t=10, person 1 votes.

t=0, p=0t=5, p=1t=10, p=1t=15, p=0t=20, p=0t=25, p=1t=30, p=0counts: {0:1, 1:2}leader = 1 → leaders[2] = 1

Person 1 now has 2 votes. Still leading. leaders[2] = 1.

Preprocessing Step 4: Vote at t=15, person 0 votes.

t=0, p=0t=5, p=1t=10, p=1t=15, p=0t=20, p=0t=25, p=1t=30, p=0counts: {0:2, 1:2}leader = 0 → leaders[3] = 0

Person 0 now has 2 votes, tied with person 1. Most recent vote wins, so leader = 0.

Preprocessing Step 5: Vote at t=20, person 0 votes.

t=0, p=0t=5, p=1t=10, p=1t=15, p=0t=20, p=0t=25, p=1t=30, p=0counts: {0:3, 1:2}leader = 0 → leaders[4] = 0

Person 0 pulls ahead with 3 votes. leaders[4] = 0.

Preprocessing Step 6: Vote at t=25, person 1 votes.

t=0, p=0t=5, p=1t=10, p=1t=15, p=0t=20, p=0t=25, p=1t=30, p=0counts: {0:3, 1:3}leader = 1 → leaders[5] = 1

Tied 3-3. Since counts[1] >= counts[leader=0] (3 >= 3), the leader updates to person 1. The >= check means the most recent voter wins ties.

Preprocessing Step 7: Vote at t=30, person 0 votes.

t=0, p=0t=5, p=1t=10, p=1t=15, p=0t=20, p=0t=25, p=1t=30, p=0counts: {0:4, 1:3}leader = 0 → leaders[6] = 0

Person 0 has 4 votes. leaders[6] = 0. Preprocessing complete.

Phase 2: Queries (binary search on times)

For q(t), use bisect_right to find the last vote time <= t, then return leaders[idx].

Query: q(3) → bisect_right(times, 3) - 1 = 0 → leaders[0] = 0

0t=01t=51t=100t=150t=201t=250t=30q(3)=0

Time 3 falls between t=0 and t=5. The last vote at or before t=3 was at index 0.

Query: q(12) → bisect_right(times, 12) - 1 = 2 → leaders[2] = 1

0t=01t=51t=100t=150t=201t=250t=30q(12)=1

Time 12 falls between t=10 and t=15. The last vote at or before t=12 was at index 2.

Query: q(25) → bisect_right(times, 25) - 1 = 5 → leaders[5] = 1

0t=01t=51t=100t=150t=201t=250t=30q(25)=1

Time 25 exactly matches a vote time. bisect_right gives the position after 25, so index = 5.

Complexity analysis

MetricValue
TimeO(n + q log n), where q is the number of queries
SpaceO(n)

The initialization processes all n votes in a single pass, which is O(n). Each query performs a binary search on the times array, which is O(log n). If you make q queries, the total query time is O(q log n). Combined, the total is O(n + q log n).

The space is O(n) for storing the times array and the leaders array. The counts dictionary uses at most O(n) space as well during preprocessing, though it could be discarded afterward since the leaders array captures all the information needed for queries.

Building blocks

1. Precomputed prefix answers

The core pattern is computing answers at each prefix so that later queries become lookups:

answers = []
state = initial_state
for item in data:
    state = update(state, item)
    answers.append(extract(state))

This is useful any time queries ask about "the state at some point in the sequence." Instead of replaying the sequence for each query, you precompute and store. The Online Election problem uses this with vote counts and a running leader.

2. Binary search on sorted keys

When you have precomputed answers at sorted positions (like timestamps), queries for arbitrary positions use binary search:

import bisect
idx = bisect.bisect_right(sorted_keys, query_key) - 1
return answers[idx]

bisect_right gives the insertion point where query_key would go to keep the list sorted. Subtracting 1 gives the last existing key that is <= the query key. This pattern works for any "find the most recent event before time t" query.

The combination of precomputed prefix answers with binary search on sorted keys is a general pattern. You will see it in problems involving time-based lookups, range queries, and snapshot systems.

Edge cases

Before submitting, verify your solution handles:

  • All votes for the same person like persons = [0, 0, 0]: the leader is always 0. Every query returns 0.
  • Query at exact vote time: q(times[i]) should return leaders[i]. The bisect_right - 1 formula handles this correctly since bisect_right returns the index after the match.
  • Query between two vote times: q(t) where times[i] < t < times[i+1] should return leaders[i]. Binary search naturally finds index i.
  • Tie-breaking: when two candidates have equal votes, the one who received the most recent vote wins. The >= comparison ensures this.
  • Single vote: one vote at time 0. Any query returns that person.
  • Query at the last time: q(times[n-1]) should return the final leader.

Common mistakes

  1. Using bisect_left instead of bisect_right. With bisect_left, a query at an exact vote time might land at that index instead of after it, which still works when you subtract 1. However, bisect_right is the standard approach and makes the intent clearer: find the first position after t, then step back one.

  2. Using > instead of >= for the leader comparison. The problem states that in a tie, the most recent voter leads. Using > would not update the leader on a tie, giving the wrong answer.

  3. Not storing leaders at all. Some people try to binary search to find the index and then recount votes up to that index. This defeats the purpose of preprocessing and makes queries O(n).

  4. Off-by-one in binary search. Forgetting to subtract 1 from bisect_right gives you the index after the last valid vote, which may be out of bounds or point to a future vote.

From understanding to recall

You have walked through the Online Election algorithm in detail. The idea is clean: precompute the leader at each vote time using a running count, then answer queries with binary search on the times array. Two phases, two data structures, one clean solution.

The details that trip people up under pressure are subtle. Is the comparison >= or >? Do you use bisect_left or bisect_right? Do you subtract 1 or not? These small choices determine whether your solution passes or fails on edge cases with ties and exact timestamps.

Spaced repetition turns this from "I remember the general idea" into "I can write this in two minutes." By revisiting the precompute-and-search pattern at increasing intervals, you build reliable recall of both the structure and the details. When you see a variant, like a time-based key-value store or a snapshot array, the pattern clicks immediately.

Related posts

CodeBricks drills the precompute-and-binary-search pattern as a reusable building block. You practice the preprocessing loop and the bisect query separately, then combine them under timed conditions. When Online Election or its variants appear in an interview, you write the solution confidently without second-guessing the details.