题目描述
给你一个整数数组 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 选左边,玩家 1 得到 1 分,玩家 2 得到 5 分,玩家 1 比玩家 2 少 4 分,玩家 1 败(
-
当游戏数组有三个数的时候,如
[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 得分为正,则胜利,否则为失败。
- 玩家 1 选择左边,玩家 2 从
-
以此类推,游戏数组有 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]
…(进入递归)
- 玩家 1 选择
- 玩家 2 选择
nums[n-1]
,玩家 1 又成为先手从[1, n-2]
中选择:- 玩家 1 选择
nums[1]
…(进入递归) - 玩家 1 选择
nums[n-2]
…(进入递归)
- 玩家 1 选择
- 玩家 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] | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
0 | 0 | 1 | 3 | 5 | 10 | 15 |
1 | - | 0 | 2 | 5 | 9 | 14 |
2 | - | - | 0 | 4 | 8 | 13 |
3 | - | - | - | 0 | 7 | 12 |
4 | - | - | - | - | 0 | 11 |
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;
};