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:
- Ban another senator: Remove the voting rights of one opposing senator (they can't vote in current or future rounds)
- 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.
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.
Solution Approach
The solution uses two queues and simulation to model the voting process efficiently.
Data Structure Setup:
- Create two deques
qr
andqd
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
):
-
Dequeue the front senator from each party:
qr[0]
represents the next Radiant senator's positionqd[0]
represents the next Dire senator's position
-
Determine who acts first by comparing indices:
- If
qr[0] < qd[0]
: The Radiant senator at positionqr[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
- They ban the Dire senator at position
- If
qd[0] < qr[0]
: The Dire senator at positionqd[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
- They ban the Radiant senator at position
- If
-
Remove both senators from the front of their queues:
qr.popleft()
andqd.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"
ifqr
is not empty - Return
"Dire"
ifqd
is not empty (whenqr
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 EvaluatorExample 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
vsqd[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
vsqd[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 bannedqr
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).
Which of these properties could exist for a graph but not a tree?
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Queue Intro Think of the last time you stood in line to buy movie tickets The first person in line gets their ticket first and any newcomers join the end of the line This system where the first person to arrive is the first to be served is a queue in real
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
Want a Structured Path to Master System Design Too? Don’t Miss This!