• 【力扣刷题】预测赢家


    🔗 题目链接

    题目描述

    给你一个整数数组 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;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这里插入图片描述

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

    记忆化递归

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

    /**
     * @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;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    在这里插入图片描述

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

    动态规划

    状态定义: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;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    松露老师

  • 相关阅读:
    基于RSSI的室内wifi定位系统 计算机竞赛
    Swift 中的类型占位符
    Leetcode Practice -- 数组
    原型-设计模式
    盛唐诗人三杰,儒释道的代表
    JS高级 之 深拷贝 && 浅拷贝
    Java学习Go(入门)
    new Date() 格式化日期注意事项
    简易版剪辑视频程序(python-VideoFileClip)
    DRDS的介绍
  • 原文地址:https://blog.csdn.net/u011718737/article/details/127804692