持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第16天,点击查看活动详情
题目链接:464. 我能赢吗
题目描述
在 "100 game" 这个游戏中,两名玩家轮流选择从 1 到 10 的任意整数,累计整数和,先使得累计整数和 达到或超过 100 的玩家,即为胜者。
如果我们将游戏规则改为 “玩家 不能 重复使用整数” 呢?
例如,两个玩家可以轮流从公共整数池中抽取从 1 到 15 的整数(不放回),直到累计整数和 >= 100。
给定两个整数 maxChoosableInteger (整数池中可选择的最大数)和 desiredTotal(累计和),若先出手的玩家是否能稳赢则返回 true ,否则返回 false 。假设两位玩家游戏时都表现 最佳 。
提示:
1 <= maxChoosableInteger <= 200 <= 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。
具体实现
- 首先判断边界情况,当所有数字选完仍无法到达给定的总和
desiredTotal时返回false。 - 从状态
0开始搜索。 - 每次判断当前状态是否遍历过,如果遍历过直接返回答案。
- 尝试选取当前状态下未被选择的每一个整数,判断选择后是否能够获胜,否则将选择后的状态交给对手进行搜索,需要注意的是,这里需要判断对手拿到选择后的状态是否能够获胜,如果不能获胜则是我们获胜,反之我们失败(这里因为排除了边界总和小于
desiredTotal的情况,所以当所有数字的和大于等于desiredTotal时,其中一方一定能获得胜利)。 - 如果当遍历完所有未被选择的整数后都无法获得胜利,那么就失败了,此时返回
false。 - 期间不断记录每种状态的最终结果。
复杂度分析
- 时间复杂度:,其中
n = maxChoosableInteger。记忆化后,函数dfs最多调用 次,每次消耗 时间,总时间复杂度为 。 - 空间复杂度:,其中
n = maxChoosableInteger。搜索的状态有 种,需要消耗空间记忆化。
代码实现
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。 - 测试结果:
结束语
我们总在仰望和羡慕着别人,一回头,却发现自己正被别人仰望和羡慕着。其实,每个人都有属于自己的幸福,与其总是与别人攀比,不如珍惜此刻所拥有的。把我当下的生活,才能通往明天的幸福。