📜  谜题73 |纸牌游戏(1)

📅  最后修改于: 2023-12-03 15:28:12.196000             🧑  作者: Mango

谜题73 |纸牌游戏

题目描述

你和另一个人玩一种有趣的纸牌游戏。你们轮流从一副牌(没有大小王)中取牌,直到取完为止。每次取牌时,可以从最左侧或最右侧取走一张牌。每张牌上都带有一个正整数,表示你能获得的分数。假设你先手,双方都采用最优的策略,问你最终能获得多少分数?

分析

这是一道经典的博弈论问题。可以用递归或动态规划两种方式求解。

递归:

假设此时牌堆为一个长度为n的序列a[1..n],最终你能获得的分数为f[1][n]。若此时该轮为你取牌,则你可选择从两端中的任意一端取走一张牌,获得对应的分数。

  1. 如果你从最左侧取牌,则你在剩下的牌中仍然是先手,所获得的分数为a[1],对手在剩下的牌中变为后手,所获得的分数为f[2][n]。
  2. 如果你从最右侧取牌,则你在剩下的牌中仍然是先手,所获得的分数为a[n],对手在剩下的牌中变为后手,所获得的分数为f[1][n-1]。

故此时你获得的分数为score=a[1]+max(f[2][n],f[1][n-1])。

若此时该轮为对手取牌,则对手可选择从两端中的任意一端取走一张牌。不妨假设他从最左侧取牌,则他在剩下的牌中变为先手,所获得的分数为a[1],你在剩下的牌中变为后手,所获得的分数为f[2][n]。故此时你获得的分数为min(f[2][n-1],f[1][n-1])。

综上,我们得出递推式:f[i][j] = max(a[i]+min(f[i+1][j-1], f[i+2][j]), a[j]+min(f[i+1][j-1], f[i][j-2]))。

边界情况为f[i][i]=a[i]。

最终结果为f[1][n]。

动态规划:

我们可以使用备忘录优化递归实现,也可以直接使用动态规划实现。

定义状态数组f[i][j]为从i到j范围内取牌时能获得的最大分数,则最终结果为f[1][n]。初始值为f[i][i]=a[i](i从1到n)。

对于长度为2的牌堆(即相邻的两张牌),我们可以直接根据规则获得最优解。

接下来我们考虑长度为3的牌堆,此时我们需要注意从哪一端取牌以及对手的取牌策略,具体如下:

  1. 如果你取两端的任意一端,则对手会取本轮中你所剩牌堆中的最大值。
    • 如果你从左端取牌,则对手会取a[2]或a[3]中的最大值,故你获得的分数为a[1]+min(f[3][n], f[2][n-1])。
    • 如果你从右端取牌,则对手会取a[1]或a[2]中的最大值,故你获得的分数为a[3]+min(f[1][n-2], f[2][n-1])。
  2. 如果你取中间的牌,则对手会取本轮中你所剩牌堆中的最大值,故你获得的分数为a[2]+min(f[1][n-2], f[2][n-1])。

我们可以依次类推,得到状态转移方程:f[i][j]=max(a[i]+min(f[i+2][j], f[i+1][j-1]), a[j]+min(f[i+1][j-1], f[i][j-2]))。

代码实现如下:

def card_game(nums: List[int]) -> int:
    n = len(nums)
    f = [[0] * n for _ in range(n)]
    
    # 初始化长度为1的牌堆
    for i in range(n):
        f[i][i] = nums[i]
    
    # 枚举长度为2至n的牌堆
    for k in range(2, n+1):
        for i in range(n-k+1):
            j = i + k - 1
            # 取两端的牌
            score1 = nums[i] + min(f[i+2][j], f[i+1][j-1])
            score2 = nums[j] + min(f[i+1][j-1], f[i][j-2])
            f[i][j] = max(score1, score2)
            # 取中间的牌
            f[i][j] = max(f[i][j], nums[i+1] + min(f[i+1][j-1], f[i][j-2]))
    
    return f[0][n-1]
总结

本题是一道经典的博弈论问题,可用递归或动态规划两种方式实现。使用动态规划实现时,需要特别注意状态转移方程的对称性,以及长度为3的牌堆的处理方法。