Facebook Pixel

649. Dota2 Senate

Problem Description

This problem is about simulating a voting process in the Dota2 senate between two parties: Radiant ('R') and Dire ('D').

You're given a string senate where each character represents a senator from either party. The senators vote in rounds, going from left to right in the string order. Each senator, when it's their turn, can take one of two actions:

  1. Ban another senator: Remove the voting rights of one opposing senator (they can't vote in current or future rounds)
  2. Declare victory: If all remaining senators with voting rights belong to the same party, that party wins

The key rules are:

  • Senators act in the order they appear in the string
  • Each senator plays optimally for their own party
  • A banned senator is skipped in all future rounds
  • The process continues in rounds until one party achieves victory

The optimal strategy is greedy: each senator should ban the next opposing senator who would get to vote. This is implemented using two queues to track the positions of 'R' and 'D' senators. When comparing senators from each queue:

  • The senator with the smaller index gets to act first
  • They ban the opposing senator (remove from opposing queue)
  • The acting senator gets added back to their queue with index increased by n (to participate in the next round)

For example, with senate = "RDD":

  • Round 1: R at position 0 bans D at position 1
  • Round 1 continued: D at position 2 bans R (who would be at position 3 in next round)
  • Result: Only D remains, so "Dire" wins

The solution simulates this process until one queue is empty, indicating that party has no senators left and the other party wins.

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

Intuition

The key insight is that each senator should use their voting power as soon as possible to eliminate opponents before those opponents can vote. Since senators act in order, we need to think about who gets to act first.

Consider what happens when a senator gets their turn: they want to ban an opposing senator. But which one? The optimal choice is to ban the next opposing senator who would get a turn. Why? Because that senator poses the most immediate threat - they could ban someone from your party very soon.

This leads us to a greedy strategy: when it's your turn, always ban the earliest opposing senator in the queue who hasn't been banned yet. This prevents them from exercising their power against your party.

To implement this, we can track the positions of all 'R' and 'D' senators. When we compare two senators (one from each party), whoever has the smaller index gets to act first due to the left-to-right processing order. That senator bans the other one.

But here's the clever part: after a senator successfully bans an opponent, they're still alive and will get another turn in the next round. To model this "circular" behavior where senators keep coming back for future rounds, we add n to their index when re-queuing them. This ensures they maintain proper ordering relative to other senators while indicating they're participating in a later round.

Using two queues (one for each party) naturally handles this process:

  • Dequeue the front senator from each party
  • Compare their indices to see who acts first
  • The winner bans the loser (loser is removed)
  • The winner goes back to their queue with index + n for the next round

This continues until one queue is empty, meaning that party has no senators left to vote, and the other party wins.

Learn more about Greedy and Queue patterns.

Solution Approach

The solution uses two queues and simulation to model the voting process efficiently.

Data Structure Setup:

  • Create two deques qr and qd to store the indices of Radiant and Dire senators respectively
  • Iterate through the input string and populate the queues with the original indices (0 to n-1)

Simulation Process: The main loop continues while both queues have senators (while qr and qd):

  1. Dequeue the front senator from each party:

    • qr[0] represents the next Radiant senator's position
    • qd[0] represents the next Dire senator's position
  2. Determine who acts first by comparing indices:

    • If qr[0] < qd[0]: The Radiant senator at position qr[0] gets to act first
      • They ban the Dire senator at position qd[0]
      • The Radiant senator survives and gets re-queued with index qr[0] + n
    • If qd[0] < qr[0]: The Dire senator at position qd[0] gets to act first
      • They ban the Radiant senator at position qr[0]
      • The Dire senator survives and gets re-queued with index qd[0] + n
  3. Remove both senators from the front of their queues:

    • qr.popleft() and qd.popleft() are called regardless of who won
    • The winner was already added back to their queue with an updated index

Why add n to the index? Adding n ensures that senators who survive maintain their relative ordering for future rounds. For example, if a senator at position 2 survives round 1, they become position 2 + n for round 2, which is guaranteed to be larger than any first-round positions but maintains order among second-round participants.

Determining the Winner: After the loop ends, one queue will be empty while the other still has senators. The party with remaining senators wins:

  • Return "Radiant" if qr is not empty
  • Return "Dire" if qd is not empty (when qr is empty)

Time Complexity: O(n) where n is the length of the senate string. Each senator can be processed at most twice. Space Complexity: O(n) for storing the two queues.

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 the example senate = "RDDR" step by step:

Initial Setup:

  • n = 4 (length of senate)
  • qr = [0, 2] (Radiant senators at positions 0 and 2)
  • qd = [1, 3] (Dire senators at positions 1 and 3)

Round 1, First Vote:

  • Compare front senators: qr[0] = 0 vs qd[0] = 1
  • Since 0 < 1, Radiant senator at position 0 acts first
  • R(0) bans D(1), then gets re-queued at position 0 + 4 = 4
  • Remove both from front: qr.popleft(), qd.popleft()
  • State: qr = [2, 4], qd = [3]

Round 1, Second Vote:

  • Compare front senators: qr[0] = 2 vs qd[0] = 3
  • Since 2 < 3, Radiant senator at position 2 acts first
  • R(2) bans D(3), then gets re-queued at position 2 + 4 = 6
  • Remove both from front: qr.popleft(), qd.popleft()
  • State: qr = [4, 6], qd = [] (empty!)

Result:

  • qd is empty, meaning all Dire senators have been banned
  • qr still has senators [4, 6] with voting rights
  • Return "Radiant"

The key insight: Even though the original string had equal numbers of R and D senators, the Radiant senators at positions 0 and 2 got to act before the Dire senators at positions 1 and 3, allowing them to eliminate all opposition before those Dire senators could retaliate.

Solution Implementation

1from collections import deque
2from typing import Deque
3
4class Solution:
5    def predictPartyVictory(self, senate: str) -> str:
6        # Initialize two queues to store the indices of R and D senators
7        radiant_queue: Deque[int] = deque()
8        dire_queue: Deque[int] = deque()
9      
10        # Populate the queues with initial positions of each party's senators
11        for index, senator in enumerate(senate):
12            if senator == "R":
13                radiant_queue.append(index)
14            else:
15                dire_queue.append(index)
16      
17        # Store the length of the senate for calculating next round positions
18        senate_length = len(senate)
19      
20        # Simulate the voting process until one party has no senators left
21        while radiant_queue and dire_queue:
22            # Get the current positions of the first senator from each party
23            radiant_position = radiant_queue.popleft()
24            dire_position = dire_queue.popleft()
25          
26            # The senator with the smaller index acts first and bans the other
27            if radiant_position < dire_position:
28                # Radiant senator bans Dire senator and gets to vote again next round
29                radiant_queue.append(radiant_position + senate_length)
30            else:
31                # Dire senator bans Radiant senator and gets to vote again next round
32                dire_queue.append(dire_position + senate_length)
33      
34        # Return the party that still has senators remaining
35        return "Radiant" if radiant_queue else "Dire"
36
1class Solution {
2    public String predictPartyVictory(String senate) {
3        int senateLength = senate.length();
4      
5        // Queue to store indices of Radiant senators
6        Deque<Integer> radiantQueue = new ArrayDeque<>();
7        // Queue to store indices of Dire senators
8        Deque<Integer> direQueue = new ArrayDeque<>();
9      
10        // Initialize queues with initial positions of senators
11        for (int i = 0; i < senateLength; ++i) {
12            if (senate.charAt(i) == 'R') {
13                radiantQueue.offer(i);
14            } else {
15                direQueue.offer(i);
16            }
17        }
18      
19        // Simulate the voting process until one party has no senators left
20        while (!radiantQueue.isEmpty() && !direQueue.isEmpty()) {
21            // Get the indices of the next senators from each party
22            int radiantIndex = radiantQueue.poll();
23            int direIndex = direQueue.poll();
24          
25            // The senator with the smaller index acts first and bans the other
26            // The winning senator gets to act again in the next round (index + n)
27            if (radiantIndex < direIndex) {
28                // Radiant senator bans Dire senator and queues for next round
29                radiantQueue.offer(radiantIndex + senateLength);
30            } else {
31                // Dire senator bans Radiant senator and queues for next round
32                direQueue.offer(direIndex + senateLength);
33            }
34        }
35      
36        // Return the name of the winning party
37        return radiantQueue.isEmpty() ? "Dire" : "Radiant";
38    }
39}
40
1class Solution {
2public:
3    string predictPartyVictory(string senate) {
4        int senateSize = senate.size();
5      
6        // Queue to store indices of Radiant senators
7        queue<int> radiantQueue;
8        // Queue to store indices of Dire senators
9        queue<int> direQueue;
10      
11        // Initialize queues with initial positions of each party's senators
12        for (int i = 0; i < senateSize; ++i) {
13            if (senate[i] == 'R') {
14                radiantQueue.push(i);
15            } else {
16                direQueue.push(i);
17            }
18        }
19      
20        // Simulate the voting process until one party has no senators left
21        while (!radiantQueue.empty() && !direQueue.empty()) {
22            // Get the front senator from each party
23            int radiantIndex = radiantQueue.front();
24            int direIndex = direQueue.front();
25            radiantQueue.pop();
26            direQueue.pop();
27          
28            // The senator with smaller index acts first and bans the opponent
29            // The winner gets to vote again in the next round (add senateSize to maintain order)
30            if (radiantIndex < direIndex) {
31                // Radiant senator bans Dire senator and gets next turn
32                radiantQueue.push(radiantIndex + senateSize);
33            } else {
34                // Dire senator bans Radiant senator and gets next turn
35                direQueue.push(direIndex + senateSize);
36            }
37        }
38      
39        // Return the party name that still has senators
40        return radiantQueue.empty() ? "Dire" : "Radiant";
41    }
42};
43
1/**
2 * Predicts which party will win in the Dota2 Senate voting game
3 * Uses a greedy strategy where each senator bans the next opponent senator
4 * 
5 * @param senate - String representing the senate with 'R' for Radiant and 'D' for Dire
6 * @returns The name of the winning party: "Radiant" or "Dire"
7 */
8function predictPartyVictory(senate: string): string {
9    const senateLength: number = senate.length;
10  
11    // Queue to store indices of Radiant senators
12    const radiantQueue: number[] = [];
13    // Queue to store indices of Dire senators
14    const direQueue: number[] = [];
15  
16    // Initialize queues with initial positions of each senator
17    for (let i = 0; i < senateLength; ++i) {
18        if (senate[i] === 'R') {
19            radiantQueue.push(i);
20        } else {
21            direQueue.push(i);
22        }
23    }
24  
25    // Simulate the voting process
26    // Each senator bans the next opponent in the circular queue
27    while (radiantQueue.length > 0 && direQueue.length > 0) {
28        // Get the front senator from each party
29        const radiantIndex: number = radiantQueue.shift()!;
30        const direIndex: number = direQueue.shift()!;
31      
32        // The senator with smaller index acts first and bans the opponent
33        // The winner gets to act again in the next round (circular)
34        if (radiantIndex < direIndex) {
35            // Radiant senator bans Dire senator and gets next turn
36            radiantQueue.push(radiantIndex + senateLength);
37        } else {
38            // Dire senator bans Radiant senator and gets next turn
39            direQueue.push(direIndex + senateLength);
40        }
41    }
42  
43    // The party with remaining senators wins
44    return radiantQueue.length > 0 ? 'Radiant' : 'Dire';
45}
46

Time and Space Complexity

Time Complexity: O(n)

Each senator gets processed at most twice - once in their initial position and potentially once more when they're added back to the queue with index i + n. In the worst case, we might have alternating senators (like "RDRDRD..."), where each round eliminates one senator from each party. This would result in approximately n/2 rounds, and in each round, we process 2 senators. Therefore, the total number of operations is bounded by O(n).

Space Complexity: O(n)

We use two deques qr and qd to store the indices of senators. In the worst case, all senators belong to one party initially, requiring O(n) space. Even though we add indices with value i + n during processing, at any point in time, the total number of elements across both queues never exceeds n (as we remove one element for each element we add). Therefore, the space complexity is O(n).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Not Understanding Why We Add n to Indices

A common mistake is trying to use boolean flags or separate tracking arrays to mark banned senators instead of the queue-based approach with index incrementation.

Incorrect Approach:

def predictPartyVictory(self, senate: str) -> str:
    banned = [False] * len(senate)
    r_count = senate.count('R')
    d_count = senate.count('D')
  
    while r_count > 0 and d_count > 0:
        for i in range(len(senate)):
            if banned[i]:
                continue
            # Complex logic to find and ban next opponent...

Why it's problematic: This approach requires multiple passes through the array and complex bookkeeping to track who can still vote in each round.

Solution: Use the queue approach where adding n naturally handles the round-based ordering without extra data structures.

2. Forgetting to Remove Both Senators from Queues

A critical error is only removing the banned senator from their queue while forgetting to remove the acting senator (who gets re-added with updated index).

Incorrect Code:

while radiant_queue and dire_queue:
    if radiant_queue[0] < dire_queue[0]:
        dire_queue.popleft()  # Only removing the banned senator
        radiant_queue.append(radiant_queue[0] + senate_length)
        # Missing: radiant_queue.popleft()

Why it fails: The acting senator never gets removed from the front of their queue, causing an infinite loop or incorrect simulation.

Solution: Always remove both senators from the front of their queues, regardless of who wins the confrontation:

radiant_position = radiant_queue.popleft()  # Remove first
dire_position = dire_queue.popleft()        # Remove first
# Then decide who survives and gets re-queued

3. Using Wrong Data Structure

Using a list instead of a deque leads to inefficient operations.

Inefficient Approach:

radiant_queue = []
dire_queue = []
# Later in the loop:
radiant_position = radiant_queue.pop(0)  # O(n) operation!

Why it's problematic: list.pop(0) is O(n) because it requires shifting all remaining elements. With multiple rounds, this creates O(n²) time complexity.

Solution: Use collections.deque which provides O(1) operations for both append() and popleft():

from collections import deque
radiant_queue = deque()
dire_queue = deque()

4. Misunderstanding the Greedy Strategy

Some might think each senator should ban the strongest future opponent or use complex lookahead logic.

Overthinking Example:

# Trying to find the "best" senator to ban
def find_best_target(queue, positions):
    # Complex logic to evaluate future threats...

Why it's wrong: The optimal strategy is simply to ban the very next opposing senator who would vote. Any other strategy allows more opposing senators to act.

Solution: Trust the greedy approach - always ban the opponent at the front of their queue (the next one to act).

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

Which of these properties could exist for a graph but not a tree?


Recommended Readings

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

Load More