【力扣刷题】预测赢家

🔗 题目链接

题目描述

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

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

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

题目求解

递归法

这道题我们把玩家 1 和玩家 2 都想象成绝对聪明的人,他们都会做最优的选择。先把问题简化,找出玩家 1 和玩家 2 比分的分差,看这个分差大于 0 还是小于 0:

  • 当这个数组只有一个数的时候,即[1]这个数组,那么无论怎么选玩家 1 都会胜利,玩家 1 得到 1 分,玩家 2 没得分,分差为 1;

  • 当游戏数组有两个数的时候,如[1, 5]

    • 玩家 1 选左边,玩家 1 得到 1 分,玩家 2 得到 5 分,玩家 1 比玩家 2 少 4 分,玩家 1 败(得分差是-4)。
    • 玩家 1 选右边,玩家 1 得到 5 分,玩家 2 得到 1 分,玩家 1 比玩家 2 多 4 分,玩家 1 胜(得分差是-4)。

    所以,当有两个数的时候,当前玩家要选择较大的那个数。

  • 当游戏数组有三个数的时候,如[1, 5, 7]

    • 玩家 1 选择左边,玩家 2 从[5, 7]中选择较大的数。最终,玩家 1 得 6 分,玩家 2 得 7 分,玩家 1 比玩家 2 少 1 分,玩家 1 败(得分差是-1)。
    • 玩家 1 选择右边,玩家 2 从[1, 5]中选择较大的数。最终,玩家 1 得 8 分,玩家 2 得 5 分,玩家 1 比玩家 2 多 3 分,玩家 1 胜(得分差是+3)。
    • 因此,玩家 1 要先选择右边。

    有三个数组的时候,玩家 1 选走左边(或右边)后,要计算胜利的数组的分差。然后用,选走的数减去剩下分差,就是玩家 1 最终得分,然后比较取走左边(或右边)的情况下哪个大,则表示玩家 1 最终得分。如果玩家 1 得分为正,则胜利,否则为失败。

  • 以此类推,游戏数组有 4 个数的时候,还是按照上面的递归方式,先取走左边(或右边)把剩下数组变成 3 个数组,然后按照上面的方法继续递归处理。我们以[1, 5, 88, 7]数组为例,来梳理题意。其中,红色表示玩家 1,绿色表示玩家 2。
    在这里插入图片描述

    我们发现,当玩家 1 第一次选择 1 的时候,最终是一定会胜利的。当玩家 1 第一次选择 7 的时候,最后可能会失败。所以,玩家 1 第一次要选择 1。

当游戏数组有 n 个数的时候,玩家 1 作为先手从 [0,n-1] 中选择:

  • 当玩家 1 选择 nums[0],玩家 2 成为先手从 [1, n-1] 中选择:
    • 玩家 2 选择 nums[1],玩家 1 又成为先手从 [2, n-1] 中选择:
      • 玩家 1 选择 nums[2] …(进入递归
      • 玩家 1 选择 nums[n-1] …(进入递归
    • 玩家 2 选择 nums[n-1],玩家 1 又成为先手从 [1, n-2] 中选择:
      • 玩家 1 选择 nums[1] …(进入递归
      • 玩家 1 选择 nums[n-2] …(进入递归
    • 玩家 2 来比较选择左右两边,取最大值
  • 当玩家 1 选择 nums[n-1] ,玩家 2 成为先手从 [0, n-2] 中选择:
  • 玩家 1 比较选择左右两边,取最大值,如果最大值大于 0 则胜利

玩家 1 选择 nums[0] 时,玩家 1 已经得了 nums[0] 对应的分数。接下来,玩家 2 成为了先手,我们就来看看玩家 2 作为先手在 [1, n-1] 中比玩家 1 多得(或少得)多少分,这个分数就是玩家 1 在后续的游戏中比玩家 2 少得(或多得)分数,两者相减就可知玩家 1 最终比玩家 2 多得(或少得)多少分。

同样的道理,如果玩家 1 选择nums[n-1],那么接下来看[0, n-2] 中玩家 1 的分差。

最终,两个分差取最大值,如果该值大于 0 则玩家 1 胜利,否则失败。

/**
 * @param {number[]} nums
 * @return {boolean}
 */
var PredictTheWinner = function (nums) {
  const totalScore = (start, end) => {
    // 只剩下1个数,只能选择这个数
    if (start == end) return nums[start];
    // 当前玩家选择左边的数 nums[start],对手则从 [start+1, end] 来选择
    const scoreStart = nums[start] - totalScore(start + 1, end);
    // 当前玩家选择右边的数 nums[end],对手则从 [start, end-1] 来选择
    const scoreEnd = nums[end] - totalScore(start, end - 1);
    return Math.max(scoreStart, scoreEnd);
  };
  return totalScore(0, nums.length - 1) >= 0;
};

在这里插入图片描述

此题采用递归法求解,递归思路就是把大问题拆解,看到子问题,然后自底而上地解决,算法比较好理解,但是会重复计算,所以时间效率较低。

记忆化递归

上面的方法中,会出现重复计算的情况,这时候我们可以把已经计算过的存储起来,这样就避免了重复计算。
在这里插入图片描述

/**
 * @param {number[]} nums
 * @return {boolean}
 */
var PredictTheWinner = function (nums) {
  // 二维数组,存储计算过的数据
  const tempArr = new Array(nums.length);
  for (let i = 0; i < tempArr.length; i++) {
    tempArr[i] = new Array(nums.length);
  }
  const totalScore = (start, end) => {
    // 只剩下1个数,只能选择这个数
    if (start == end) return nums[start];
    // 如果存储的数组有值,则表示计算过,直接返回
    if (tempArr[start][end] !== undefined) return tempArr[start][end];
    // 当前玩家选择左边的数 nums[start],对手则从 [start+1, end] 来选择
    const scoreStart = nums[start] - totalScore(start + 1, end);
    // 当前玩家选择右边的数 nums[end],对手则从 [start, end-1] 来选择
    const scoreEnd = nums[end] - totalScore(start, end - 1);
    tempArr[start][end] = Math.max(scoreStart, scoreEnd);
    return tempArr[start][end];
  };
  return totalScore(0, nums.length - 1) >= 0;
};

在这里插入图片描述

使用记忆化递归后,耗时明显减少。

动态规划

状态定义:dp[i][j]表示作为先手玩家在区间nums[i, j]中获得的相对分数。

相对分数:自己得分为正,对手得分为负。

状态转移方程为:
在这里插入图片描述

画出状态转移矩阵,因为i>j才有意义,所以只需要画出矩阵的上半部分。按照表格中的顺序,依次依次计算 dp[0][1]dp[1][2]dp[0][2]dp[2][3]dp[1][3]dp[0][3]

nums[i, j]012345
001351015
1-025914
2--04813
3---0712
4----011
5-----0
/**
 * @param {number[]} nums
 * @return {boolean}
 */
var PredictTheWinner = function (nums) {
  const len = nums.length;
  const dp = new Array(len);
  for (let i = 0; i < len; i++) {
    dp[i] = new Array(len);
    dp[i][i] = nums[i];
  }
  // 依次计算 dp[0][1]、dp[1][2]、dp[0][2]、dp[2][3]、dp[1][3]、dp[0][3]......
  for (let j = 1; j < len; j++) {
    for (let i = j - 1; i >= 0; i--) {
      dp[i][j] = Math.max(nums[i] - dp[i + 1][j], nums[j] - dp[i][j - 1]);
    }
  }
  return dp[0][len - 1] >= 0;
};

松露老师

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值