Leetcode 464. 我能赢吗

在 “100 game” 这个游戏中,两名玩家轮流选择从 1 到 10 的任意整数,累计整数和,先使得累计整数和 达到或超过 100 的玩家,即为胜者。

如果我们将游戏规则改为 “玩家 不能 重复使用整数” 呢?

例如,两个玩家可以轮流从公共整数池中抽取从 1 到 15 的整数(不放回),直到累计整数和 >= 100。

给定两个整数 maxChoosableInteger (整数池中可选择的最大数)和 desiredTotal(累计和),若先出手的玩家能稳赢则返回 true ,否则返回 false 。假设两位玩家游戏时都表现 最佳 。

示例 1:

输入:maxChoosableInteger = 10, desiredTotal = 11
输出:false
解释:
无论第一个玩家选择哪个整数,他都会失败。
第一个玩家可以选择从 1 到 10 的整数。
如果第一个玩家选择 1,那么第二个玩家只能选择从 2 到 10 的整数。
第二个玩家可以通过选择整数 10(那么累积和为 11 >= desiredTotal),从而取得胜利.
同样地,第一个玩家选择任意其他整数,第二个玩家都会赢。
示例 2:

输入:maxChoosableInteger = 10, desiredTotal = 0
输出:true
示例 3:

输入:maxChoosableInteger = 10, desiredTotal = 1
输出:true

提示:

1 <= maxChoosableInteger <= 20
0 <= desiredTotal <= 300

class Solution {
public:
    vector<int> f;
    int n, m;

    bool dp(int x) {
        if(f[x] != -1) return f[x];
        int sum = 0;
        for(int i = 0; i < n; i ++ )
            if(x >> i & 1)
                sum += i + 1;
        for(int i = 0; i < n; i ++ ) {
            if(x >> i & 1) continue;
            if(sum + i + 1 >= m) return f[x] = 1;
            if(!dp(x + (1 << i))) return f[x] = 1;
        }
        return f[x] = 0;
    }

    bool canIWin(int maxChoosableInteger, int desiredTotal) {
        n = maxChoosableInteger, m = desiredTotal;
        if(n * (n + 1) / 2 < m) return false;
        f.resize(1 << n, -1);
        return dp(0);
    }
};

代码详解:

这段代码解决的是一个经典的动态规划问题,即“100 游戏”的变体。在这个游戏中,两个玩家轮流从整数池中选择一个整数,累加这些整数的和,先使得累积和达到或超过 desiredTotal 的玩家获胜。每个整数只能被选一次。

题目描述

给定两个整数 maxChoosableIntegerdesiredTotal,判断先手玩家是否必胜,假设两位玩家都表现最佳。

思路和详细解释

1. 问题建模

首先,我们将整数池建模为从 1 到 maxChoosableInteger 的所有整数。我们需要判断在两位玩家都采取最优策略的情况下,先手玩家是否必胜。

2. 动态规划定义

我们使用一个动态规划数组 f,其中 f[x] 表示状态 x 下当前玩家是否必胜。状态 x 用一个整数的二进制表示,位 i 表示整数 i+1 是否已经被选择。f[x] = 1 表示当前玩家在状态 x 下必胜,f[x] = 0 表示必输。

3. 基本边界条件
  1. 如果整数池中所有数的和小于 desiredTotal,即 maxChoosableInteger * (maxChoosableInteger + 1) / 2 < desiredTotal,则先手玩家必输,返回 false
  2. 初始化动态规划数组 f,大小为 2^n,所有值初始为 -1 表示未计算。
4. 动态规划状态转移
  1. 从状态 0 开始,逐步计算每个状态下的结果。
  2. 计算当前状态 x 下所有被选数的和 sum
  3. 遍历所有可能的选择 i
    • 如果整数 i+1 已经被选过,则跳过。
    • 如果选择 i+1 后,累积和达到或超过 desiredTotal,则当前玩家赢。
    • 如果对手在选择 i+1 后的状态下无法赢,则当前玩家赢。
5. 状态转移的实现

动态规划函数 dp 的具体实现如下:

bool dp(int x) {
    if(f[x] != -1) return f[x];  // 如果状态 x 已经计算过,则直接返回结果
    int sum = 0;
    for(int i = 0; i < n; i++) {
        if(x >> i & 1)  // 检查位 x 的第 i 位是否已经被使用
            sum += i + 1;  // 计算当前状态 x 下所有被选数的和
    }
    for(int i = 0; i < n; i++) {
        if(x >> i & 1) continue;  // 如果整数 i+1 已经被选过,则跳过
        if(sum + i + 1 >= m) return f[x] = 1;  // 如果选择 i+1 后,累积和达到或超过 desiredTotal,则当前玩家赢
        if(!dp(x + (1 << i))) return f[x] = 1;  // 如果对手在选择 i+1 后的状态下无法赢,则当前玩家赢
    }
    return f[x] = 0;  // 否则,当前玩家输
}
6. 主函数 canIWin
  1. 初始化整数池大小 n 和目标和 m
  2. 检查边界条件。
  3. 初始化动态规划数组 f
  4. 从初始状态开始计算。
bool canIWin(int maxChoosableInteger, int desiredTotal) {
    n = maxChoosableInteger;
    m = desiredTotal;
    if(n * (n + 1) / 2 < m) return false;  // 如果整数池中所有数的和小于 desiredTotal,则先手玩家必输
    f.resize(1 << n, -1);  // 初始化动态规划数组
    return dp(0);  // 从初始状态开始计算
}

例子

考虑一个例子:maxChoosableInteger = 10desiredTotal = 11

  1. 总和 1 + 2 + ... + 10 = 55,大于 desiredTotal
  2. 从状态 0 开始:
    • 先手玩家选择 1,后手玩家在剩余数字 [2, 3, ..., 10] 下。
    • 对于后手玩家来说,如果选择 2,累积和为 3,再选择 3 累积和为 6,最终会超过 11
    • 因此,先手玩家可以选择 1,然后通过最佳策略赢得游戏。

通过这种方式,动态规划会计算所有可能的状态和结果,最终确定先手玩家是否必胜。

怎么判断i + 1已经被选过了?
在这段代码中,判断整数 i+1 是否已经被选过是通过位运算实现的。每个状态 x 用一个整数的二进制表示,位 i 表示整数 i+1 是否已经被选择。具体来说,如果状态 x 的第 i 位是 1,则表示整数 i+1 已经被选过。如果是 0,则表示整数 i+1 还没有被选。

具体实现

if (x >> i & 1) continue;

这行代码的作用是跳过已经被选过的整数 i+1,具体解释如下:

  1. 右移操作x >> i 将状态 x 右移 i 位。

    • 例如,如果 x 的二进制表示是 1101(即选择了第 1、3 和 4 个整数),当 i = 2 时,x >> 2 的结果是 11(即 3)。
  2. 按位与操作x >> i & 1 将右移后的结果与 1 进行按位与运算。

    • 按位与运算的结果是取右移后结果的最后一位。如果最后一位是 1,表示整数 i+1 已经被选过;如果是 0,表示整数 i+1 还没有被选过。
    • 例如,对于右移后的结果 11,与 1 进行按位与运算的结果是 1,表示整数 i+1 已经被选过。
  3. 条件判断:如果 x >> i & 1 为真(即非零),则说明整数 i+1 已经被选过,继续下一次循环,跳过当前的 i

示例

假设 x = 13(二进制表示为 1101),表示选择了第 1、3 和 4 个整数。

  • i = 0 时,x >> 0 & 1 等于 1101 & 1,结果为 1,表示第 1 个整数已经被选过,跳过。
  • i = 1 时,x >> 1 & 1 等于 110 & 1,结果为 0,表示第 2 个整数还没有被选过,不跳过。
  • i = 2 时,x >> 2 & 1 等于 11 & 1,结果为 1,表示第 3 个整数已经被选过,跳过。
  • i = 3 时,x >> 3 & 1 等于 1 & 1,结果为 1,表示第 4 个整数已经被选过,跳过。

总结

通过位运算判断整数是否已经被选过,可以高效地实现状态的检查和更新。这种方法利用二进制表示的特性,将整数池中的选择状态压缩在一个整数中,便于动态规划状态的存储和转移。

  • 16
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值