leetcode
leetcode 2651 ~ 2700
预测赢家

预测赢家

难度:

标签:

题目描述

You are given an integer array nums. Two players are playing a game with this array: player 1 and player 2.

Player 1 and player 2 take turns, with player 1 starting first. Both players start the game with a score of 0. At each turn, the player takes one of the numbers from either end of the array (i.e., nums[0] or nums[nums.length - 1]) which reduces the size of the array by 1. The player adds the chosen number to their score. The game ends when there are no more elements in the array.

Return true if Player 1 can win the game. If the scores of both players are equal, then player 1 is still the winner, and you should also return true. You may assume that both players are playing optimally.

 

Example 1:

Input: nums = [1,5,2]
Output: false
Explanation: Initially, player 1 can choose between 1 and 2. 
If he chooses 2 (or 1), then player 2 can choose from 1 (or 2) and 5. If player 2 chooses 5, then player 1 will be left with 1 (or 2). 
So, final score of player 1 is 1 + 2 = 3, and player 2 is 5. 
Hence, player 1 will never be the winner and you need to return false.

Example 2:

Input: nums = [1,5,233,7]
Output: true
Explanation: Player 1 first chooses 1. Then player 2 has to choose between 5 and 7. No matter which number player 2 choose, player 1 can choose 233.
Finally, player 1 has more score (234) than player 2 (12), so you need to return True representing player1 can win.

 

Constraints:

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

代码结果

运行时间: 48 ms, 内存: 16.3 MB


/*
 * Solution for the given problem using Java Streams
 * 
 * While Java Streams are typically used for processing collections and streams of data,
 * the dynamic programming approach doesn't lend itself well to a direct stream-based solution.
 * However, we can still use functional programming concepts to initialize and process the DP table.
 */

import java.util.stream.IntStream;

public class Solution {
    public boolean PredictTheWinner(int[] nums) {
        int n = nums.length;
        int[][] dp = new int[n][n];
        IntStream.range(0, n).forEach(i -> dp[i][i] = nums[i]);
        IntStream.range(1, n).forEach(len -> 
            IntStream.range(0, n - len).forEach(i -> {
                int j = i + len;
                dp[i][j] = Math.max(nums[i] - dp[i + 1][j], nums[j] - dp[i][j - 1]);
            })
        );
        return dp[0][n - 1] >= 0;
    }
}

解释

方法:

这个题解使用记忆化搜索(递归+记忆化)的方法来解决问题。定义函数 dfs(l,r) 表示当剩下的数组索引范围为 [l,r] 时,当前玩家与另一个玩家的分数之差的最大值。在每一步中,当前玩家可以选择 nums[l] 或 nums[r],然后轮到另一个玩家在剩下的数组范围内进行选择。玩家 1 会选择最大化分数差值的方案,因此最终结果只要大于等于 0,就说明玩家 1 可以获胜。

时间复杂度:

O(n^2)

空间复杂度:

O(n^2)

代码细节讲解

🦆
在函数 dfs(l, r) 中,当 l 大于 r 时直接返回 0 的原因是什么?
当 l 大于 r 时,意味着数组中没有剩余的元素可以选择。在这种情况下,当前玩家无法进行任何操作,因此无法增加或减少分数差值。返回 0 是表示在这种边界条件下,没有分数可以被加到任一玩家的总分上,因此分数差维持不变。
🦆
递归函数 dfs 使用了 @cache 装饰器进行记忆化,这对算法的效率提升作用是如何体现的?记忆化存储了哪些信息?
记忆化是通过存储已经计算过的结果来避免重复计算,从而提高算法效率的技术。在这个场景中,@cache 装饰器会自动存储每一对 (l, r) 的 dfs 函数的结果。这意味着每个子问题只解决一次,而不是每次递归调用时都重新计算。这种方式显著减少了递归的调用次数,从而加速整体的计算过程。记忆化存储的信息是每一对 (l, r) 索引范围内,当前玩家与另一个玩家的分数之差的最大值。
🦆
为什么在计算分数差值时使用 nums[l] - dfs(l + 1, r) 和 nums[r] - dfs(l, r - 1) 这种形式?这样的计算逻辑是基于什么考虑?
这种计算形式是基于博弈理论中的最优策略选择。当前玩家面临选择数组端点 nums[l] 或 nums[r] 的决策。选择 nums[l] 后,剩余数组范围变为 [l+1, r],而选择 nums[r] 后,剩余范围变为 [l, r-1]。在每一种选择后,分数差值的计算考虑了当前选择的值减去对手在剩余数组范围内的最优选择(即 dfs 返回的值)。这样的计算确保当前玩家在任一步骤都尽可能扩大分数差,从而在游戏中保持优势或实现逆转。

相关问题

我能赢吗

在 "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