Leetcode 486 Predict the Winner

给定一个表示分数的非负整数数组。 玩家1从数组任意一端拿取一个分数,随后玩家2继续从剩余数组任意一端拿取分数,然后玩家1拿,……。每次一个玩家只能拿取一个分数,分数被拿取之后不再可取。直到没有剩余分数可取时游戏结束。最终获得分数总和最多的玩家获胜。
给定一个表示分数的数组,预测玩家1是否会成为赢家。你可以假设每个玩家的玩法都会使他的分数最大化。

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

注意:

  1. 1 <= 给定的数组长度 <= 20.
  2. 数组里所有分数都为非负数且不会大于10000000。
  3. 如果最终两个玩家的分数相等,那么玩家1仍为赢家。

分析:

  1. 显然,第一个人要么拿第1个要么拿最后1个,具体拿哪个取决于哪种拿法能拿的分数更大。
  2. 此时,我们不妨定义score(nums)代表在剩余数组为nums时,先拿的人能获得的最大分数。
  3. 故第一个人能拿到的最大分数就为score(nums),第二个人能拿到的分数自然是sum(nums)-score(nums)。因总和不变,所以第二个人的策略是使第一个人能得到的分数尽量小!

思路:

  1. 对于score(nums),如果len(nums) <= 2,则score(nums) = max(nums)
  2. 如果len(nums)>3,先拿的人可以拿nums[0],也可以拿nums[-1],不妨设拿的nums[0],则此时该第二个人在nums[1:]数组先拿
  3. 第二个人可能拿nums[1]或者nums[-1],拿哪个取决于让第一个人在剩余数组能获得的最大分数最小,即min(score(nums[2:]),score(nums[1:-1]))
  4. 依次类推,注意中间有大量重复计算,即使nums.len <= 20,我们最好还是用记忆化的形式来计算,又因为list是不可哈希对象,所以先转换成tuple记录

补充:
这种记忆化的递归就是dp,转换成dp更容易想清楚,这里我就不写dp方法了。
时间复杂度的分析很简单,观察字典d即可,我们是每递归一次就有一个新的d[nums[i:j]],i处于(0,n-1),j处于(i+1,n-1),故为n^2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution(object):
def PredictTheWinner(self, nums):
"""
:type nums: List[int]
:rtype: bool
"""
d = {}

def score(nums):
if tuple(nums) in d: return d[tuple(nums)]
if len(nums) == 1: return nums[0]
if len(nums) == 2: return max(nums)
res = max(nums[0] + min(score(nums[2:]),score(nums[1:-1])), nums[-1] + min(score(nums[1:-1]), score(nums[:-2])))
d[tuple(nums)] = res
return res

s1 = score(nums)
s2 = sum(nums) - s1
return s1 >= s2

O(n^2) time, O(n^2) space
61 / 61 test cases passed.
Difficulty: medium
Runtime: 40 ms, beats 73.5%