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 博客
本文解析了LeetCode 486题——预测赢家的动态规划解决方案,通过逐步分析,介绍了三种由繁至简的算法实现,帮助读者深入理解博弈论中策略选择与动态规划的关系。
386

被折叠的 条评论
为什么被折叠?



