LeetCode 464. 我能赢吗

https://www.bilibili.com/video/BV15a4y1o7NA

464. 我能赢吗

一、题意概括

有一个整数池,包含 \(1 \sim \text{maxChoosableInteger}\)每个数只能用一次
两名玩家轮流取数,并把取到的数累加到公共和中。
谁先使累计和 \(\ge \text{desiredTotal}\) ,谁获胜

假设双方都采取最优策略,判断 先手是否必胜


二、核心思路(通俗版)

这是一个典型的博弈 + 记忆化搜索(状态压缩 DP)问题

1️⃣ 博弈的本质

  • 当前轮到我走
  • 我可以从还没被用过的数中选一个
  • 选完之后,轮到对手
  • 如果我能找到一种选法,使得:
    • 要么我立刻赢
    • 要么对手在后续状态必输
  • 那么当前状态就是 必胜态

否则是 必败态


2️⃣ 状态如何表示?

由于:

  • 数字最多 20 个
  • 每个数字“用 / 没用”只有两种状态

👉 用一个 二进制 bitmask 表示状态:

  • \(i\) 位为 1:数字 \(i+1\) 已被用过
  • \(i\) 位为 0:还没用

例如(max=5):

mask = 01001(二进制)
表示:1 和 4 已经被使用

3️⃣ 递归状态定义

定义函数:

\[dfs(mask, sum) = \text{当前玩家在该状态下是否能必胜} \]

  • mask:已使用数字的集合
  • sum:当前累计和

4️⃣ 状态转移(关键)

对所有没被用过的数字 i

  • sum + i >= desiredTotal
    • 👉 当前玩家 立刻获胜
  • 否则:
    • 递归判断 dfs(next_mask, sum + i)
    • 如果 对手输(返回 false)
      • 👉 当前玩家就赢

只要存在一种这样的选择,当前状态就是 必胜态


5️⃣ 记忆化(否则会爆炸)

  • 状态数最多 \(2^{20} \approx 10^6\)
  • 用数组 / 哈希表缓存 mask 的结果
  • 每个状态只算一次

6️⃣ 必要的剪枝(非常重要)

  1. desiredTotal ≤ 0
    • 先手啥都不做就赢
  2. 所有数的总和 < desiredTotal
    • 谁都不可能达到目标,先手必输

三、C++ 实现(OI 风格,详细注释)

#include <bits/stdc++.h>
using namespace std;

/*
  dp[mask]:
  -1 表示尚未计算
   0 表示当前状态必败
   1 表示当前状态必胜
*/
int dp[1 << 20];

int maxN, target;

/*
  dfs(mask, sum):
  当前已使用数字集合为 mask,当前累计和为 sum
  返回:当前玩家是否必胜
*/
bool dfs(int mask, int sum) {
    // 如果已经算过,直接返回结果
    if (dp[mask] != -1) return dp[mask];

    // 尝试选择每一个还没用过的数字
    for (int i = 1; i <= maxN; i++) {
        int bit = 1 << (i - 1);
        if (mask & bit) continue; // 这个数已经用过

        // 如果选 i 可以直接达到目标,当前玩家立刻获胜
        if (sum + i >= target) {
            dp[mask] = 1;
            return true;
        }

        // 否则,看对手在下一状态是否失败
        if (!dfs(mask | bit, sum + i)) {
            dp[mask] = 1; // 对手必败,当前必胜
            return true;
        }
    }

    // 所有选择都会让对手获胜,则当前必败
    dp[mask] = 0;
    return false;
}

bool canIWin(int maxChoosableInteger, int desiredTotal) {
    maxN = maxChoosableInteger;
    target = desiredTotal;

    // 特判 1:目标为 0,先手直接赢
    if (target <= 0) return true;

    // 特判 2:所有数加起来都不够
    int sum = maxN * (maxN + 1) / 2;
    if (sum < target) return false;

    // 初始化记忆数组
    memset(dp, -1, sizeof(dp));

    // 从初始状态开始:mask = 0,sum = 0
    return dfs(0, 0);
}

四、复杂度分析

  • 状态数\(2^{\text{maxChoosableInteger}}\) ,最多 \(2^{20} \approx 10^6\)
  • 每个状态最多枚举 20 个数字
  • 时间复杂度

    \[O(2^n \cdot n) \]

  • 空间复杂度

    \[O(2^n) \]

在题目限制内完全可行。

我的代码

dfs记忆化只需记忆state这个可变参数,因为sum由state决定

点击查看代码
class Solution {
public:
    int memo[1<<20];// -1 没算过, 1 true , 0 false
    int m_maxChoosableInteger, m_desiredTotal;
    bool canIWin(int maxChoosableInteger, int desiredTotal) {
        if((1+maxChoosableInteger)*maxChoosableInteger/2<desiredTotal) return false;//总和小于目标,谁都不能获胜
        m_maxChoosableInteger=maxChoosableInteger, m_desiredTotal=desiredTotal;
        memset(memo, -1, sizeof memo);
        return dfs(0, 0);
    }
    //当前玩家在当前状态下,是否能获胜。 sum由state决定,所以不必缓存
    bool dfs(int state, int sum){
        if(memo[state]!=-1) return memo[state];//算过,直接返回
        for(int i=0;i<m_maxChoosableInteger;i++){
            if((state>>i)&1!=0) continue;// i+1用过了
            if(sum+i+1>=m_desiredTotal) return memo[state]=true;
            if(!dfs(state|(1<<i), sum+i+1)) return memo[state]=true;//选了i+1, 并且对手不能赢,我就赢了
        }
        return memo[state]=false;//遍历所有情况,我还不能赢
    }
};
posted @ 2026-01-06 12:46  katago  阅读(7)  评论(0)    收藏  举报