leetcode
leetcode 401 ~ 450
我能赢吗

我能赢吗

难度:

标签:

题目描述

在 "100 game" 这个游戏中,两名玩家轮流选择从 110 的任意整数,累计整数和,先使得累计整数和 达到或超过  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

代码结果

运行时间: 40 ms, 内存: 16.7 MB


/*
 * Solution for the '100 game' problem using Java Streams.
 * Approach:
 * - Similar to the Java solution but utilizing Java Streams for clarity and functional style.
 * - The game logic remains the same: use memoization and bitmasking to track used numbers and determine possible winning moves.
 */
 
import java.util.stream.IntStream;
 
public class SolutionStream {
    public boolean canIWin(int maxChoosableInteger, int desiredTotal) {
        if (maxChoosableInteger * (maxChoosableInteger + 1) / 2 < desiredTotal) {
            return false;
        }
        Boolean[] memo = new Boolean[1 << maxChoosableInteger];
        return canIWin(maxChoosableInteger, desiredTotal, 0, memo);
    }
 
    private boolean canIWin(int maxChoosableInteger, int desiredTotal, int usedNumbers, Boolean[] memo) {
        if (memo[usedNumbers] != null) {
            return memo[usedNumbers];
        }
        boolean canWin = IntStream.range(0, maxChoosableInteger).anyMatch(i -> {
            int current = 1 << i;
            if ((usedNumbers & current) == 0) {
                return desiredTotal <= i + 1 || !canIWin(maxChoosableInteger, desiredTotal - (i + 1), usedNumbers | current, memo);
            }
            return false;
        });
        memo[usedNumbers] = canWin;
        return canWin;
    }
}

解释

方法:

此题解采用深度优先搜索(DFS)与记忆化的方法解决问题。首先,通过位操作存储每个数字是否已经被使用,以优化存储和检查。对于每一轮游戏,递归地尝试每一个可用的数字,并将当前选中的数字加到已有的总和上。如果当前玩家选择任意一个数字后直接赢得了游戏(即总和达到或超过所需的总和),或者当前选择使得对手在后续任何操作下都无法赢得游戏,则当前玩家将赢得比赛。如果所有可能的数字都被尝试过后,当前玩家依然无法保证获胜,则返回失败。此外,还有一个预判,如果所有数字之和小于desiredTotal,则直接返回False。

时间复杂度:

O(maxChoosableInteger * 2^maxChoosableInteger)

空间复杂度:

O(maxChoosableInteger + 2^maxChoosableInteger)

代码细节讲解

🦆
在DFS递归中,为什么当`currentTotal + i + 1 >= desiredTotal`时,可以直接返回True而不继续探索其他分支?
在深度优先搜索中,当`currentTotal + i + 1 >= desiredTotal`条件满足时,意味着当前玩家的选择已经使得累计的总和达到或超过了胜利的条件(desiredTotal)。因此,当前玩家已经直接赢得了游戏。在这种情况下,没有必要继续探索其他可能的数字选择,因为我们已经找到了一种必胜的策略。继续探索其他分支只会浪费计算资源,因为胜利已经通过当前的选择被确定了。
🦆
使用位操作来标记哪些数字已被使用的决策是基于什么优势?是否存在某些情况下这种方法效率不如其他存储方式?
使用位操作来标记已被使用的数字主要基于空间效率和访问速度的优势。位操作允许我们用一个整数的单个位来表示一个数字是否已被选择,这样可以极大地减少内存的使用量,并且通过位运算(如位与、位或和位移)可以快速检查和更新状态。然而,在某些情况下,如数字范围非常大时,位操作可能会受到整数位数的限制,或者当数字范围和序列非常大而且不连续时,使用位数组或布尔数组可能会更直观或易于处理。在这些情况下,位操作的效率和实用性可能不如使用哈希表或数组直接索引。
🦆
记忆化搜索中`@cache`装饰器的主要作用是什么?如果没有使用记忆化,算法的时间复杂度会如何变化?
在记忆化搜索中,`@cache`装饰器的主要作用是存储已经计算过的函数结果,以避免重复计算相同参数的函数调用,从而大幅提高算法的效率。这种方法特别适用于递归函数,其中某些参数组合会被多次调用。如果没有使用记忆化,深度优先搜索中的每个状态可能需要多次重新计算,导致时间复杂度急剧增加。特别是在这种游戏问题中,状态空间可能非常大(取决于`maxChoosableInteger`和`desiredTotal`),没有记忆化的话,算法的时间复杂度将从指数级别的时间复杂度降低到多项式级别,这使得问题在实际应用中变得不可解。

相关问题

翻转游戏 II

翻转游戏 II

猜数字大小 II

我们正在玩一个猜数游戏,游戏规则如下:

  1. 我从 1 n 之间选择一个数字。
  2. 你来猜我选了哪个数字。
  3. 如果你猜到正确的数字,就会 赢得游戏
  4. 如果你猜错了,那么我会告诉你,我选的数字比你的 更大或者更小 ,并且你需要继续猜数。
  5. 每当你猜了数字 x 并且猜错了的时候,你需要支付金额为 x 的现金。如果你花光了钱,就会 输掉游戏

给你一个特定的数字 n ,返回能够 确保你获胜 的最小现金数,不管我选择那个数字

 

示例 1:

输入:n = 10
输出:16
解释:制胜策略如下:
- 数字范围是 [1,10] 。你先猜测数字为 7 。
    - 如果这是我选中的数字,你的总费用为 $0 。否则,你需要支付 $7 。
    - 如果我的数字更大,则下一步需要猜测的数字范围是 [8,10] 。你可以猜测数字为 9 。
        - 如果这是我选中的数字,你的总费用为 $7 。否则,你需要支付 $9 。
        - 如果我的数字更大,那么这个数字一定是 10 。你猜测数字为 10 并赢得游戏,总费用为 $7 + $9 = $16 。
        - 如果我的数字更小,那么这个数字一定是 8 。你猜测数字为 8 并赢得游戏,总费用为 $7 + $9 = $16 。
    - 如果我的数字更小,则下一步需要猜测的数字范围是 [1,6] 。你可以猜测数字为 3 。
        - 如果这是我选中的数字,你的总费用为 $7 。否则,你需要支付 $3 。
        - 如果我的数字更大,则下一步需要猜测的数字范围是 [4,6] 。你可以猜测数字为 5 。
            - 如果这是我选中的数字,你的总费用为 $7 + $3 = $10 。否则,你需要支付 $5 。
            - 如果我的数字更大,那么这个数字一定是 6 。你猜测数字为 6 并赢得游戏,总费用为 $7 + $3 + $5 = $15 。
            - 如果我的数字更小,那么这个数字一定是 4 。你猜测数字为 4 并赢得游戏,总费用为 $7 + $3 + $5 = $15 。
        - 如果我的数字更小,则下一步需要猜测的数字范围是 [1,2] 。你可以猜测数字为 1 。
            - 如果这是我选中的数字,你的总费用为 $7 + $3 = $10 。否则,你需要支付 $1 。
            - 如果我的数字更大,那么这个数字一定是 2 。你猜测数字为 2 并赢得游戏,总费用为 $7 + $3 + $1 = $11 。
在最糟糕的情况下,你需要支付 $16 。因此,你只需要 $16 就可以确保自己赢得游戏。

示例 2:

输入:n = 1
输出:0
解释:只有一个可能的数字,所以你可以直接猜 1 并赢得游戏,无需支付任何费用。

示例 3:

输入:n = 2
输出:1
解释:有两个可能的数字 1 和 2 。
- 你可以先猜 1 。
    - 如果这是我选中的数字,你的总费用为 $0 。否则,你需要支付 $1 。
    - 如果我的数字更大,那么这个数字一定是 2 。你猜测数字为 2 并赢得游戏,总费用为 $1 。
最糟糕的情况下,你需要支付 $1 。

 

提示:

  • 1 <= n <= 200

预测赢家

给你一个整数数组 nums 。玩家 1 和玩家 2 基于这个数组设计了一个游戏。

玩家 1 和玩家 2 轮流进行自己的回合,玩家 1 先手。开始时,两个玩家的初始分值都是 0 。每一回合,玩家从数组的任意一端取一个数字(即,nums[0]nums[nums.length - 1]),取到的数字将会从数组中移除(数组长度减 1 )。玩家选中的数字将会加到他的得分上。当数组中没有剩余数字可取时,游戏结束。

如果玩家 1 能成为赢家,返回 true 。如果两个玩家得分相等,同样认为玩家 1 是游戏的赢家,也返回 true 。你可以假设每个玩家的玩法都会使他的分数最大化。

 

示例 1:

输入:nums = [1,5,2]
输出:false
解释:一开始,玩家 1 可以从 1 和 2 中进行选择。
如果他选择 2(或者 1 ),那么玩家 2 可以从 1(或者 2 )和 5 中进行选择。如果玩家 2 选择了 5 ,那么玩家 1 则只剩下 1(或者 2 )可选。 
所以,玩家 1 的最终分数为 1 + 2 = 3,而玩家 2 为 5 。
因此,玩家 1 永远不会成为赢家,返回 false 。

示例 2:

输入:nums = [1,5,233,7]
输出:true
解释:玩家 1 一开始选择 1 。然后玩家 2 必须从 5 和 7 中进行选择。无论玩家 2 选择了哪个,玩家 1 都可以选择 233 。
最终,玩家 1(234 分)比玩家 2(12 分)获得更多的分数,所以返回 true,表示玩家 1 可以成为赢家。

 

提示:

  • 1 <= nums.length <= 20
  • 0 <= nums[i] <= 107