Problem Overview
LeetCode 649 Dota2 Senate presents a string of characters, each either R (Radiant) or D (Dire), representing senators in a circular voting session. In each round, every senator can either ban one senator from the opposing party or, if no opposition remains, declare victory for their own party. The goal is to determine which party — "Radiant" or "Dire" — ultimately wins.
The process is circular: senators act in the order they appear in the string, and the round repeats until one side is completely eliminated. A senator who has been banned cannot act. The string length n can be up to 10,000, so an efficient simulation is required — a naive O(n^2) per-round approach would be too slow.
Understanding the circular nature is key. After all senators in the original string have acted once, any surviving senators begin again in the same relative order. This circularity is what drives the two-queue design: instead of modifying a circular list, we simulate re-entry by appending the winning senator back to their queue with an updated index.
- Input: string of R and D characters representing senators
- Each round: a senator bans one opponent or declares victory
- Process is circular — surviving senators act again in order
- Goal: return "Radiant" or "Dire" (the winning party)
- Constraint: 1 <= senate.length <= 10,000
Greedy Insight
The optimal strategy for every senator is to ban the earliest remaining opponent — the one who would act next in the queue. Banning any other senator is strictly suboptimal: if you skip the next opponent, they will act before your banned senator would have, costing your party an ally. The greedy choice is always to eliminate the most immediate threat.
This greedy approach is provably correct by an exchange argument. Suppose senator A at position i chooses to ban senator C at position k instead of the nearer opponent B at position j (where j < k). Then B acts before C would have, banning one of A's allies. The net result is no better than if A had banned B directly. Therefore, banning the earliest opponent is always at least as good as any other choice.
The greedy insight reduces the problem to a simple comparison: at each step, compare the front of the Radiant queue and the front of the Dire queue. Whichever senator has the smaller index acts first and eliminates the other. The winner re-enters their queue at index+n to represent participation in the next round.
Always Ban the Earliest Opponent
Each senator's optimal strategy is to ban the earliest remaining opponent — the one at the front of the opposing queue. This prevents the opponent from acting next and banning one of your own senators. By an exchange argument, any other ban is strictly suboptimal: the skipped opponent will act sooner and deal damage your party could have avoided.
Two-Queue Approach
Initialize two queues: one for Radiant senator indices and one for Dire senator indices, populated by scanning the input string left to right. For example, "RDD" gives radiant=[0] and dire=[1,2]. These indices represent the order in which senators act in the first round.
While both queues are non-empty, compare the front elements. The senator with the smaller index acts first and bans the senator at the front of the opposing queue. The winner is then re-inserted into their own queue at index+n, placing them at the end of the circular order. The loser is discarded — popped from their queue and not re-inserted.
Repeat until one queue is empty. The party with the non-empty queue wins and is returned as the result. This approach naturally handles all rounds of the simulation in a single while loop without needing to track round boundaries explicitly.
- 1Initialize radiant queue with indices of all R senators
- 2Initialize dire queue with indices of all D senators
- 3While both queues non-empty: compare front indices r and d
- 4If r < d: Radiant wins this duel — pop both fronts, push r+n to radiant queue
- 5If d < r: Dire wins this duel — pop both fronts, push d+n to dire queue
- 6When one queue is empty, return the party with the remaining queue
Why Index + n for Re-entry
After winning a duel, the victorious senator participates in the next round of voting. In the circular model, after all n original senators have acted, the survivors repeat in the same relative order. Adding n to the winner's index places them at the correct position in the extended circular sequence — after all current-round senators but in the same relative order among survivors.
This +n trick converts a circular problem into a linear one. Instead of maintaining a circular buffer or resetting indices each round, we simply let indices grow unboundedly. Senators in round 1 have indices 0 to n-1, survivors in round 2 have indices n to 2n-1, and so on. The comparison logic remains the same: smaller index always acts first.
The correctness of this approach follows from the fact that two senators A and B with original indices i < j will always satisfy A's round-2 index (i+n) < B's round-2 index (j+n). The relative ordering is perfectly preserved across rounds, so the greedy comparison by index correctly captures the circular acting order.
The +n Trick Linearizes Circularity
The +n trick converts a circular problem to a linear one: the winning senator re-enters at position index+n, which is always after all current-round senators. This means you never need to reset indices or manage round boundaries — the queue comparison naturally handles all rounds. Senators in round k have indices in the range [(k-1)*n, k*n-1], and smaller index always acts first regardless of which round.
Walk-Through Example
Consider "RDD": n=3. Initialize radiant=[0], dire=[1,2]. Round 1, step 1: compare front indices 0 (R) vs 1 (D). Since 0 < 1, Radiant senator 0 wins. Pop both fronts. Re-insert Radiant at 0+3=3. State: radiant=[3], dire=[2].
Round 1, step 2: compare front indices 3 (R) vs 2 (D). Since 2 < 3, Dire senator 2 wins. Pop both fronts. Re-insert Dire at 2+3=5. State: radiant=[], dire=[5]. Radiant queue is now empty — Dire wins! Return "Dire".
Now consider "RD": n=2. radiant=[0], dire=[1]. Compare 0 vs 1: Radiant wins, re-enters at 2. State: radiant=[2], dire=[]. Dire queue empty — return "Radiant". And "RRDDD": radiant=[0,1], dire=[2,3,4]. Step 1: 0<2, R wins, radiant=[1,3], dire=[3,4]. Step 2: 1<3, R wins, radiant=[3,4], dire=[4]. Step 3: 3<4, R wins, radiant=[4,6], dire=[]. Return "Radiant".
- 1"RDD": n=3, radiant=[0], dire=[1,2]
- 2Step 1: 0 < 1, Radiant wins — radiant=[3], dire=[2]
- 3Step 2: 2 < 3, Dire wins — radiant=[], dire=[5]
- 4Radiant queue empty — return "Dire"
- 5"RRDDD": radiant=[0,1], dire=[2,3,4]
- 6Steps proceed until dire=[] — return "Radiant"
Code Walkthrough: Python and Java
Python solution using collections.deque: from collections import deque; def predictPartyVictory(senate: str) -> str: n = len(senate); r = deque(); d = deque(); [r.append(i) if c=="R" else d.append(i) for i, c in enumerate(senate)]; while r and d: ri, di = r.popleft(), d.popleft(); (r if ri < di else d).append((ri if ri < di else di) + n); return "Radiant" if r else "Dire". Time complexity O(n), space complexity O(n) for the two queues.
Java solution: public String predictPartyVictory(String senate) { int n = senate.length(); Queue<Integer> r = new LinkedList<>(), d = new LinkedList<>(); for (int i = 0; i < n; i++) { if (senate.charAt(i) == "R") r.add(i); else d.add(i); } while (!r.isEmpty() && !d.isEmpty()) { int ri = r.poll(), di = d.poll(); if (ri < di) r.add(ri + n); else d.add(di + n); } return r.isEmpty() ? "Dire" : "Radiant"; }. Uses LinkedList as Queue, O(n) time O(n) space.
Both solutions are clean and efficient. The while loop runs at most O(n) total iterations because each iteration eliminates exactly one senator permanently. The total number of re-insertions is bounded by the number of senators in the minority party — once that party is gone, the loop ends. No sorting, no string rebuilding, no inner loops.
Do Not Simulate In-Place on the String
Do not simulate by modifying the string or list in-place each round. Removing or marking characters per round is O(n) per round, and there can be O(n) rounds, giving O(n^2) total — too slow for n=10,000. The two-queue approach processes each senator at most once per round they survive, and the total work across all rounds is O(n) because each senator is eventually eliminated exactly once.