石子游戏

标签: 数组 数学 动态规划 博弈

难度: Medium

Alice 和 Bob 用几堆石子在做游戏。一共有偶数堆石子,排成一行;每堆都有 整数颗石子,数目为 piles[i] 。

游戏以谁手中的石子最多来决出胜负。石子的 总数奇数 ,所以没有平局。

Alice 和 Bob 轮流进行,Alice 先开始 。 每回合,玩家从行的 开始结束 处取走整堆石头。 这种情况一直持续到没有更多的石子堆为止,此时手中 石子最多 的玩家 获胜

假设 Alice 和 Bob 都发挥出最佳水平,当 Alice 赢得比赛时返回 true ,当 Bob 赢得比赛时返回 false 。

示例 1:

输入:piles = [5,3,4,5]
输出:true
解释:
Alice 先开始,只能拿前 5 颗或后 5 颗石子 。
假设他取了前 5 颗,这一行就变成了 [3,4,5] 。
如果 Bob 拿走前 3 颗,那么剩下的是 [4,5],Alice 拿走后 5 颗赢得 10 分。
如果 Bob 拿走后 5 颗,那么剩下的是 [3,4],Alice 拿走后 4 颗赢得 9 分。
这表明,取前 5 颗石子对 Alice 来说是一个胜利的举动,所以返回 true 。

示例 2:

输入:piles = [3,7,2,3]
输出:true

提示:

  • 2 <= piles.length <= 500
  • piles.length偶数
  • 1 <= piles[i] <= 500
  • sum(piles[i]) 是 奇数

Submission

运行时间: 1004 ms

内存: 173.8 MB

from typing import List


class Solution:
    def stoneGame(self, piles: List[int]) -> bool:
        memo = dict()
        n = len(piles)

        def dp(i, j):
            if (i, j) in memo:
                return memo[(i, j)]
            if i == j:
                return piles[i], 0
            left = dp(i + 1, j)[1] + piles[i]
            right = dp(i, j - 1)[1] + piles[j]
            if left > right:
                memo[(i, j)] = (left, dp(i + 1, j)[0])
            else:
                memo[(i, j)] = (right, dp(i, j - 1)[0])
            return memo[(i, j)]

        a, b = dp(0, n - 1)
        return a > b

Explain

这个题解使用了动态规划的思路。定义了一个二维的备忘录memo,用于记录区间 [i, j] 内先手和后手最多能拿到的石子数。 在 dp 函数中,对区间 [i, j] 进行划分,如果区间长度为 1,直接返回该堆石子的数量。否则,分别计算先手拿左边或右边的石子堆时,对手能拿到的最大数量。最后返回两种情况下,先手能拿到石子数更多的那个。 在主函数中,调用 dp(0, n-1),返回先手和后手拿到的石子数,比较大小即可判断先手是否获胜。

时间复杂度: O(n^2)

空间复杂度: O(n^2)

from typing import List


class Solution:
    def stoneGame(self, piles: List[int]) -> bool:
        memo = dict()
        n = len(piles)

        def dp(i, j):
            if (i, j) in memo:
                return memo[(i, j)]
            if i == j:
                return piles[i], 0
            # 先手拿左边,后手的最大值为dp(i+1,j)[1],先手的值为后手值加上piles[i]
            left = dp(i + 1, j)[1] + piles[i] 
            # 先手拿右边,后手的最大值为dp(i,j-1)[1],先手的值为后手值加上piles[j]
            right = dp(i, j - 1)[1] + piles[j]  
            if left > right:
                memo[(i, j)] = (left, dp(i + 1, j)[0])
            else:
                memo[(i, j)] = (right, dp(i, j - 1)[0])
            return memo[(i, j)]

        a, b = dp(0, n - 1)  # a为先手的最大值,b为后手的最大值
        return a > b

Explore

在解决这个问题时,我们需要存储每一对索引(i, j)的结果,这对应于每一个子问题的解。使用二维备忘录(如字典)使得我们可以灵活地存储和查询任意(i, j)对的结果,而不必为每一对可能的索引组合预先分配固定的空间。这种方式在处理稀疏数据或者不连续的索引时特别有效,并且可以节省内存。如果使用数组,我们需要预先定义一个足够大的二维数组来存储所有可能的索引组合,这可能会导致空间的浪费,特别是当索引的可能组合并不是全部需要被使用时。

当区间长度为1时,即只有一堆石子,先手可以拿走这堆石子,因为之后没有剩余的石子可供后手选,所以后手无法拿到任何石子。因此,在这种情况下返回的元组中第二个元素是0,确实表示在这个特定的子问题中,后手没有石子可拿。这不是说只有一个玩家可以拿石子,而是在这种特定的局面下,后手无石子可拿。

在决策过程中,我们计算两种情况:先手拿左边的石子或右边的石子。对于每种情况,我们都会计算在先手拿走石子后,后手所能拿到的最大石子数。然后,我们会把后手的最大值加到当前先手拿的石子数上,得到先手在这两种决策下的总石子数。最后,我们比较这两种情况下的先手总石子数,并选择先手石子数更多的那种情况。这种方法确保了先手可以在当前局面下最大化其拿到的石子总数。

在这个特定的石子游戏中,因为先手总是可以选择对其最有利的策略,实际上先手总是处于有利位置。因此,可以更直接地预测游戏结果:无论如何,先手总是会赢。这是因为先手可以根据堆中石子的总量和分布,制定拿石子的策略以确保总是处于领先。但在代码实现中,我们通常通过动态规划来验证这一点,以确保考虑所有可能的情况和策略。