LeetCode 464. 我能赢吗
https://www.bilibili.com/video/BV15a4y1o7NA
一、题意概括
有一个整数池,包含 \(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️⃣ 必要的剪枝(非常重要)
- desiredTotal ≤ 0
- 先手啥都不做就赢
- 所有数的总和 < 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;//遍历所有情况,我还不能赢
}
};

浙公网安备 33010602011771号