【C/C++】464. 我能赢吗

120 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第16天,点击查看活动详情


题目链接:464. 我能赢吗

题目描述

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

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

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

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

提示:

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

示例 1:

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

示例 2:

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

示例 3:

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

整理题意

题目给定一个整数 maxChoosableInteger ,两个人可以从 [1, maxChoosableInteger] 中轮流选择整数,且每个整数只能选择一次,将两个人选择的整数累加求和,首先使得累计整数和 达到或超过 desiredTotal 的人,即为获胜者。求先手玩家能否获胜,能够获胜返回 true ,否则返回 false。题目给定前提:假设两位玩家游戏时都表现 最佳 ,也就是两人都使用 最优策略

解题思路分析

首先观察题目数据范围,给定的整数不会超过 20 ,且每个整数只有两种状态(已被选和未被选),这里可以想到使用 状态压缩 来存储所有整数的选择情况。

对于每种状态来说,当前可以选择剩下未被选择的整数,对于每个未被选择的整数进行暴搜,将选择后的状态交给对手进行选择(继续暴搜),如果对手无法获胜,就是我们获胜。如果当前无论选择哪个整数都无法获胜就返回 false

这样暴力搜索会超时 TLE,此时我们考虑对于每种状态我们是否仅需暴搜一遍即可,如果是,我们就可以通过记录每个状态的答案,当再次遍历到这种状态时就可以直接返回答案即可。

因为每种状态暴搜后的结果不会改变,仅需遍历一次即可,而对于所有整数的选择存在先后顺序问题,但最后会得到相同的状态,也就是殊途同归的,所以我们可以采用 记忆化搜索 来优化时间复杂度,将每个状态所得到的结果记录下来,当再次遍历到这种情况时就不需要再次搜索了(重复计算),而是直接返回答案。

需要注意的是,我们需要考虑边界情况,当所有数字选完仍无法到达给定的总和 desiredTotal 时,两人都无法获胜,此时返回 false

具体实现

  1. 首先判断边界情况,当所有数字选完仍无法到达给定的总和 desiredTotal 时返回 false
  2. 从状态 0 开始搜索。
  3. 每次判断当前状态是否遍历过,如果遍历过直接返回答案。
  4. 尝试选取当前状态下未被选择的每一个整数,判断选择后是否能够获胜,否则将选择后的状态交给对手进行搜索,需要注意的是,这里需要判断对手拿到选择后的状态是否能够获胜,如果不能获胜则是我们获胜,反之我们失败(这里因为排除了边界总和小于 desiredTotal 的情况,所以当所有数字的和大于等于 desiredTotal 时,其中一方一定能获得胜利)。
  5. 如果当遍历完所有未被选择的整数后都无法获得胜利,那么就失败了,此时返回 false
  6. 期间不断记录每种状态的最终结果。

复杂度分析

  • 时间复杂度:O(2n×n)O(2 ^ n \times n),其中 n = maxChoosableInteger。记忆化后,函数 dfs 最多调用 O(2n)O(2 ^ n) 次,每次消耗 O(n)O(n) 时间,总时间复杂度为 O(2n×n)O(2 ^ n \times n)
  • 空间复杂度:O(2n)O(2 ^ n),其中 n = maxChoosableInteger。搜索的状态有 O(2n)O(2 ^ n) 种,需要消耗空间记忆化。

代码实现

class Solution {
private:
    //改用数组记录状态,0表示没有遍历过,1表示结果赢返回真,2表示结果输返回假
    int mp[1 << 21];
    bool dfs(int maxChoosableInteger, int state, int desiredTotal, int currentTotal){
        if(mp[state] == 1) return true;
        if(mp[state] == 2) return false;
        //遍历所有可选择的数
        for(int i = 1; i <= maxChoosableInteger; i++){
            //没有选过i才进行选取搜索和判断,注意运算符优先级 == 运算符比 & 运算符优先
            if(((state >> i) & 1) == 0){
                //如果选取i后大于目标数则返回true
                if(currentTotal + i >= desiredTotal){
                    mp[state] = 1;
                    return true;
                }
                //如果选取i后对手无法获胜返回true
                if(!dfs(maxChoosableInteger, (1 << i) | state, desiredTotal, currentTotal + i)){
                    mp[state] = 1;
                    return true;
                }
            }
        }
        //无论选择哪个数都无法赢的话就输了
        mp[state] = 2;
        return false;
    }
public:
    bool canIWin(int maxChoosableInteger, int desiredTotal) {
        //如果总和小于目标和,无法达到目标,输
        if(maxChoosableInteger * (maxChoosableInteger + 1) / 2 < desiredTotal) return false;
        //如果当前可选择的数大于目标和,直接获胜
        if(maxChoosableInteger >= desiredTotal) return true;
        //初始化记录数组
        memset(mp, 0, sizeof(mp));
        return dfs(maxChoosableInteger, 0, desiredTotal, 0);
    }
};

总结

  • 当我们看见数据范围较小(小于等于 20 时),同时这道题目中有涉及到是否选取、是否使用这样的二元状态,那么这道题目很可能就是一道状态压缩的题目。
  • 在暴力搜索题中如果存在重复搜索的时候,往往需要记录答案,避免重复计算,这就叫做 记忆化搜索,这样就能够降低时间复杂度。
  • 该题需要判断 必胜态和必败态,同时还需要对 边界 进行处理,该题边界为两人都无法取得胜利时返回 false
  • 测试结果: 微信截图_20220609224724.png

结束语

我们总在仰望和羡慕着别人,一回头,却发现自己正被别人仰望和羡慕着。其实,每个人都有属于自己的幸福,与其总是与别人攀比,不如珍惜此刻所拥有的。把我当下的生活,才能通往明天的幸福。