2029. Stone Game IX
Problem Description
Alice and Bob are playing a stone game. There are n
stones in a row, each with an associated value given in the array stones
, where stones[i]
represents the value of the i-th
stone.
The game proceeds as follows:
- Alice and Bob take turns removing stones, with Alice going first
- On each turn, a player can remove any one stone from the remaining stones
- A player loses if after their move, the sum of all removed stones becomes divisible by 3
- If all stones are removed and no one has lost yet, Bob wins automatically (even if it's Alice's turn)
Both players play optimally, meaning they always make the best possible move to try to win.
Return true
if Alice wins the game, and false
if Bob wins.
The key insight is that since we only care about divisibility by 3, we only need to track the remainder when each stone's value is divided by 3. The game becomes a strategic battle where players must carefully choose which stones to remove to avoid making the total sum divisible by 3.
For example, if the running sum of removed stones is currently 2 (mod 3), removing a stone with value 1 (mod 3) would make the total 0 (mod 3), causing that player to lose. The solution involves counting stones by their remainders (0, 1, or 2) and simulating the optimal play patterns to determine the winner.
Intuition
Since we only care about whether the sum is divisible by 3, we can simplify the problem by only considering each stone's value modulo 3. This means every stone falls into one of three categories: remainder 0, 1, or 2.
The crucial observation is that stones with remainder 0 don't affect whether the sum is divisible by 3 - they can be freely removed without changing the game state modulo 3. So these stones essentially just add extra turns to the game.
Now let's think about Alice's first move. She cannot pick a stone with remainder 0, as that would immediately make the sum divisible by 3 (starting from 0), causing her to lose. So Alice must start with either a stone of remainder 1 or remainder 2.
If Alice picks a stone with remainder 1, the running sum becomes 1 (mod 3). Now Bob needs to avoid making it 0 (mod 3), so:
- Bob cannot pick remainder 2 (as 1 + 2 = 0 mod 3)
- Bob can only pick remainder 1, making the sum 2 (mod 3)
- Then Alice must pick remainder 2 (to avoid 2 + 1 = 0 mod 3), returning to sum 1 (mod 3)
- This creates a pattern: 1, 1, 2, 1, 2, 1, 2...
Similarly, if Alice starts with remainder 2, the pattern becomes: 2, 2, 1, 2, 1, 2, 1...
The key insight is that after the initial moves, the game follows a predictable pattern where players alternate between picking stones of remainder 1 and 2. The winner depends on:
- Whether there are enough stones to sustain the required pattern
- Whether the total number of moves (including remainder 0 stones) is odd or even
Since both players play optimally, Alice will choose the starting move (remainder 1 or 2) that gives her the best chance to win. The solution checks both possibilities and returns true if either leads to Alice's victory.
Solution Approach
The solution implements the game simulation by counting stones based on their remainder when divided by 3, then checking both possible starting moves for Alice.
First, we count the stones by their remainders:
cnt[0]
= count of stones with value % 3 == 0cnt[1]
= count of stones with value % 3 == 1cnt[2]
= count of stones with value % 3 == 2
The check
function simulates the game when Alice starts by picking a stone with remainder 1:
-
Initial Check: If
cnt[1] == 0
, Alice cannot start with remainder 1, so returnFalse
. -
Alice's First Move: Remove one stone with remainder 1 (
cnt[1] -= 1
), starting the game sequence. -
Calculate Total Moves:
- Start with 1 move (Alice's first move)
- Add
min(cnt[1], cnt[2]) * 2
moves for the alternating pattern where players pick 1, 2, 1, 2... - Add
cnt[0]
moves (stones with remainder 0 can be picked at any time) - This gives us
r = 1 + min(cnt[1], cnt[2]) * 2 + cnt[0]
-
Handle Extra Stones: If
cnt[1] > cnt[2]
, there's one extra stone with remainder 1 that can be picked:- Decrement
cnt[1]
and incrementr
by 1
- Decrement
-
Determine Winner: Alice wins if:
- The total number of moves
r
is odd (Alice makes the last valid move) - AND
cnt[1] != cnt[2]
(ensuring the game doesn't end with all stones removed, which would make Bob win)
- The total number of moves
The main function creates two scenarios:
c1 = [cnt[0], cnt[1], cnt[2]]
: Check if Alice can win by starting with remainder 1c2 = [cnt[0], cnt[2], cnt[1]]
: Check if Alice can win by starting with remainder 2 (by swapping cnt[1] and cnt[2])
Alice wins if either starting strategy leads to victory: return check(c1) or check(c2)
The key insight is that after the initial moves establish the pattern, the game becomes deterministic. By counting moves and checking parity, we can determine the winner without actually simulating every possible game state.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through an example with stones = [5, 1, 2, 4, 3]
.
Step 1: Convert to remainders modulo 3
- 5 % 3 = 2
- 1 % 3 = 1
- 2 % 3 = 2
- 4 % 3 = 1
- 3 % 3 = 0
Step 2: Count stones by remainder
cnt[0] = 1
(one stone with remainder 0)cnt[1] = 2
(two stones with remainder 1)cnt[2] = 2
(two stones with remainder 2)
Step 3: Try Strategy 1 - Alice starts with remainder 1
Check with c1 = [1, 2, 2]
:
- Alice picks remainder 1:
cnt[1] = 1
, running sum = 1 (mod 3) - Calculate moves:
r = 1 + min(1, 2) * 2 + 1 = 1 + 2 + 1 = 4
- 1 for Alice's first move
- min(1, 2) * 2 = 2 for the alternating pattern (Bob picks 1, Alice picks 2)
- 1 for the remainder 0 stone
- Since
cnt[1] = 1 < cnt[2] = 2
, there's an extra remainder 2 stone- But we can't pick it (would make sum 0 mod 3)
- Total moves
r = 4
(even), andcnt[1] ≠ cnt[2]
- Alice doesn't win with this strategy
Step 4: Try Strategy 2 - Alice starts with remainder 2
Check with c2 = [1, 2, 2]
(swapping cnt[1] and cnt[2] for the check):
- Alice picks remainder 2:
cnt[2] = 1
, running sum = 2 (mod 3) - Calculate moves:
r = 1 + min(2, 1) * 2 + 1 = 1 + 2 + 1 = 4
- Following same logic as above
- Since
cnt[1] = 2 > cnt[2] = 1
, there's an extra remainder 1 stone- Increment
r
to 5 and decrementcnt[1]
to 1
- Increment
- Total moves
r = 5
(odd), andcnt[1] = cnt[2] = 1
- Since they're equal, Alice doesn't win
Result: Neither strategy works, so Bob wins (return false
).
The game would play out: Alice must start with either remainder 1 or 2. After the initial moves, players alternate picking stones to avoid making the sum divisible by 3. The remainder 0 stone can be picked safely at any point. With 5 total stones and the constraint patterns, Bob makes the final safe move, forcing Alice into a losing position.
Solution Implementation
1class Solution:
2 def stoneGameIX(self, stones: List[int]) -> bool:
3 def can_alice_win(count: List[int]) -> bool:
4 """
5 Check if Alice can win with a specific starting strategy.
6 Alice starts by picking a stone with value % 3 == 1.
7
8 Args:
9 count: List where count[i] represents stones with value % 3 == i
10
11 Returns:
12 True if Alice can win with this strategy
13 """
14 # If no stones with remainder 1, Alice cannot start with this strategy
15 if count[1] == 0:
16 return False
17
18 # Alice picks a stone with remainder 1 first
19 count[1] -= 1
20
21 # Calculate total moves in the game
22 # Start with 1 (Alice's first move)
23 # Add pairs of remainder 1 and 2 stones (each pair contributes 2 moves)
24 # Add all remainder 0 stones (they don't change the sum mod 3)
25 total_moves = 1 + min(count[1], count[2]) * 2 + count[0]
26
27 # If there are extra remainder 1 stones after pairing
28 if count[1] > count[2]:
29 count[1] -= 1
30 total_moves += 1
31
32 # Alice wins if:
33 # 1. Total moves is odd (Bob runs out of valid moves)
34 # 2. The remaining stones don't leave Bob in a winning position
35 return total_moves % 2 == 1 and count[1] != count[2]
36
37 # Count stones by their remainder when divided by 3
38 remainder_count = [0] * 3
39 for stone in stones:
40 remainder_count[stone % 3] += 1
41
42 # Try two strategies for Alice:
43 # Strategy 1: Start with a stone where value % 3 == 1
44 strategy_one = [remainder_count[0], remainder_count[1], remainder_count[2]]
45
46 # Strategy 2: Start with a stone where value % 3 == 2
47 strategy_two = [remainder_count[0], remainder_count[2], remainder_count[1]]
48
49 # Alice wins if either strategy works
50 return can_alice_win(strategy_one) or can_alice_win(strategy_two)
51
1class Solution {
2 public boolean stoneGameIX(int[] stones) {
3 // Count stones by their remainder when divided by 3
4 int[] countByRemainder = new int[3];
5 for (int stone : stones) {
6 countByRemainder[stone % 3]++;
7 }
8
9 // Try two scenarios: Alice starts with remainder 1 or remainder 2
10 // Scenario 1: Start with remainder 1 (original count distribution)
11 int[] scenario1 = {countByRemainder[0], countByRemainder[1], countByRemainder[2]};
12
13 // Scenario 2: Start with remainder 2 (swap counts of remainder 1 and 2)
14 int[] scenario2 = {countByRemainder[0], countByRemainder[2], countByRemainder[1]};
15
16 // Alice wins if she can win in either scenario
17 return check(scenario1) || check(scenario2);
18 }
19
20 private boolean check(int[] remainderCounts) {
21 // Alice must pick a stone with remainder 1 to start this scenario
22 // If no such stone exists, this scenario is invalid
23 remainderCounts[1]--;
24 if (remainderCounts[1] < 0) {
25 return false;
26 }
27
28 // Calculate total turns in the game
29 // Start with 1 (Alice's first move)
30 // Add pairs of remainder 1 and 2 stones (each pair adds 2 turns)
31 // Add all remainder 0 stones (they don't affect the sum modulo 3)
32 int totalTurns = 1 + Math.min(remainderCounts[1], remainderCounts[2]) * 2 + remainderCounts[0];
33
34 // If there are more remainder 1 stones than remainder 2 stones,
35 // one more remainder 1 stone can be played
36 if (remainderCounts[1] > remainderCounts[2]) {
37 remainderCounts[1]--;
38 totalTurns++;
39 }
40
41 // Alice wins if:
42 // 1. Total turns is odd (Bob runs out of valid moves)
43 // 2. The remaining counts are unequal (prevents Bob from winning by exhausting stones)
44 return totalTurns % 2 == 1 && remainderCounts[1] != remainderCounts[2];
45 }
46}
47
1class Solution {
2public:
3 bool stoneGameIX(vector<int>& stones) {
4 // Count stones by their remainder when divided by 3
5 // count[0]: stones with remainder 0
6 // count[1]: stones with remainder 1
7 // count[2]: stones with remainder 2
8 vector<int> count(3, 0);
9 for (int stone : stones) {
10 count[stone % 3]++;
11 }
12
13 // Create a swapped version where remainder 1 and 2 counts are swapped
14 // This simulates Alice starting with a stone of remainder 2 instead of 1
15 vector<int> countSwapped = {count[0], count[2], count[1]};
16
17 // Lambda function to check if Alice can win with a given stone configuration
18 // The strategy assumes Alice starts by picking a stone with remainder 1
19 auto checkAliceWins = [](vector<int> stoneCount) -> bool {
20 // Alice must start with a stone of remainder 1
21 stoneCount[1]--;
22 if (stoneCount[1] < 0) {
23 return false; // No stone with remainder 1 to start with
24 }
25
26 // Calculate total moves in optimal play
27 // Start with 1 (Alice's first move)
28 // Add pairs of (remainder 1, remainder 2) stones that maintain sum % 3 != 0
29 // Add all remainder 0 stones (they don't change sum % 3)
30 int totalMoves = 1 + min(stoneCount[1], stoneCount[2]) * 2 + stoneCount[0];
31
32 // If there are extra remainder 1 stones after pairing
33 if (stoneCount[1] > stoneCount[2]) {
34 stoneCount[1]--;
35 totalMoves++;
36 }
37
38 // Alice wins if:
39 // 1. Total moves is odd (Bob runs out of valid moves)
40 // 2. The remaining stones don't force a draw (equal remainder 1 and 2)
41 return (totalMoves % 2 == 1) && (stoneCount[1] != stoneCount[2]);
42 };
43
44 // Alice wins if she can win starting with remainder 1 OR remainder 2
45 return checkAliceWins(count) || checkAliceWins(countSwapped);
46 }
47};
48
1/**
2 * Determines if Alice can win the Stone Game IX
3 * @param stones - Array of stone values
4 * @returns true if Alice wins, false otherwise
5 */
6function stoneGameIX(stones: number[]): boolean {
7 // Count stones by their modulo 3 values
8 const countByModThree: number[] = Array(3).fill(0);
9 for (const stone of stones) {
10 countByModThree[stone % 3]++;
11 }
12
13 // Create alternative count array with swapped positions for mod 1 and mod 2
14 const alternativeCount: number[] = [
15 countByModThree[0],
16 countByModThree[2],
17 countByModThree[1]
18 ];
19
20 /**
21 * Checks if Alice can win with given stone count configuration
22 * @param stoneCount - Array containing counts of stones with mod values [0, 1, 2]
23 * @returns true if Alice wins with this configuration
24 */
25 const checkWinCondition = (stoneCount: number[]): boolean => {
26 // Alice must start with a stone of mod 1
27 stoneCount[1]--;
28 if (stoneCount[1] < 0) {
29 return false;
30 }
31
32 // Calculate total rounds played
33 // Start with 1 (Alice's first move) + pairs of mod 1 and mod 2 + remaining mod 0 stones
34 let totalRounds: number = 1 + Math.min(stoneCount[1], stoneCount[2]) * 2 + stoneCount[0];
35
36 // If there are more mod 1 stones than mod 2, Alice takes one more
37 if (stoneCount[1] > stoneCount[2]) {
38 stoneCount[1]--;
39 totalRounds++;
40 }
41
42 // Alice wins if total rounds is odd and there's an imbalance between mod 1 and mod 2
43 return totalRounds % 2 === 1 && stoneCount[1] !== stoneCount[2];
44 };
45
46 // Check both starting strategies: starting with mod 1 or effectively starting with mod 2
47 return checkWinCondition([...countByModThree]) || checkWinCondition([...alternativeCount]);
48}
49
Time and Space Complexity
The time complexity of this solution is O(n)
, where n
is the length of the array stones
. This is because the algorithm iterates through the entire stones
array once to count the frequency of remainders when divided by 3. The loop for x in stones: c1[x % 3] += 1
processes each element exactly once, performing a constant-time modulo operation and increment for each stone.
The space complexity is O(1)
. The algorithm uses two fixed-size arrays c1
and c2
, each containing exactly 3 elements to store the count of stones with remainders 0, 1, and 2 when divided by 3. Since the size of these arrays is constant and independent of the input size n
, the space complexity remains constant. The check
function also only uses a constant amount of additional space for its local variables (r
and modifications to the cnt
array passed by reference).
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Misunderstanding the Losing Condition
A critical pitfall is misinterpreting when a player loses. Players lose when after their move the sum becomes divisible by 3, not before. This means if the current sum modulo 3 is 2, and you pick a stone with value 1 (mod 3), you lose immediately because the new sum is 0 (mod 3).
Wrong approach:
# Incorrectly checking if current sum is divisible by 3 if current_sum % 3 == 0: current_player_loses = True
Correct approach:
# Check if the move would make the sum divisible by 3 if (current_sum + stone_value) % 3 == 0: # This move would cause the player to lose
2. Incorrectly Handling the End Game Condition
Many solutions forget that if all stones are removed without anyone losing, Bob wins automatically. This is why the condition count[1] != count[2]
is crucial in the check function.
Wrong approach:
def can_alice_win(count):
# ... calculate total_moves ...
return total_moves % 2 == 1 # Missing the end game check
Correct approach:
def can_alice_win(count):
# ... calculate total_moves ...
# Must check both parity AND that stones don't run out evenly
return total_moves % 2 == 1 and count[1] != count[2]
3. Not Considering Both Starting Strategies
Alice has two viable starting strategies (picking remainder 1 or remainder 2), and the solution must check both. A common mistake is only checking one strategy.
Wrong approach:
# Only checking if Alice starts with remainder 1 return can_alice_win([cnt[0], cnt[1], cnt[2]])
Correct approach:
# Check both starting strategies strategy_one = [cnt[0], cnt[1], cnt[2]] # Start with remainder 1 strategy_two = [cnt[0], cnt[2], cnt[1]] # Start with remainder 2 return can_alice_win(strategy_one) or can_alice_win(strategy_two)
4. Modifying the Original Count Array
The check function modifies the count array in place. If you pass the same array to multiple checks without copying, you'll get incorrect results.
Wrong approach:
remainder_count = [0] * 3 for stone in stones: remainder_count[stone % 3] += 1 # This modifies remainder_count! result1 = can_alice_win(remainder_count) # remainder_count is now corrupted for the second check result2 = can_alice_win([remainder_count[0], remainder_count[2], remainder_count[1]])
Correct approach:
# Create separate copies for each strategy check strategy_one = [remainder_count[0], remainder_count[1], remainder_count[2]] strategy_two = [remainder_count[0], remainder_count[2], remainder_count[1]]
5. Edge Case: All Stones Have Same Remainder
When all stones have the same remainder modulo 3, special care is needed:
- If all stones have remainder 0: First player always loses
- If all stones have remainder 1 or 2: The game follows a specific pattern
Solution: The current implementation handles this correctly through the initial check if count[1] == 0: return False
and the symmetric logic for strategy_two.
What's the output of running the following function using input 56
?
1KEYBOARD = {
2 '2': 'abc',
3 '3': 'def',
4 '4': 'ghi',
5 '5': 'jkl',
6 '6': 'mno',
7 '7': 'pqrs',
8 '8': 'tuv',
9 '9': 'wxyz',
10}
11
12def letter_combinations_of_phone_number(digits):
13 def dfs(path, res):
14 if len(path) == len(digits):
15 res.append(''.join(path))
16 return
17
18 next_number = digits[len(path)]
19 for letter in KEYBOARD[next_number]:
20 path.append(letter)
21 dfs(path, res)
22 path.pop()
23
24 res = []
25 dfs([], res)
26 return res
27
1private static final Map<Character, char[]> KEYBOARD = Map.of(
2 '2', "abc".toCharArray(),
3 '3', "def".toCharArray(),
4 '4', "ghi".toCharArray(),
5 '5', "jkl".toCharArray(),
6 '6', "mno".toCharArray(),
7 '7', "pqrs".toCharArray(),
8 '8', "tuv".toCharArray(),
9 '9', "wxyz".toCharArray()
10);
11
12public static List<String> letterCombinationsOfPhoneNumber(String digits) {
13 List<String> res = new ArrayList<>();
14 dfs(new StringBuilder(), res, digits.toCharArray());
15 return res;
16}
17
18private static void dfs(StringBuilder path, List<String> res, char[] digits) {
19 if (path.length() == digits.length) {
20 res.add(path.toString());
21 return;
22 }
23 char next_digit = digits[path.length()];
24 for (char letter : KEYBOARD.get(next_digit)) {
25 path.append(letter);
26 dfs(path, res, digits);
27 path.deleteCharAt(path.length() - 1);
28 }
29}
30
1const KEYBOARD = {
2 '2': 'abc',
3 '3': 'def',
4 '4': 'ghi',
5 '5': 'jkl',
6 '6': 'mno',
7 '7': 'pqrs',
8 '8': 'tuv',
9 '9': 'wxyz',
10}
11
12function letter_combinations_of_phone_number(digits) {
13 let res = [];
14 dfs(digits, [], res);
15 return res;
16}
17
18function dfs(digits, path, res) {
19 if (path.length === digits.length) {
20 res.push(path.join(''));
21 return;
22 }
23 let next_number = digits.charAt(path.length);
24 for (let letter of KEYBOARD[next_number]) {
25 path.push(letter);
26 dfs(digits, path, res);
27 path.pop();
28 }
29}
30
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
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
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!