649. Dota2 Senate - Explanation

Problem Link

Description

In the world of Dota2, there are two parties: the Radiant and the Dire.

The Dota2 senate consists of senators coming from two parties. Now the Senate wants to decide on a change in the Dota2 game. The voting for this change is a round-based procedure. In each round, each senator can exercise one of the two rights:

  • Ban one senator's right: A senator can make another senator lose all his rights in this and all the following rounds.

  • Announce the victory: If this senator found the senators who still have rights to vote are all from the same party, he can announce the victory and decide on the change in the game.

You are given a string senate representing each senator's party belonging. The character 'R' and 'D' represent the Radiant party and the Dire party. Then if there are n senators, the size of the given string will be n.

The round-based procedure starts from the first senator to the last senator in the given order. This procedure will last until the end of voting. All the senators who have lost their rights will be skipped during the procedure.

Suppose every senator is smart enough and will play the best strategy for his own party. Predict which party will finally announce the victory and change the Dota2 game. The output should be "Radiant" or "Dire".

Example 1:

Input: senate = "RRDDD"

Output: "Radiant"

Explanation:

  • The first 'R' takes the rights of the first 'D'.
  • THe second 'R' takes the rights of the second 'D'.
  • The next two 'D's have lost their rights.
  • The last 'D' takes the rights of the first 'R'.
  • The last remaining 'R' takes the rights of the last 'D'.
  • As only 'R' is left, he announces the victory.

Example 2:

Input: senate = "RDD"

Output: "Dire"

Constraints:

  • 1 <= senate.length <= 10,000
  • senate[i] is either 'R' or 'D'.


Topics

Company Tags

Please upgrade to NeetCode Pro to view company tags.



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Queues - The optimal solution uses two queues to track senator positions and simulate the voting process efficiently
  • Greedy Algorithms - Understanding when to make locally optimal choices (banning the nearest opponent) to achieve the global optimum
  • Simulation - The brute force approach involves directly simulating the circular voting process

1. Brute Force

Intuition

We can simulate the voting process directly. Each senator, when it's their turn, will ban the next opposing senator in the circular order. We keep iterating through the remaining senators until only one party remains. This approach mirrors the problem's rules exactly but requires repeatedly scanning the list to find and remove opponents.

Algorithm

  1. Convert the senate string to a list for easy modification.
  2. Iterate through the list repeatedly in rounds.
  3. For each senator at position i:
    • Check if the game is over (only one party left).
    • Find the next senator of the opposing party (wrapping around circularly).
    • Remove that opponent from the list.
    • Adjust the current index if the removed senator was before i.
  4. Return the winning party once one side is eliminated.
class Solution:
    def predictPartyVictory(self, senate: str) -> str:
        s = list(senate)
        while True:
            i = 0
            while i < len(s):
                if 'R' not in s:
                    return "Dire"
                if 'D' not in s:
                    return "Radiant"
                if s[i] == 'R':
                    j = (i + 1) % len(s)
                    while s[j] == 'R':
                        j = (j + 1) % len(s)
                    s.pop(j)
                    if j < i:
                        i -= 1
                else:
                    j = (i + 1) % len(s)
                    while s[j] == 'D':
                        j = (j + 1) % len(s)
                    s.pop(j)
                    if j < i:
                        i -= 1
                i += 1

Time & Space Complexity

  • Time complexity: O(n2)O(n ^ 2)
  • Space complexity: O(n)O(n)

2. Greedy (Two Queues)

Intuition

The key insight is that a senator should always ban the nearest opposing senator who would otherwise act before them. We use two queues to track the positions of Radiant and Dire senators. When comparing the front of both queues, the senator with the smaller index acts first and bans the other. The surviving senator then re-enters at the end of the queue with an updated index (adding n to represent the next round).

Algorithm

  1. Create two queues R and D to store indices of Radiant and Dire senators.
  2. Populate the queues by scanning through the senate string.
  3. While both queues are non-empty:
    • Compare the front indices from both queues.
    • The senator with the smaller index survives and gets re-added with index + n.
    • The other senator is banned (removed permanently).
  4. Return "Radiant" if the R queue is non-empty, otherwise "Dire".
class Solution:
    def predictPartyVictory(self, senate: str) -> str:
        D, R = deque(), deque()
        n = len(senate)

        for i, c in enumerate(senate):
            if c == 'R':
                R.append(i)
            else:
                D.append(i)

        while D and R:
            dTurn = D.popleft()
            rTurn = R.popleft()

            if rTurn < dTurn:
                R.append(rTurn + n)
            else:
                D.append(dTurn + n)

        return "Radiant" if R else "Dire"

Time & Space Complexity

  • Time complexity: O(n)O(n)
  • Space complexity: O(n)O(n)

3. Greedy

Intuition

We can track pending bans using a single counter. When we encounter a Radiant senator (R), they either get banned by a waiting Dire (if cnt < 0) or they ban a future Dire senator. Similarly, Dire senators either get banned or ban future Radiant senators. The twist is that when a senator is banned, their surviving opponent is appended to the end to act again in future rounds.

Algorithm

  1. Convert the string to a list and initialize a counter cnt = 0.
  2. Iterate through the list (which grows as we append survivors):
    • If the senator is R:
      • If cnt < 0, a Dire senator bans them; append D to the end.
      • Increment cnt (representing a pending Radiant action).
    • If the senator is D:
      • If cnt > 0, a Radiant senator bans them; append R to the end.
      • Decrement cnt (representing a pending Dire action).
  3. After processing, if cnt > 0, Radiant wins; otherwise, Dire wins.
class Solution:
    def predictPartyVictory(self, senate: str) -> str:
        senate = list(senate)
        cnt = i = 0

        while i < len(senate):
            c = senate[i]
            if c == 'R':
                if cnt < 0:
                    senate.append('D')
                cnt += 1
            else:
                if cnt > 0:
                    senate.append('R')
                cnt -= 1
            i += 1

        return "Radiant" if cnt > 0 else "Dire"

Time & Space Complexity

  • Time complexity: O(n)O(n)
  • Space complexity: O(n)O(n)

Common Pitfalls

Not Understanding the Circular Nature

The senate voting is circular, meaning after the last senator votes, it wraps back to the first. A banned senator in round 1 could have banned someone who would act later in the same round or in subsequent rounds.

# Wrong: Only considering senators ahead in the array
j = i + 1
while j < len(s) and s[j] == s[i]:
    j += 1

# Correct: Circular wrap-around
j = (i + 1) % len(s)
while s[j] == s[i]:
    j = (j + 1) % len(s)

Banning the Wrong Opponent

Each senator should optimally ban the nearest opposing senator who would otherwise act before them in the next round. Banning a distant opponent wastes the opportunity and may lead to suboptimal outcomes.

# Wrong strategy: Banning any random opponent
# Correct strategy: Ban the next opponent in circular order

Forgetting to Re-add Surviving Senators

In the queue-based approach, when a senator bans an opponent, the survivor must be re-added to the queue to vote again in future rounds. The index must be incremented by n to maintain proper ordering.

# Wrong: Not re-adding the survivor
if rTurn < dTurn:
    pass  # R survives but isn't re-added

# Correct: Re-add survivor with updated index
if rTurn < dTurn:
    R.append(rTurn + n)