LeetCode:486. 预测赢家(python)

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

LeetCode 链接

分析:动态规划
思路1:先介绍一种容易理解的动态规划方案
  • 入手

    • 确定背景:本题为博弈双方,先手和后手,他们面对不同状态都会做出收益最大的选择,则用 dp_firdp_sec 分别记录双方的最大收益

    • 确定状态:本题的状态为序列的不同情况,即 i~ji 为序列起始位置,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~ji~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 题解1

LeetCode 题解2

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
给定一个整数数组 nums 和一个目标值 target,要求在数组中找出两个数的和等于目标值,并返回这两个数的索引。 思路1:暴力法 最简单的思路是使用两层循环遍历数组的所有组合,判断两个数的和是否等于目标值。如果等于目标值,则返回这两个数的索引。 此方法的时间复杂度为O(n^2),空间复杂度为O(1)。 思路2:哈希表 为了优化时间复杂度,可以使用哈希表来存储数组中的元素和对应的索引。遍历数组,对于每个元素nums[i],我们可以通过计算target - nums[i]的值,查找哈希表中是否存在这个差值。 如果存在,则说明找到了两个数的和等于目标值,返回它们的索引。如果不存在,将当前元素nums[i]和它的索引存入哈希表中。 此方法的时间复杂度为O(n),空间复杂度为O(n)。 思路3:双指针 如果数组已经排序,可以使用双指针的方法来求解。假设数组从小到大排序,定义左指针left指向数组的第一个元素,右指针right指向数组的最后一个元素。 如果当前两个指针指向的数的和等于目标值,则返回它们的索引。如果和小于目标值,则将左指针右移一位,使得和增大;如果和大于目标值,则将右指针左移一位,使得和减小。 继续移动指针,直到找到两个数的和等于目标值或者左指针超过了右指针。 此方法的时间复杂度为O(nlogn),空间复杂度为O(1)。 以上三种方法都可以解决问题,选择合适的方法取决于具体的应用场景和要求。如果数组规模较小并且不需要考虑额外的空间使用,则暴力法是最简单的方法。如果数组较大或者需要优化时间复杂度,则哈希表或双指针方法更合适。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值