题目来源:🔒LeetCode294:翻转游戏 II
问题抽象: 给定一个仅含 '+' 和 '-' 的字符串 s,要求判断 先手玩家 在两人轮流操作的翻转游戏中是否存在 必胜策略,需满足以下核心需求:
-
游戏规则定义:
- 玩家每次操作必须选择字符串中 一组连续的
"++"并翻转为"--"; - 玩家若 无法操作 则失败,对方获胜。
- 玩家每次操作必须选择字符串中 一组连续的
-
策略目标:
- 从初始字符串
s开始,先手玩家(当前操作方)是否可通过 最优决策 确保获胜(无论后手如何应对)。
- 从初始字符串
-
输入约束:
- 字符串长度
n ∈ [1, 300],且字符仅含'+'或'-'; - 需高效处理 连续长串
'+'的复杂场景(如"+++++++++")。
- 字符串长度
-
输出要求:
- 返回布尔值
True(先手必胜)或False(先手必败); - 示例:
s = "++++"时返回True(先手翻转中间得"+--+"后后手无法操作);s = "+"时返回False(先手无法操作)。
- 返回布尔值
输入:字符串 s(如 "++++")
输出:布尔值(表示先手必胜性)。
解题思路
方法1:记忆化递归(回溯 + 状态缓存)
- 核心思想:模拟所有可能的翻转操作。若存在一种翻转使得对手无法获胜,则当前玩家必胜。
- 递归逻辑:
- 遍历字符串,找到连续子串
"++"。 - 将其翻转成
"--",生成新字符串。 - 递归判断新字符串下对手是否能赢(
!canWin(new_s))。 - 若对手无法赢,则当前玩家必胜。
- 遍历字符串,找到连续子串
- 优化点:使用
Map缓存已计算字符串的结果,避免重复计算。 - 时间复杂度:最坏 O ( n ⋅ 2 n / 2 ) O(n \cdot 2^{n/2}) O(n⋅2n/2),实际因剪枝和缓存远低于此。
- 空间复杂度: O ( n ⋅ 2 n / 2 ) O(n \cdot 2^{n/2}) O(n⋅2n/2),用于状态缓存。
方法2:Sprague-Grundy 定理(组合博弈优化)
- 核心思想:将字符串拆分为独立连续
'+'段,计算每段的 SG 值(Sprague-Grundy 数)。全局 SG 值的异或和>0时,先手必胜。 - SG 值计算:
- 定义
sg[i]表示长度为i的连续'+'的 SG 值。 - 状态转移:翻转操作将长度为
i的段拆分为两个子段(长度分别为j和i-j-2),其 SG 值为sg[j] ^ sg[i-j-2]。 sg[i] = mex{ sg[j] ^ sg[i-j-2] },其中mex是不在集合中的最小非负整数。
- 定义
- 步骤:
- 预处理
sg数组(长度≤60)。 - 分割原字符串为多个连续
'+'段。 - 计算所有段 SG 值的异或和
ans。 - 若
ans > 0,返回true;否则false。
- 预处理
代码实现🔥点击下载源码
方法1:记忆化递归(Java)
class Solution {
private Map<String, Boolean> memo = new HashMap<>();
public boolean canWin(String currentState) {
if (memo.containsKey(currentState)) {
return memo.get(currentState);
}
int n = currentState.length();
for (int i = 0; i < n - 1; i++) {
// 找到连续 "++"
if (currentState.charAt(i) == '+' && currentState.charAt(i + 1) == '+') {
// 生成新字符串:翻转 "++" 为 "--"
String newState = currentState.substring(0, i) + "--" + currentState.substring(i + 2);
// 递归判断对手是否能赢
if (!canWin(newState)) {
memo.put(currentState, true); // 缓存当前状态必胜
return true;
}
}
}
memo.put(currentState, false); // 所有操作均无法获胜
return false;
}
}
方法2:Sprague-Grundy 定理(Java)
class Solution {
private int[] sg; // SG 值数组
public boolean canWin(String currentState) {
int n = currentState.length();
sg = new int[n + 1];
Arrays.fill(sg, -1);
sg[0] = 0; // 空串 SG=0
sg[1] = 0; // 单字符无法翻转,SG=0
int ans = 0; // 全局异或和
int i = 0;
while (i < n) {
int j = i;
// 分割连续 '+' 段
while (j < n && currentState.charAt(j) == '+') j++;
ans ^= win(j - i); // 计算当前段 SG 值并异或
i = j + 1; // 跳过 '-' 字符
}
return ans > 0; // SG 异或和 >0 则先手必胜
}
private int win(int len) {
if (sg[len] != -1) return sg[len];
boolean[] vis = new boolean[len + 1]; // 标记子状态 SG 值
// 枚举所有可能的翻转位置
for (int j = 0; j < len - 1; j++) {
// 翻转后拆分为两个独立子段 [0, j-1] 和 [j+2, len-1]
int stateSG = win(j) ^ win(len - j - 2);
if (stateSG <= len) vis[stateSG] = true;
}
// mex 计算:找最小的未出现非负整数
for (int k = 0; k <= len; k++) {
if (!vis[k]) {
sg[len] = k;
return k;
}
}
return 0;
}
}
代码说明
-
记忆化递归:
- 优势:逻辑直观,易于实现。
- 适用场景:字符串长度较小时(
n ≤ 30)。 - 注意点:使用
memo缓存避免重复计算是关键优化。
-
Sprague-Grundy 定理:
- 优势:时间复杂度
O
(
n
2
)
O(n^2)
O(n2) 显著优于记忆化递归,适用于
n ≤ 60的约束。 - 关键步骤:
- 预处理
sg数组:计算所有可能长度连续'+'的 SG 值。 - 分割原字符串:按
'-'分割为独立子段。 - 异或和判断:若所有子段 SG 值的异或和
>0,则先手必胜。
- 预处理
- 优势:时间复杂度
O
(
n
2
)
O(n^2)
O(n2) 显著优于记忆化递归,适用于

902

被折叠的 条评论
为什么被折叠?



