Facebook Pixel

293. Flip Game 🔒

EasyString
Leetcode Link

Problem Description

You're playing a Flip Game where you start with a string containing only '+' and '-' characters. In this game, players take turns flipping two consecutive "++" characters into "--". The game continues until no more moves are possible, and the player who cannot make a move loses.

Your task is to find all possible states of the string after making one valid move from the current state. A valid move consists of finding two consecutive '+' characters and flipping them both to '-'.

For example:

  • If currentState = "+++++", you can flip positions (0,1), (1,2), (2,3), or (3,4), resulting in ["--+++", "+--++", "++--+", "+++--"]
  • If currentState = "---", no moves are possible, so return []

The solution works by:

  1. Converting the string to a list for easier manipulation
  2. Iterating through consecutive pairs of characters using pairwise()
  3. When finding two consecutive '+' characters:
    • Temporarily change them to '-'
    • Add the resulting string to the answer list
    • Change them back to '+' to continue checking other positions
  4. Returning all possible next states

The time complexity is O(n²) where n is the length of the string (iterating through pairs takes O(n) and creating each new string takes O(n)). The space complexity is O(n) for storing the character list.

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

Intuition

The key insight is that we need to find all possible outcomes after making exactly one move. Since a move involves flipping two consecutive "++" to "--", we need to identify all positions where this pattern exists.

Think of it like scanning through the string with a sliding window of size 2. At each position, we check if both characters in our window are '+'. If they are, that represents one valid move we can make.

The natural approach is to:

  1. Check every consecutive pair of characters
  2. When we find a "++" pattern, we know we can make a move there
  3. Generate the new state by flipping those specific characters
  4. Continue checking the rest of the string for other possible moves

Why do we need to flip back after recording each state? Because we want to find ALL possible moves from the original state, not from a modified state. If we didn't flip back, we'd miss other valid positions. For example, in "++++", after flipping position (0,1) to get "--++", if we didn't restore it back to "++++", we'd miss the opportunity to find the move at position (1,2).

The use of pairwise() is a clean way to iterate through consecutive pairs without manually managing indices like (i, i+1). Converting to a list makes the string mutable, allowing us to efficiently modify and restore characters without creating new strings each time we want to test a position.

This exhaustive search guarantees we find all possible next states, which is exactly what the problem asks for.

Solution Approach

The solution uses a traversal and simulation approach to generate all possible next states.

Step-by-step implementation:

  1. Convert string to list: We first convert the input string currentState to a list s because strings in Python are immutable. This allows us to modify characters in-place without creating new strings repeatedly.

    s = list(currentState)
  2. Initialize result list: Create an empty list ans to store all possible next states.

    ans = []
  3. Traverse consecutive pairs: Use Python's pairwise() function to iterate through consecutive pairs of characters. The enumerate() gives us the index i of the first character in each pair.

    for i, (a, b) in enumerate(pairwise(s)):
  4. Check for valid move: For each pair (a, b), check if both characters are '+'. This represents a valid position where we can make a move.

    if a == b == "+":
  5. Simulate the move: When we find a valid position:

    • Temporarily flip the two '+' characters to '-'
    • Create the new state string and add it to our result list
    • Restore the characters back to '+' for the next iteration
    s[i] = s[i + 1] = "-"      # Make the move
    ans.append("".join(s))     # Record the new state
    s[i] = s[i + 1] = "+"      # Restore for next iteration
  6. Return all states: After checking all consecutive pairs, return the list containing all possible next states.

    return ans

The algorithm efficiently explores all positions in a single pass through the string, making temporary modifications and restoring them to ensure we capture every possible move from the original state. The pattern of "modify-record-restore" ensures we don't miss any valid moves while keeping the implementation clean and straightforward.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's trace through the algorithm with currentState = "+++-++":

Initial Setup:

  • Convert to list: s = ['+', '+', '+', '-', '+', '+']
  • Initialize empty result: ans = []

Iteration Process:

Step 1: Check position (0,1): ('+', '+')

  • Both are '+', so this is a valid move
  • Flip: s = ['-', '-', '+', '-', '+', '+']
  • Add to result: ans = ["--+-++"]
  • Restore: s = ['+', '+', '+', '-', '+', '+']

Step 2: Check position (1,2): ('+', '+')

  • Both are '+', so this is a valid move
  • Flip: s = ['+', '-', '-', '-', '+', '+']
  • Add to result: ans = ["--+-++", "+---++"]
  • Restore: s = ['+', '+', '+', '-', '+', '+']

Step 3: Check position (2,3): ('+', '-')

  • Not both '+', skip this position

Step 4: Check position (3,4): ('-', '+')

  • Not both '+', skip this position

Step 5: Check position (4,5): ('+', '+')

  • Both are '+', so this is a valid move
  • Flip: s = ['+', '+', '+', '-', '-', '-']
  • Add to result: ans = ["--+-++", "+---++", "+++---"]
  • Restore: s = ['+', '+', '+', '-', '+', '+']

Final Result: ["--+-++", "+---++", "+++---"]

The algorithm found all three possible moves: flipping positions (0,1), (1,2), and (4,5). Notice how after each flip operation, we restore the original state before checking the next position. This ensures we're always making moves from the original "+++-++" state, not from a modified version.

Solution Implementation

1from typing import List
2
3class Solution:
4    def generatePossibleNextMoves(self, currentState: str) -> List[str]:
5        """
6        Generate all possible next moves by flipping consecutive "++" to "--".
7      
8        Args:
9            currentState: A string containing only '+' and '-' characters
10          
11        Returns:
12            List of all possible next states after one valid move
13        """
14        # Convert string to list for easier manipulation
15        state_list = list(currentState)
16        result = []
17      
18        # Iterate through consecutive pairs of characters
19        for i in range(len(state_list) - 1):
20            # Check if current pair is "++"
21            if state_list[i] == '+' and state_list[i + 1] == '+':
22                # Flip "++" to "--"
23                state_list[i] = '-'
24                state_list[i + 1] = '-'
25              
26                # Add the new state to result
27                result.append(''.join(state_list))
28              
29                # Restore original state for next iteration
30                state_list[i] = '+'
31                state_list[i + 1] = '+'
32      
33        return result
34
1class Solution {
2    /**
3     * Generates all possible next moves for the Flip Game.
4     * In each move, two consecutive '+' characters can be flipped to '--'.
5     * 
6     * @param currentState The current state of the game as a string of '+' and '-' characters
7     * @return A list of all possible next states after making one valid move
8     */
9    public List<String> generatePossibleNextMoves(String currentState) {
10        // Initialize result list to store all possible next states
11        List<String> possibleMoves = new ArrayList<>();
12      
13        // Convert string to character array for easier manipulation
14        char[] stateArray = currentState.toCharArray();
15      
16        // Iterate through the array to find consecutive '++' pairs
17        for (int i = 0; i < stateArray.length - 1; i++) {
18            // Check if current and next characters form a valid '++' pair
19            if (stateArray[i] == '+' && stateArray[i + 1] == '+') {
20                // Flip the consecutive '+' characters to '-'
21                stateArray[i] = '-';
22                stateArray[i + 1] = '-';
23              
24                // Add the new state to the result list
25                possibleMoves.add(new String(stateArray));
26              
27                // Restore the original state for next iteration
28                stateArray[i] = '+';
29                stateArray[i + 1] = '+';
30            }
31        }
32      
33        return possibleMoves;
34    }
35}
36
1class Solution {
2public:
3    vector<string> generatePossibleNextMoves(string s) {
4        // Vector to store all possible next move states
5        vector<string> result;
6      
7        // Iterate through the string, checking consecutive pairs
8        // Stop at size()-1 to avoid out of bounds when checking i+1
9        for (int i = 0; i < s.size() - 1; ++i) {
10            // Check if current and next characters form a valid flip pair "++"
11            if (s[i] == '+' && s[i + 1] == '+') {
12                // Flip the consecutive "++" to "--"
13                s[i] = '-';
14                s[i + 1] = '-';
15              
16                // Add the modified string to the result vector
17                result.emplace_back(s);
18              
19                // Restore the original state for next iteration
20                s[i] = '+';
21                s[i + 1] = '+';
22            }
23        }
24      
25        // Return all possible next moves
26        return result;
27    }
28};
29
1/**
2 * Generates all possible next moves in a Flip Game
3 * where consecutive "++" can be flipped to "--"
4 * 
5 * @param currentState - The current game state string containing '+' and '-' characters
6 * @returns An array of all possible next states after making one valid move
7 */
8function generatePossibleNextMoves(currentState: string): string[] {
9    // Convert the string to an array of characters for easier manipulation
10    const stateArray: string[] = currentState.split('');
11  
12    // Store all possible next moves
13    const possibleMoves: string[] = [];
14  
15    // Iterate through the string to find consecutive "++" pairs
16    for (let i = 0; i < stateArray.length - 1; i++) {
17        // Check if current and next characters form a "++" pair
18        if (stateArray[i] === '+' && stateArray[i + 1] === '+') {
19            // Flip the "++" pair to "--"
20            stateArray[i] = '-';
21            stateArray[i + 1] = '-';
22          
23            // Add the new state to possible moves
24            possibleMoves.push(stateArray.join(''));
25          
26            // Restore the original "++" pair for next iteration
27            stateArray[i] = '+';
28            stateArray[i + 1] = '+';
29        }
30    }
31  
32    return possibleMoves;
33}
34

Time and Space Complexity

Time Complexity: O(n^2), where n is the length of the string.

The analysis breaks down as follows:

  • The pairwise function iterates through the string once, creating n-1 pairs in O(n) time
  • For each valid pair of consecutive "++", we perform:
    • Two character assignments to change "++" to "--": O(1)
    • Join operation on the list to create a string: O(n)
    • Append to result list: O(1)
    • Two character assignments to restore "++": O(1)
  • In the worst case, all n-1 pairs could be "++", resulting in (n-1) * O(n) = O(n^2)

Space Complexity: O(n) (excluding the output array)

The space analysis:

  • Converting the input string to a list: O(n)
  • The pairwise iterator uses constant extra space: O(1)
  • Temporary string creation during join operation: O(n)
  • If we include the output array, it would be O(n^2) in the worst case (up to n-1 strings, each of length n)

The reference mentions O(1) space complexity as an alternative, which would apply if the implementation modified the input string directly without creating a copy, though this isn't possible with Python's immutable strings without the list conversion.

Common Pitfalls

1. Modifying the Original String Without Restoration

A common mistake is forgetting to restore the characters back to their original state after recording each move. This would cause subsequent iterations to work with a modified string rather than the original state.

Incorrect approach:

def generatePossibleNextMoves(self, currentState: str) -> List[str]:
    state_list = list(currentState)
    result = []
  
    for i in range(len(state_list) - 1):
        if state_list[i] == '+' and state_list[i + 1] == '+':
            state_list[i] = '-'
            state_list[i + 1] = '-'
            result.append(''.join(state_list))
            # MISSING: Restoration of original characters!
  
    return result

This would only find the first valid move and then work with an already modified string, missing other possible moves from the original state.

2. Creating Multiple String Copies Inefficiently

Another pitfall is creating a new copy of the string for each potential move, which is inefficient:

Inefficient approach:

def generatePossibleNextMoves(self, currentState: str) -> List[str]:
    result = []
  
    for i in range(len(currentState) - 1):
        if currentState[i:i+2] == '++':
            # Creating a new string copy for each move
            new_state = currentState[:i] + '--' + currentState[i+2:]
            result.append(new_state)
  
    return result

While this works correctly, it creates multiple substring operations and concatenations, which can be less efficient than the in-place modification approach.

3. Off-by-One Error in Loop Range

Using range(len(state_list)) instead of range(len(state_list) - 1) would cause an index out of bounds error:

Incorrect range:

for i in range(len(state_list)):  # Wrong! Goes up to last index
    if state_list[i] == '+' and state_list[i + 1] == '+':  # i+1 out of bounds!

4. Not Handling Edge Cases

Forgetting to handle edge cases like empty strings or single-character strings:

Solution with proper edge case handling:

def generatePossibleNextMoves(self, currentState: str) -> List[str]:
    # Handle edge cases
    if len(currentState) < 2:
        return []
  
    state_list = list(currentState)
    result = []
  
    for i in range(len(state_list) - 1):
        if state_list[i] == '+' and state_list[i + 1] == '+':
            state_list[i] = state_list[i + 1] = '-'
            result.append(''.join(state_list))
            state_list[i] = state_list[i + 1] = '+'
  
    return result
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which of the following is a good use case for backtracking?


Recommended Readings

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

Load More