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

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 5-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.

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

Loading...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What is the best way of checking if an element exists in an unsorted array once in terms of time complexity? Select the best that applies.


Recommended Readings

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

Load More