LeetCode 464. 我能赢吗
题目描述


题意分析
双方轮流从
1到maxChoosableInteger的公共整数池中选一个尚未使用的数,加到共同的累计和中。谁先使累计和达到或超过desiredTotal,谁就获胜。两人都采取最优策略,判断先手能否保证获胜。
解法:记忆化搜索 + 位掩码
核心思路
[!blue]
最大可选数
M不超过 20,可以用位掩码used表示已经选过的数字:第pick - 1位为 1,就表示pick已用。已用集合确定了累计和,也就确定了剩余目标remaining和所有可选数,因此即使到达同一集合的选取顺序不同,之后的局面也相同。定义
canWin(used, remaining)表示当前行动玩家能否必胜。枚举一个未使用的pick:若pick >= remaining,本轮直接获胜;否则进入对手的回合,只要canWin(used | bit, remaining - pick)为false,当前玩家就能靠这个选择保证获胜。若所有选择都会让对手必胜,当前局面才是必败。搜索结果只需按
used缓存,remaining作为参数只是避免重复计算集合的和。缓存用 0、1、-1 分别表示未计算、必胜、必败,不能用默认false同时表示未知和失败。每次选择都会增加一个已用数字,递归最多深入M层,不会成环。搜索前先处理两个边界:目标不大于 0 时已满足条件;所有可选数的总和仍小于目标时,任何人都无法达到目标,先手不能保证获胜。后一种情况必须先排除,剩下的对局最迟会在取完全部数字时分出胜负。
解题步骤
desiredTotal <= 0时返回true;1 + 2 + ... + M < desiredTotal时返回false。- 创建长度为
1 << M的三态缓存,从used = 0、remaining = desiredTotal开始搜索。- 若当前掩码已有结果,直接返回;否则枚举所有尚未使用的数。
- 找到能直接达标或让对手必败的选择,就缓存 1 并返回
true。- 所有选择都失败时,缓存 -1 并返回
false。
代码实现
class Solution {
private int maxChoosable;
private byte[] memo;
public boolean canIWin(int maxChoosableInteger, int desiredTotal) {
if (desiredTotal <= 0) {
return true;
}
int total = maxChoosableInteger * (maxChoosableInteger + 1) / 2;
if (total < desiredTotal) {
return false;
}
maxChoosable = maxChoosableInteger;
// 三态区分未知、必胜、必败;剩余目标由已用集合决定。
memo = new byte[1 << maxChoosableInteger];
return canWin(0, desiredTotal);
}
private boolean canWin(int used, int remaining) {
if (memo[used] != 0) {
return memo[used] == 1;
}
for (int pick = 1; pick <= maxChoosable; pick++) {
int bit = 1 << (pick - 1);
if ((used & bit) != 0) {
continue;
}
// 直接达标,或让对手进入必败态,都能保证本方获胜。
if (pick >= remaining || !canWin(used | bit, remaining - pick)) {
memo[used] = 1;
return true;
}
}
memo[used] = -1;
return false;
}
}
func canIWin(maxChoosableInteger int, desiredTotal int) bool {
if desiredTotal <= 0 {
return true
}
total := maxChoosableInteger * (maxChoosableInteger + 1) / 2
if total < desiredTotal {
return false
}
// 三态区分未知、必胜、必败;剩余目标由已用集合决定。
memo := make([]int8, 1<<maxChoosableInteger)
var canWin func(int, int) bool
canWin = func(used, remaining int) bool {
if memo[used] != 0 {
return memo[used] == 1
}
for pick := 1; pick <= maxChoosableInteger; pick++ {
bit := 1 << (pick - 1)
if used&bit != 0 {
continue
}
// 直接达标,或让对手进入必败态,都能保证本方获胜。
if pick >= remaining || !canWin(used|bit, remaining-pick) {
memo[used] = 1
return true
}
}
memo[used] = -1
return false
}
return canWin(0, desiredTotal)
}
复杂度分析
- 时间复杂度:$O(M2^M)$,M 为最大可选数,每个掩码至多枚举 M 个选择。
- 空间复杂度:$O(2^M)$,记忆数组占主导,递归栈至多 M 层。
关键点总结
[!green]
- 存在一个让对手失败的选择即可获胜。
- 掩码决定剩余目标,无需把两个量都作为缓存键。
- 每次公开调用重新建立缓存,目标不同不能混用。
易错点总结
[!yellow]
- 递归结果不取反:把对手必胜当成自己必胜。
- 只按剩余目标缓存:同样剩余目标可能拥有不同可选集合。
- 默认 false 就当已计算必败:混淆未知状态。
- 达标不包含相等:漏掉累计和恰好达到目标的获胜方式。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 486. 预测赢家 | 中等 | 同样在双方最优策略下判胜负,原题可选区间两端,本题可选任意未使用整数,需要访问集合状态。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!