Python面试宝典第3题:石子游戏

题目

Alice 和 Bob 用几堆石子在做游戏:一共有偶数堆石子,排成一行;每堆都有正整数颗石子,数目为 piles[i] 。游戏以谁手中的石子最多来决出胜负,石子的总数是奇数 ,所以没有平局。

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

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

示例 1:

python 复制代码
输入: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:

python 复制代码
输入:piles = [3,7,2,3]
输出:true

暴力法

在石子游戏这类策略游戏中应用暴力法,通常意味着递归地尝试所有可能的取石子序列,直到游戏结束,然后根据这些尝试的结果判断先手玩家是否能赢得比赛。由于暴力法的指数级时间复杂度,在实际应用中,这种解法只适用于非常小的输入规模。使用暴力法求解本题的主要步骤如下。

1、定义递归函数。接收当前石子堆数组、Alice和Bob当前的得分、以及当前轮到的玩家作为参数。

2、递归尝试。对于当前可选的每堆石子,分别尝试从两端取走,更新双方得分,并递归调用函数检查后续步骤。

3、回溯判断。汇总所有尝试的结果,只要有任意一条路径能让Alice最终得分高于Bob,则返回True。否则,返回False。

根据上面的算法步骤,我们可以得出下面的示例代码。

python 复制代码
def stone_game_brute_force(piles, aliceScore=0, bobScore=0, isAlice=True):
    # 没有更多石子堆,判断Alice是否赢得比赛
    if not piles:
        return aliceScore > bobScore
    
    # 选择取走最左边还是最右边的石子堆
    for i in (0, -1):
        nextPile = piles[i]
        # 移除已取的石子堆
        nextPiles = piles[:i] + piles[i+1:]
        
        # 根据当前轮到的玩家更新分数
        nextAliceScore = 0
        nextBobScore = 0
        if isAlice:
            nextAliceScore = aliceScore + nextPile
        else:
            nextBobScore = bobScore + nextPile
        
        # 递归尝试下一步
        if stone_game_brute_force(nextPiles, nextAliceScore, bobScore, not isAlice):
            return True
        if stone_game_brute_force(nextPiles, aliceScore, nextBobScore, not isAlice):
            return True
    
    # 如果所有尝试都无法让Alice赢得比赛,则返回False
    return False

piles = [5, 3, 4, 5]
# 输出: True
print(stone_game_brute_force(piles))

动态规划法

考虑到游戏的最优策略具有重叠子问题和最优子结构的特点,故可以使用动态规划法来解决。我们可以定义一个二维数组dp,其中dp[i][j]表示在石子堆从第i堆到第j堆之间,当前玩家与对手玩家之间的最大得分差。由于每次操作后剩下的石子堆序列也会形成一个新的子问题,因此可以通过比较两端石子堆大小来决定先手玩家的最佳选择,并以此更新dp数组。使用动态规划法求解本题的主要步骤如下。

1、状态定义。定义dp[i][j]为在石子堆区间[i, j]内,按照最优策略进行游戏,先手相对于后手所能多获得的石子数量。这里的i和j代表石子堆的索引,i ≤ j。

2、状态转移。状态转移的关键在于:考虑先手玩家的最优选择。对于区间[i, j],先手有以下两种选择。

(1)取走左侧的石子堆,此时后手面对的是区间[i+1, j],先手优势变为piles[i] - dp[i+1][j]。

(2)取走右侧的石子堆,此时后手面对的是区间[i, j-1],先手优势变为piles[j] - dp[i][j-1]。

3、边界条件。当区间长度为1,即只有一个石子堆时,dp[i][i] = piles[i]。因为此时先手直接拿走全部石子,相对于后手(无石子可拿)优势就是这堆石子的全部数量。

根据上面的算法步骤,我们可以得出下面的示例代码。首先,我们创建一个大小为n x n的二维数组dp,并初始化对角线上的元素,即单个石子堆的情况。然后,按照区间长度从小到大(从长度为2到n)遍历所有可能的区间,并应用状态转移方程更新dp数组中的值。最后,dp[0][n-1]即表示整个石子序列按照最优策略进行游戏时,先手相对于后手的石子优势。如果dp[0][n-1] > 0,则表示Alice能赢得比赛。

python 复制代码
def stone_game_dp(piles):
    n = len(piles)
    # 初始化dp数组,大小为n*n,用于存储区间[i, j]内先手的优势
    dp = [[0]*n for _ in range(n)]
    
    # 边界条件:单个石子堆,先手直接拿走全部,优势为该堆石子的数量
    for i in range(n):
        dp[i][i] = piles[i]
    
    # 构建状态转移方程,从长度为2的区间开始遍历到整个序列
    for length in range(2, n + 1):
        # 遍历区间起始位置
        for i in range(n - length + 1):
            # 计算区间结束位置
            j = i + length - 1
            # 应用状态转移方程,选择使先手优势最大的操作
            dp[i][j] = max(piles[i] - dp[i+1][j], piles[j] - dp[i][j-1])
    
    # 如果dp[0][n-1] > 0,说明按照最优策略,先手能赢得比赛
    return dp[0][n-1] > 0

piles = [5, 3, 4, 5]
# 输出: True
print(stone_game_dp(piles))

总结

暴力法通过递归尝试所有可能的取石子序列来决定胜负,其时间复杂度是指数级别的。具体来说,对于每一步决策,都有两种选择(取左侧或右侧的石子堆),故总的时间复杂度大约为O(2^n),其中n是石子堆的数量。在递归过程中,每一层递归调用都会消耗一定的栈空间来存储函数调用信息。最深的递归深度同样与石子堆的数量n有关,因此空间复杂度也是O(n)。暴力法虽然直观易懂,但对于较大的n值来说,其执行时间将迅速增长至不可接受的程度。

动态规划法通过构建一个二维数组来避免重复计算,时间复杂度主要来自于填充这个数组的过程。对于长度为n的石子堆序列,需要填充一个n×n的表格,每个状态的计算基于之前较小状态的计算结果,因此总体的时间复杂度为O(n^2)。其空间复杂度同样为O(n^2),因为需要一个n×n的二维数组来存储每个子问题的解。动态规划法通过预计算和存储子问题的解,避免了重复计算,极大地提高了效率。对于石子游戏这类具有重叠子问题和最优子结构的问题,动态规划是十分有效的。

相关推荐
互联网架构小马6 分钟前
Flask使用SQLAlchemy添加悲观锁和乐观锁
后端·python·flask
鸽芷咕12 分钟前
【pyhont报错已解决】ERROR: Could not find a version that satisfies the requirement
python·bug
数据分析螺丝钉23 分钟前
力扣第218题“天际线问题”
经验分享·python·算法·leetcode·面试
知其然亦知其所以然1 小时前
深入Kafka:如何保证数据一致性与可靠性?
后端·面试·kafka
IT·陈寒1 小时前
Kotlin vs Java:深入解析两者之间的最新差异与优劣(全面指南)
java·python·kotlin
知识分享小能手2 小时前
从新手到高手:Scala函数式编程完全指南,Scala 访问修饰符(6)
大数据·开发语言·后端·python·数据分析·scala·函数式编程
elderingezez2 小时前
2024年用scrapy爬取BOSS直聘的操作
爬虫·python·scrapy
Eiceblue2 小时前
用Python轻松转换Markdown文件为PDF文档
开发语言·vscode·python·pdf·word
杰哥在此2 小时前
Java面试题:解释跨站脚本攻击(XSS)的原理,并讨论如何防范
java·开发语言·面试·编程·xss