In the Flip Game II problem, you are given a string s
consisting only of the characters '+'
and '-'
. Two players take turns flipping any two consecutive '+'
characters into '--'
. The player who cannot make a move loses the game. Your task is to determine if the starting player (the one to move first) can guarantee a win, assuming both players play optimally.
'+'
characters at a time.'--'
cannot be flipped back.true
if the starting player can guarantee a win; otherwise, return false
.
At first glance, it seems we could try every possible move recursively for each player. For each move, we flip two consecutive '+'
to '--'
and let the other player play. If at any point the current player can force the opponent into a losing position, they can win.
However, this brute-force approach leads to exponential time complexity because the number of possible game states grows rapidly as the string gets longer. To optimize, we need to avoid recalculating the result for the same game state multiple times. This is a classic case for memoization.
The key insight is to represent each unique state of the string and remember if it's a winning or losing position. If the current player can make any move that leaves the opponent in a losing position, then the current position is a winning one.
Let's break down the optimized solution step by step:
'+'
in the string, flip them to '--'
and recursively check if the opponent loses in the resulting state.false
), then the current player can guarantee a win.false
(current player loses).
Let's consider the input s = "++++"
.
'+'
: Resulting string is "--++"
.'+'
: Resulting string is "+--+"
.'+'
: Resulting string is "++--"
."--++"
, the only valid move is flipping the last two '+'
to get "----"
(no more moves left)."+--+"
, the only valid move is flipping the first and last two '+'
(but in this case, no two consecutive '+'
exist, so no move is possible)."++--"
, the only valid move is flipping the first two '+'
to get "----"
.class Solution:
def canWin(self, s: str) -> bool:
memo = {}
def can_player_win(state):
if state in memo:
return memo[state]
for i in range(len(state) - 1):
if state[i] == state[i+1] == '+':
next_state = state[:i] + '--' + state[i+2:]
if not can_player_win(next_state):
memo[state] = True
return True
memo[state] = False
return False
return can_player_win(s)
class Solution {
public:
unordered_map<string, bool> memo;
bool canWin(string s) {
if (memo.count(s)) return memo[s];
for (int i = 0; i < s.size() - 1; ++i) {
if (s[i] == '+' && s[i+1] == '+') {
s[i] = s[i+1] = '-';
if (!canWin(s)) {
s[i] = s[i+1] = '+';
return memo[s] = true;
}
s[i] = s[i+1] = '+';
}
}
return memo[s] = false;
}
};
import java.util.HashMap;
public class Solution {
private HashMap<String, Boolean> memo = new HashMap<>();
public boolean canWin(String s) {
if (memo.containsKey(s)) return memo.get(s);
for (int i = 0; i < s.length() - 1; i++) {
if (s.charAt(i) == '+' && s.charAt(i + 1) == '+') {
String next = s.substring(0, i) + "--" + s.substring(i + 2);
if (!canWin(next)) {
memo.put(s, true);
return true;
}
}
}
memo.put(s, false);
return false;
}
}
var canWin = function(s) {
const memo = new Map();
function canPlayerWin(state) {
if (memo.has(state)) return memo.get(state);
for (let i = 0; i < state.length - 1; i++) {
if (state[i] === '+' && state[i+1] === '+') {
let next = state.slice(0, i) + '--' + state.slice(i + 2);
if (!canPlayerWin(next)) {
memo.set(state, true);
return true;
}
}
}
memo.set(state, false);
return false;
}
return canPlayerWin(s);
};
The optimization comes from memoization, which avoids redundant work and makes the solution feasible for strings of moderate length (e.g., up to 20).
The Flip Game II problem is a classic example of recursive game theory with memoization. By exploring all possible moves and caching results for each game state, we can efficiently determine if the starting player can force a win. The main insight is to recognize and reuse previously computed outcomes for the same board state, which dramatically reduces computation time compared to brute-force recursion. This approach elegantly balances recursion and dynamic programming for optimal performance.