Files
codetutor/backend/data/questions/dota2-senate.yaml

220 lines
11 KiB
YAML

title: Dota2 Senate
slug: dota2-senate
difficulty: medium
leetcode_id: 649
leetcode_url: https://leetcode.com/problems/dota2-senate/
categories:
- strings
- queue
patterns:
- slug: greedy
is_optimal: true
function_signature: "def predict_party_victory(senate: str) -> str:"
test_cases:
visible:
- input: { senate: "RD" }
expected: "Radiant"
- input: { senate: "RDD" }
expected: "Dire"
hidden:
- input: { senate: "R" }
expected: "Radiant"
- input: { senate: "D" }
expected: "Dire"
- input: { senate: "RRDD" }
expected: "Radiant"
- input: { senate: "DRRD" }
expected: "Dire"
- input: { senate: "RRDDD" }
expected: "Dire"
- input: { senate: "DDRRRR" }
expected: "Radiant"
- input: { senate: "RDRDRD" }
expected: "Radiant"
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.
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"`.
constraints: |
- `n == senate.length`
- `1 <= n <= 10^4`
- `senate[i]` is either `'R'` or `'D'`
examples:
- input: 'senate = "RD"'
output: '"Radiant"'
explanation: "The first senator comes from Radiant and he can just ban the next senator's right in round 1. The second senator can't exercise any rights anymore since his right has been banned. In round 2, the first senator can announce the victory since he is the only one who can vote."
- input: 'senate = "RDD"'
output: '"Dire"'
explanation: "The first senator (Radiant) bans the second senator (Dire) in round 1. The third senator (Dire) bans the first senator (Radiant) in round 1. In round 2, only the third senator remains, so Dire wins."
explanation:
intuition: |
Imagine senators sitting in a circle, each waiting for their turn to act. When a senator's turn comes, their optimal strategy is always to **ban the next opponent** who would otherwise act before them in the next round.
Think of it like a game of elimination: each senator wants to silence the closest threat. Why the *next* opponent? Because banning someone who already acted this round doesn't help — they've already done their damage. But banning the next opponent in line prevents them from banning one of your allies.
The key insight is that this is a **circular process**. After reaching the end of the senate string, we wrap back to the beginning for the next round. Senators who survive keep participating until only one party remains.
We can simulate this efficiently using **two queues** — one for each party. Each queue stores the *indices* of active senators. When comparing the front of both queues, the senator with the smaller index acts first and bans the other. The winner then re-enters the queue with an updated index (adding `n` to simulate joining the next round).
approach: |
We solve this using a **Two Queue Simulation**:
**Step 1: Initialise two queues**
- `radiant`: Queue storing indices of all Radiant senators
- `dire`: Queue storing indices of all Dire senators
- Iterate through the senate string and add each senator's index to the appropriate queue
&nbsp;
**Step 2: Simulate the voting rounds**
- While both queues are non-empty, compare the front elements
- The senator with the **smaller index** acts first and bans the opponent
- The winning senator re-enters their queue with index `+ n` (to represent appearing in the next round)
- Remove both front elements and continue
&nbsp;
**Step 3: Determine the winner**
- When one queue becomes empty, the other party wins
- Return `"Radiant"` if the radiant queue is non-empty, otherwise `"Dire"`
&nbsp;
This simulation correctly handles the circular nature of rounds. By adding `n` to the winner's index, we ensure they appear *after* all senators from the current round, preserving relative order for the next round.
common_pitfalls:
- title: Ignoring the Circular Nature
description: |
A common mistake is treating the senate as a single pass. After the first round ends, surviving senators continue voting in subsequent rounds.
For example, with `senate = "DRRD"`, after round 1 we might have some senators banned, but the survivors wrap around for round 2. Using a simple linear scan misses this behaviour.
wrong_approach: "Single pass through the string"
correct_approach: "Simulate multiple rounds using queues"
- title: Banning the Wrong Opponent
description: |
The optimal strategy is to ban the **next opponent in order**, not just any opponent. Banning a senator who already acted this round wastes your move — they've already used their power.
The queue approach naturally handles this: we always compare indices and the smaller one acts first, banning their immediate threat.
wrong_approach: "Ban any random opponent"
correct_approach: "Ban the next opponent who would act before you in the next round"
- title: Not Tracking Bans Correctly
description: |
If you try to simulate with a single pass and mark banned senators, you need to handle the case where a senator is banned *before* their turn comes up in the same round.
With `senate = "RD"`, R bans D immediately. D never gets to act. The queue approach handles this cleanly — D is removed from the queue before being processed.
key_takeaways:
- "**Greedy optimal play**: Each senator's best move is banning the next opponent in turn order"
- "**Two-queue simulation**: Use queues to track active senators and their positions, enabling efficient circular simulation"
- "**Index manipulation for rounds**: Adding `n` to a winner's index elegantly models the circular round structure"
- "**Pattern recognition**: This problem combines queue simulation with greedy decision-making — a common pairing in competitive programming"
time_complexity: "O(n). Each senator is processed at most twice — once when initially added to a queue, and once when they either win or lose a confrontation."
space_complexity: "O(n). We store all senator indices across the two queues."
solutions:
- approach_name: Two Queue Simulation
is_optimal: true
code: |
from collections import deque
def predict_party_victory(senate: str) -> str:
n = len(senate)
# Queues store indices of active senators
radiant = deque()
dire = deque()
# Populate queues with initial positions
for i, s in enumerate(senate):
if s == 'R':
radiant.append(i)
else:
dire.append(i)
# Simulate until one party is eliminated
while radiant and dire:
r_idx = radiant.popleft()
d_idx = dire.popleft()
# Senator with smaller index acts first and bans the other
if r_idx < d_idx:
# Radiant wins this round, re-enters for next round
radiant.append(r_idx + n)
else:
# Dire wins this round, re-enters for next round
dire.append(d_idx + n)
# Whichever queue still has senators wins
return "Radiant" if radiant else "Dire"
explanation: |
**Time Complexity:** O(n) — Each senator participates in at most one confrontation.
**Space Complexity:** O(n) — Two queues storing all senator indices.
We use two queues to track the positions of active senators. In each iteration, we compare the front of both queues — the smaller index acts first and eliminates the opponent. The winner re-enters with an offset of `n`, ensuring correct ordering in subsequent rounds.
- approach_name: Greedy with Ban Counter
is_optimal: false
code: |
def predict_party_victory(senate: str) -> str:
senate = list(senate)
# Pending bans for each party
r_bans = 0
d_bans = 0
# Keep simulating until one party wins
while True:
new_senate = []
for s in senate:
if s == 'R':
if d_bans > 0:
# This Radiant senator is banned
d_bans -= 1
else:
# Radiant senator acts, queues a ban for Dire
r_bans += 1
new_senate.append('R')
else:
if r_bans > 0:
# This Dire senator is banned
r_bans -= 1
else:
# Dire senator acts, queues a ban for Radiant
d_bans += 1
new_senate.append('D')
# Check if only one party remains
if not any(s == 'D' for s in new_senate):
return "Radiant"
if not any(s == 'R' for s in new_senate):
return "Dire"
senate = new_senate
explanation: |
**Time Complexity:** O(n^2) in worst case — Multiple passes through the senate.
**Space Complexity:** O(n) — Storing the remaining senators each round.
This approach tracks pending bans for each party. When a senator acts, they queue a ban for the opposing party. If a senator's party has pending bans against them, they're eliminated instead of acting. This is less efficient than the two-queue solution but demonstrates the greedy logic more explicitly.