LeetCode:486. 预测赢家(python)
给定一个表示分数的非负整数数组。 玩家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 <= 给定的数组长度 <= 20。
- 数组里所有分数都为非负数且不会大于10000000。
- 如果最终两个玩家的分数相等,那么玩家1仍为赢家。
分析:动态规划
思路1:先介绍一种容易理解的动态规划方案
-
入手:
-
确定背景:本题为博弈双方,先手和后手,他们面对不同状态都会做出收益最大的选择,则用
dp_fir
和dp_sec
分别记录双方的最大收益 -
确定状态:本题的状态为序列的不同情况,即
i~j
,i
为序列起始位置,j
为序列结束的位置,且j>=i
,则dp_fir[i][j]
和dp_sec[i][j]
分别表示先手和后手在序列i~j
状态上的最大收益。 -
确定选择:面对序列
i~j
,无论先手还是后手都有两种选择,即选i
(左端)或 选j
(右端)。
-
-
分析状态转移过程:
玩家在状态
i~j
选左端还是选右端取决于选哪边的收益大,列举不同选择的情况- 先手选:
- 选
i
时,先手收益为nums[i]
,当他选择完后,轮到对方选,此时,他将变为后手,面对的状态为i+1~j
,因此总收益为nums[i]+dp_sec[i+1][j]
- 同理,选
j
时,他变为后手面对的状态为i~j-1
,因此总收益为nums[j]+dp_sec[i][j-1]
- 于是,先手将比较选
i
和选j
的收益来决定自己的选择
- 选
- 在先手选择的基础上分析后手选:
- 当先手选
i
时,后手面对序列状态为i+1~j
,此时他转变为先手,收益为dp_fir[i+1][j]
- 同理,先手选
j
时,后手收益为dp_fir[i][j-1]
- 当先手选
- 先手选:
-
分析状态转移方向:
从上述状态转移过程中可得知该动态转移方向为:从下往上,从左往右,且根据入手
j>=i
的条件中可知dp
的有效矩阵为右上三角区。 -
分析初始状态:
i=j
时,序列为单值,则先手
的收益为i
位置的值,后手
的收益为0
。 -
返回值:
序列状态
0~n-1
时,n
为序列的总长度,先手
的最大收益为dp_fir[0][n-1]
,后手
的最大收益为dp_sec[0][n-1]
,则返回是否dp_fir[0][n-1] >= dp_sec[0][n-1]
。
附代码1(Python):
class Solution:
def PredictTheWinner(self, nums):
n = len(nums)
# 初始化先后手 dp,dp]i][j] 中保存先后手在序列 i~j 上的最大收益
# 序列为单数值时,即 i=j,则先手收益为 nums[i],后手收益为 0
dp_fir = [[0]*n for _ in range(n)]
dp_sec = [[0]*n for _ in range(n)]
for i in range(n):
dp_fir[i][i] = nums[i]
# 由动态转移方程可知,动态规划由下往上,由左往右遍历
for i in range(n-1, -1, -1):
for j in range(i+1, n):
left = nums[i]+dp_sec[i+1][j] # 先手面对 i~j,选 nums[i] 时,则在 i+1~j 上,他为后手,因此他的收益为 nums[i] + dp_sec[i+1][j]
right = nums[j]+dp_sec[i][j-1] # 先手面对 i~j,选 nums[j] 时,则在 i~j-1 上,他为后手,因此他的收益为 nums[j] + dp_sec[i][j-1]
if left > right:
dp_fir[i][j] = left # 先手选 left(i)
dp_sec[i][j] = dp_fir[i+1][j] # 先手选了 nums[i],剩下 i+1~j 轮到后手选,此时,后手变成 i+1~j 上的先手
else:
dp_fir[i][j] = right # 先手选 right(j)
dp_sec[i][j] = dp_fir[i][j-1] # 先手选了 nums[j],剩下 i~j-1 轮到后手选,此时,后手变成 i~j-1 上的先手
return dp_fir[0][n-1] >= dp_sec[0][n-1]
test = Solution()
nums_li = [[1, 5, 2], [1, 5, 233, 7]]
for nums in nums_li:
print(test.PredictTheWinner(nums))
False
True
思路2:以上思路的简化,用一组 dp
表示动态规划的过程
-
入手:
dp[i][j]
表示在序列状态为i~j
时,先手
比后手
多的收益最大值注意,由于游戏为回合制,因此
先手
和后手
的身份会相对交换例如:若序列状态为
i~j
时,玩家1
为先手,则在序列状态i+1~j
或i~j-1
时,玩家1
为后手 -
分析状态转移过程:
计算
dp[i][j]
,序列状态为i~j
时,先手
可选nums[i]
,也可选nums[j]
,此时:-
若选择
nums[i]
,反手
将在序列状态为i+1~j
上选择,其收益比先手
多的最大值为dp[i+1][j]
,则先手
比反手多的收益为nums[i]-dp[i+1][j]
-
同理,若选择
nums[j]
,则先手
比反手
多的收益为nums[j]-dp[i][j-1]
-
于是,在玩家都很聪明的条件下,他们选择收益最大的策略,因此更新
dp[i][j]=max(nums[i]-dp[i+1][j], nums[j]-dp[i][j-1])
-
-
分析状态转移方向(同上)
-
分析初始状态:
i=j
时,序列为单值,则先手
的收益比后手
多nums[i]
。 -
返回值:
序列状态
0~n-1
时,先手
比后手
多的最大收益为dp[0][n-1]
,因此返回是否dp[0][n-1] >= 0
。
附代码2(Python3):
class Solution:
def PredictTheWinner(self, nums):
n = len(nums)
dp = [[0]*n for _ in range(n)] # 初始化
for i in range(n-1, -1, -1):
for j in range(i, n):
if i == j:
dp[i][j] = nums[i] # 当前序列状态为单值,先手比后手收益多的最大值为 nums[i]
else:
dp[i][j] = max(nums[i]-dp[i+1][j], nums[j]-dp[i][j-1]) # 更新 dp
return dp[0][n-1] >= 0
test = Solution()
nums_li = [[1, 5, 2], [1, 5, 233, 7]]
for nums in nums_li:
print(test.PredictTheWinner(nums))
False
True
思路3:以上思路的进一步简化,用一组一维 dp
表示动态规划的过程
- 分析:思路2中的
dp
在更新的过程中只需要下一行的数据,因此可将二维dp
压缩成一维dp
。
附代码3(Python3):
class Solution:
def PredictTheWinner(self, nums):
if not nums:
return 0
n = len(nums)
dp = nums[:]
for i in range(n-1, -1, -1):
for j in range(i+1, n):
dp[j] = max(nums[i]-dp[j], nums[j]-dp[j-1])
return dp[n-1] >= 0
test = Solution()
nums_li = [[1, 5, 2], [1, 5, 233, 7]]
for nums in nums_li:
print(test.PredictTheWinner(nums))
False
True
总结:
以上为我对 LeetCode 486.预测玩家
中动态规划的理解和分析,若直接拿出思路3
的方案,很难理解,解释性太差,因此我称该方案为致幻方案,不建议直接啃,一口吃不成大胖子。
其实该题还有优化的空间,若做过 LeetCode 877.石子游戏
,则在初始时加入判定条件,若 n
为偶数或 n=1
,则先手必赢,可直接返回。详见 CSDN 博客