Facebook Pixel

1510. Stone Game IV

Problem Description

Alice and Bob are playing a turn-based game with a pile of n stones. Alice goes first.

On each turn, a player must remove a square number of stones from the pile. A square number is any number that can be expressed as where k is a positive integer (examples: 1, 4, 9, 16, 25, ...). The player can choose any valid square number that doesn't exceed the current number of stones in the pile.

The game continues with players alternating turns until one player cannot make a valid move (when there are 0 stones left). The player who cannot make a move loses the game.

Given a positive integer n representing the initial number of stones, you need to determine if Alice will win the game, assuming both players play optimally (making the best possible moves). Return true if Alice wins, false if Bob wins.

Example scenarios:

  • If n = 1: Alice can remove 1 stone (1 = 1²), leaving 0 stones for Bob. Bob cannot move, so Alice wins.
  • If n = 2: Alice can only remove 1 stone, leaving 1 stone for Bob. Bob removes 1 stone, leaving 0 for Alice. Alice cannot move, so Bob wins.
  • If n = 4: Alice can remove all 4 stones (4 = 2²), leaving 0 for Bob. Bob cannot move, so Alice wins.

The solution uses dynamic programming with memoization. The dfs(i) function returns true if the current player can win starting with i stones. For each state with i stones, the function tries all possible square number moves (1, 4, 9, ... up to i) and checks if any move leads to a losing position for the opponent. If such a move exists, the current player can win.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is recognizing this as a game theory problem where we need to determine winning and losing positions. In optimal play, each player tries to force their opponent into a losing position.

Let's think about this problem from the ground up:

  • When there are 0 stones, the current player has already lost (they can't make a move)
  • For any other number of stones, the current player wins if they can make at least one move that puts their opponent in a losing position

This naturally leads to a recursive approach: to know if position i is winning, we need to check all possible moves from that position. If we can remove stones (where j² ≤ i), we reach position i - j². If any of these resulting positions is a losing position for our opponent, then our current position is a winning position.

Why does this work? Because in optimal play:

  • If the current player can force the opponent into even one losing position, they will choose that move and win
  • If all possible moves lead to winning positions for the opponent, then the current player is doomed to lose

The recursive structure becomes clear:

  • dfs(0) returns False (base case - no moves available, current player loses)
  • dfs(i) returns True if there exists any j where j² ≤ i and dfs(i - j²) returns False
  • Otherwise, dfs(i) returns False

The @cache decorator is crucial here because the same game states will be encountered multiple times through different paths. For example, reaching 5 stones could happen from starting with 6 (removing 1) or starting with 9 (removing 4). Memoization ensures we only compute each state once, making the solution efficient.

Learn more about Math and Dynamic Programming patterns.

Solution Approach

The solution implements a top-down dynamic programming approach with memoization using Python's @cache decorator.

Implementation Details:

  1. Main Function Structure:

    • The winnerSquareGame function takes n (number of stones) as input
    • It defines an inner recursive function dfs(i) that determines if the current player can win with i stones remaining
    • Returns the result of dfs(n) to determine if Alice (the first player) wins
  2. Recursive Function dfs(i):

    @cache
    def dfs(i: int) -> bool:
    • The @cache decorator automatically memoizes results, preventing redundant calculations
    • Parameter i represents the current number of stones
    • Returns True if the current player can win from this position, False otherwise
  3. Base Case:

    if i == 0:
        return False
    • When there are 0 stones, the current player has no valid moves and loses
  4. Recursive Case - Try All Possible Moves:

    j = 1
    while j * j <= i:
        if not dfs(i - j * j):
            return True
        j += 1
    return False
    • Iterate through all possible square numbers from 1² to the largest square ≤ i
    • For each valid move removing stones, check the resulting position i - j²
    • If dfs(i - j²) returns False, it means the opponent will lose from that position, so the current player wins by making this move
    • If we find any such winning move, immediately return True
    • If all moves lead to winning positions for the opponent, return False

Time and Space Complexity:

  • Time Complexity: O(n√n) - For each of the n possible states, we check up to √n possible moves
  • Space Complexity: O(n) - The cache stores results for up to n different states, plus the recursion stack depth

The elegance of this solution lies in its simplicity: it directly models the game's win/lose conditions and leverages memoization to efficiently handle overlapping subproblems.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's trace through the algorithm with n = 5 stones to see how the solution determines the winner.

Starting with dfs(5):

  • We need to check if the current player (Alice) can win with 5 stones
  • Possible moves: remove 1 (1²) or 4 (2²) stones
  • Try removing 1: Check dfs(5 - 1) = dfs(4)
    • Computing dfs(4):
      • Possible moves: remove 1 or 4 stones
      • Try removing 1: Check dfs(4 - 1) = dfs(3)
        • Computing dfs(3):
          • Possible moves: remove 1 stone only
          • Try removing 1: Check dfs(3 - 1) = dfs(2)
            • Computing dfs(2):
              • Possible moves: remove 1 stone only
              • Try removing 1: Check dfs(2 - 1) = dfs(1)
                • Computing dfs(1):
                  • Possible moves: remove 1 stone only
                  • Try removing 1: Check dfs(1 - 1) = dfs(0)
                  • dfs(0) returns False (base case)
                  • Since opponent loses, dfs(1) returns True
              • dfs(1) returned True, so opponent wins
              • No winning move found, dfs(2) returns False
          • dfs(2) returned False, so opponent loses
          • Found a winning move! dfs(3) returns True
      • dfs(3) returned True, so opponent wins
      • Try removing 4: Check dfs(4 - 4) = dfs(0)
      • dfs(0) returns False (base case)
      • Found a winning move! dfs(4) returns True
    • dfs(4) returned True, so opponent (Bob) wins if we leave 4 stones
  • Try removing 4: Check dfs(5 - 4) = dfs(1)
    • We already computed dfs(1) = True (cached)
    • Opponent (Bob) wins if we leave 1 stone
  • Both moves lead to winning positions for Bob
  • No winning move found, dfs(5) returns False

Result: Alice loses with 5 stones, so Bob wins the game.

The key insight: Alice can only leave Bob with either 4 stones or 1 stone. In both cases, Bob has a winning strategy (removing all 4 stones from 4, or removing 1 stone from 1), so Alice is doomed to lose from position 5.

Solution Implementation

1class Solution:
2    def winnerSquareGame(self, n: int) -> bool:
3        """
4        Determine if Alice wins the square game with optimal play.
5      
6        Game rules: Players take turns removing a perfect square number of stones.
7        The player who removes the last stone wins.
8      
9        Args:
10            n: Number of stones initially in the pile
11          
12        Returns:
13            True if Alice (first player) wins, False otherwise
14        """
15        from functools import cache
16      
17        @cache
18        def can_win(remaining_stones: int) -> bool:
19            """
20            Check if the current player can win from this game state.
21          
22            Args:
23                remaining_stones: Number of stones left in the pile
24              
25            Returns:
26                True if current player can force a win, False otherwise
27            """
28            # Base case: no stones left means previous player took the last stone
29            if remaining_stones == 0:
30                return False
31          
32            # Try all possible moves (removing perfect square number of stones)
33            square_root = 1
34            while square_root * square_root <= remaining_stones:
35                stones_to_remove = square_root * square_root
36              
37                # If opponent cannot win after our move, then we can win
38                if not can_win(remaining_stones - stones_to_remove):
39                    return True
40                  
41                square_root += 1
42          
43            # No winning move found
44            return False
45      
46        # Alice starts first, check if she can win
47        return can_win(n)
48
1class Solution {
2    // Memoization array to store computed results for each state
3    private Boolean[] memo;
4
5    /**
6     * Determines if the current player can win the square game.
7     * In this game, players take turns removing square number of stones.
8     * The player who removes the last stone wins.
9     * 
10     * @param n The initial number of stones
11     * @return true if the current player can guarantee a win, false otherwise
12     */
13    public boolean winnerSquareGame(int n) {
14        // Initialize memoization array with size n+1 to handle states from 0 to n
15        memo = new Boolean[n + 1];
16      
17        // Start the recursive search from initial state n
18        return canWin(n);
19    }
20
21    /**
22     * Recursively determines if the current player can win from state i.
23     * Uses game theory: current player wins if there exists at least one move
24     * that leaves the opponent in a losing position.
25     * 
26     * @param remainingStones The number of stones remaining in current state
27     * @return true if current player can win from this state, false otherwise
28     */
29    private boolean canWin(int remainingStones) {
30        // Base case: no stones left means previous player took the last stone and won
31        if (remainingStones <= 0) {
32            return false;
33        }
34      
35        // Check if we've already computed the result for this state
36        if (memo[remainingStones] != null) {
37            return memo[remainingStones];
38        }
39      
40        // Try all possible square number moves
41        // j * j represents the square number of stones to remove
42        for (int j = 1; j * j <= remainingStones; j++) {
43            // If any move leads to opponent losing, current player wins
44            if (!canWin(remainingStones - j * j)) {
45                memo[remainingStones] = true;
46                return true;
47            }
48        }
49      
50        // If no winning move exists, current player loses
51        memo[remainingStones] = false;
52        return false;
53    }
54}
55
1class Solution {
2public:
3    bool winnerSquareGame(int n) {
4        // dp array to store game results: 0 = unvisited, 1 = win, -1 = lose
5        int dp[n + 1];
6        memset(dp, 0, sizeof(dp));
7      
8        // Recursive function with memoization to determine if current player wins
9        function<bool(int)> canWin = [&](int remainingStones) -> bool {
10            // Base case: no stones left, current player loses
11            if (remainingStones <= 0) {
12                return false;
13            }
14          
15            // Check if result is already computed
16            if (dp[remainingStones] != 0) {
17                return dp[remainingStones] == 1;
18            }
19          
20            // Try removing each possible perfect square number of stones
21            for (int k = 1; k * k <= remainingStones; ++k) {
22                // If opponent loses after our move, we win
23                if (!canWin(remainingStones - k * k)) {
24                    dp[remainingStones] = 1;  // Mark as winning position
25                    return true;
26                }
27            }
28          
29            // If no winning move found, current player loses
30            dp[remainingStones] = -1;  // Mark as losing position
31            return false;
32        };
33      
34        // Check if Alice (first player) can win with n stones
35        return canWin(n);
36    }
37};
38
1/**
2 * Determines if the first player can win the square subtraction game
3 * In this game, players take turns removing a perfect square number of stones
4 * The player who cannot make a move loses
5 * 
6 * @param n - The initial number of stones
7 * @returns true if the first player wins with optimal play, false otherwise
8 */
9function winnerSquareGame(n: number): boolean {
10    // Memoization array to cache results
11    // 0: not computed, 1: winning position, -1: losing position
12    const memo: number[] = new Array(n + 1).fill(0);
13  
14    /**
15     * Recursive helper function to determine if current player wins
16     * Uses dynamic programming with memoization
17     * 
18     * @param remainingStones - Number of stones left in the game
19     * @returns true if current player can win from this position
20     */
21    const canWin = (remainingStones: number): boolean => {
22        // Base case: no stones left means previous player took the last stone and won
23        if (remainingStones <= 0) {
24            return false;
25        }
26      
27        // Check if result is already computed
28        if (memo[remainingStones] !== 0) {
29            return memo[remainingStones] === 1;
30        }
31      
32        // Try all possible moves (removing perfect square number of stones)
33        for (let square = 1; square * square <= remainingStones; ++square) {
34            // If opponent loses after our move, we win
35            if (!canWin(remainingStones - square * square)) {
36                memo[remainingStones] = 1;
37                return true;
38            }
39        }
40      
41        // No winning move found, current player loses
42        memo[remainingStones] = -1;
43        return false;
44    };
45  
46    // Start the game with n stones
47    return canWin(n);
48}
49

Time and Space Complexity

Time Complexity: O(n * √n)

The analysis follows from the memoized recursive approach:

  • The dfs function can be called with at most n different states (from 0 to n).
  • Due to the @cache decorator, each state is computed only once.
  • For each state i, we iterate through all perfect squares less than or equal to i, which requires checking approximately √i values.
  • In the worst case, when i = n, we check up to √n perfect squares.
  • Therefore, the total time complexity is O(n) * O(√n) = O(n * √n).

Space Complexity: O(n)

The space complexity consists of:

  • Memoization cache: The @cache decorator stores up to n different states, requiring O(n) space.
  • Recursion call stack: In the worst case, the recursion depth can reach O(n) when we repeatedly subtract 1 (which is a perfect square), leading to a chain of calls like dfs(n) → dfs(n-1) → dfs(n-2) → ... → dfs(0).
  • Since both the cache and the call stack can use O(n) space, the overall space complexity is O(n).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Inefficient Square Number Generation

A common mistake is recalculating square numbers repeatedly or using inefficient methods to check if a number is a perfect square.

Pitfall Example:

# Inefficient: Checking all numbers up to n to find squares
for remove in range(1, i + 1):
    if int(remove ** 0.5) ** 2 == remove:  # Floating point issues!
        if not dfs(i - remove):
            return True

Problems:

  • Iterates through all numbers instead of just perfect squares
  • Using floating-point operations (** 0.5) can lead to precision errors
  • Time complexity becomes O(n²) instead of O(n√n)

Solution: Generate perfect squares directly by incrementing the base and squaring:

j = 1
while j * j <= i:
    if not dfs(i - j * j):
        return True
    j += 1

2. Missing or Incorrect Memoization

Without proper memoization, the solution will have exponential time complexity due to recalculating the same game states.

Pitfall Example:

def dfs(i):  # No memoization!
    if i == 0:
        return False
    j = 1
    while j * j <= i:
        if not dfs(i - j * j):
            return True
        j += 1
    return False

Problems:

  • Same game states are computed multiple times
  • Time complexity becomes exponential
  • Will timeout for larger values of n

Solution: Use @cache decorator or implement manual memoization:

from functools import cache

@cache
def dfs(i):
    # ... rest of the code

Or with manual memoization:

memo = {}
def dfs(i):
    if i in memo:
        return memo[i]
    # ... compute result
    memo[i] = result
    return result

3. Incorrect Base Case Logic

Misunderstanding when the game ends and who wins.

Pitfall Example:

def dfs(i):
    if i == 0:
        return True  # Wrong! Current player already lost

Problems:

  • When i = 0, the current player has no moves available
  • The previous player took the last stone and won
  • Returning True would incorrectly indicate the current player wins

Solution: Return False when no stones remain, as the current player cannot move:

if i == 0:
    return False  # Current player loses (cannot make a move)

4. Off-by-One Errors in Square Calculation

Incorrectly handling the boundary conditions for valid square numbers.

Pitfall Example:

j = 0  # Starting from 0 instead of 1
while j * j <= i:
    if not dfs(i - j * j):  # When j=0, removes 0 stones!
        return True
    j += 1

Problems:

  • Starting j from 0 means considering removing 0 stones as a valid move
  • This creates an infinite loop or incorrect game logic

Solution: Always start from j = 1 (smallest perfect square):

j = 1  # Start from 1² = 1
while j * j <= i:
    # ... rest of the code
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which algorithm should you use to find a node that is close to the root of the tree?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More