【中等】力扣算法题解析LeetCode294:翻转游戏 II

关注文末推广名片,即可免费获得本题测试源码

题目来源:🔒LeetCode294:翻转游戏 II

问题抽象: 给定一个仅含 '+''-' 的字符串 s,要求判断 先手玩家 在两人轮流操作的翻转游戏中是否存在 必胜策略,需满足以下核心需求:

  1. 游戏规则定义

    • 玩家每次操作必须选择字符串中 一组连续的 "++" 并翻转为 "--"
    • 玩家若 无法操作 则失败,对方获胜。
  2. 策略目标

    • 从初始字符串 s 开始,先手玩家(当前操作方)是否可通过 最优决策 确保获胜(无论后手如何应对)。
  3. 输入约束

    • 字符串长度 n ∈ [1, 300],且字符仅含 '+''-'
    • 需高效处理 连续长串 '+' 的复杂场景(如 "+++++++++")。
  4. 输出要求

    • 返回布尔值 True(先手必胜)或 False(先手必败);
    • 示例:
      • s = "++++" 时返回 True(先手翻转中间得 "+--+" 后后手无法操作);
      • s = "+" 时返回 False(先手无法操作)。

输入:字符串 s(如 "++++"
输出:布尔值(表示先手必胜性)。


解题思路

方法1:记忆化递归(回溯 + 状态缓存)
  • 核心思想:模拟所有可能的翻转操作。若存在一种翻转使得对手无法获胜,则当前玩家必胜。
  • 递归逻辑
    1. 遍历字符串,找到连续子串 "++"
    2. 将其翻转成 "--",生成新字符串。
    3. 递归判断新字符串下对手是否能赢(!canWin(new_s))。
    4. 若对手无法赢,则当前玩家必胜。
  • 优化点:使用 Map 缓存已计算字符串的结果,避免重复计算。
  • 时间复杂度:最坏 O ( n ⋅ 2 n / 2 ) O(n \cdot 2^{n/2}) O(n2n/2),实际因剪枝和缓存远低于此。
  • 空间复杂度 O ( n ⋅ 2 n / 2 ) O(n \cdot 2^{n/2}) O(n2n/2),用于状态缓存。
方法2:Sprague-Grundy 定理(组合博弈优化)
  • 核心思想:将字符串拆分为独立连续 '+' 段,计算每段的 SG 值(Sprague-Grundy 数)。全局 SG 值的异或和 >0 时,先手必胜。
  • SG 值计算
    • 定义 sg[i] 表示长度为 i 的连续 '+' 的 SG 值。
    • 状态转移:翻转操作将长度为 i 的段拆分为两个子段(长度分别为 ji-j-2),其 SG 值为 sg[j] ^ sg[i-j-2]
    • sg[i] = mex{ sg[j] ^ sg[i-j-2] },其中 mex 是不在集合中的最小非负整数。
  • 步骤
    1. 预处理 sg 数组(长度 ≤60)。
    2. 分割原字符串为多个连续 '+' 段。
    3. 计算所有段 SG 值的异或和 ans
    4. 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;
    }
}

代码说明

  1. 记忆化递归

    • 优势:逻辑直观,易于实现。
    • 适用场景:字符串长度较小时(n ≤ 30)。
    • 注意点:使用 memo 缓存避免重复计算是关键优化。
  2. Sprague-Grundy 定理

    • 优势:时间复杂度 O ( n 2 ) O(n^2) O(n2) 显著优于记忆化递归,适用于 n ≤ 60 的约束。
    • 关键步骤
      • 预处理 sg 数组:计算所有可能长度连续 '+' 的 SG 值。
      • 分割原字符串:按 '-' 分割为独立子段。
      • 异或和判断:若所有子段 SG 值的异或和 >0,则先手必胜。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

达文汐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值