带有列表的 Python 基于轮次的递归

Posted

技术标签:

【中文标题】带有列表的 Python 基于轮次的递归【英文标题】:Python Turn-based Recursion with Lists 【发布时间】:2015-11-21 06:53:41 【问题描述】:

我正在为我的一个 CS 课程解决一个问题,并且我非常有信心我有正确的想法,只是在我实施它时我没有得到正确的答案。

游戏是:有两个玩家,以及一个数字列表(即 [3,7,8,1,6,4,5])。每个玩家轮流从列表的任一端选择一个数字。一旦选择了一个数字,它就会从列表中删除,然后对手可以从这个新列表中选择他们想要的结果。目标是在列表为空时获得最大的数字总和。

我的想法:假设我们从简单的列表 [1,2,3,4,5] 开始。当玩家 1 从开头或结尾选择一个数字(1 或 5)时,我们现在有一个较小的列表可供对手选择。所以让我举一个使用这个列表的例子:

我选择5。新的列表是[1,2,3,4],对手可以选择。我不知道他们会选择列表的哪一端,但我知道它只能是1或4. 如果是 1,那么当再次轮到我时,我剩下 [2,3,4]。如果他们选择 4,我就剩下 [1,2,3]。如果我选择 1,它们会留下 [2,3],如果我选择 3,它们会留下 [1,2],依此类推,直到列表中没有数字。对手也在尽最大的努力获得最高分,所以他们不会一直贪婪地选择更大的数字。玩家同样聪明,所以他们都会使用完全相同的策略来为自己获得最高分。

这是一个每次在较小列表上的明显递归问题。

注意:我不是在寻找要给出的代码。由于这是一门 cs 课程,我真的更愿意得到关于我可能做错了什么的提示,以便我可以学习而不是得到代码。

这是我写的代码:

def Recursive(A):
    # check if there is only one item left. If so, return it 
    if len(A) == 1:
         return A[0]

    # take the left item and recurse on the list if the opponent 
    # were to take the left side, and the list if the opponent 
    # were to take the right number
    takeLeftSide = A[0] + max(Recursive(A[1:-1]), Recursive(A[2:len(A)]))
    takeRightSide = A[-1] + max(Recursive(A[0:-2]), Recursive(A[1:-1]))

    return max(takeLeftSide, takeRightSide)

if __name__ == '__main__':
    A = [5,8,1,3,6]
    print Recursive(A) 

我相信我应该期待 12,但在某些情况下,我的输出结果是 19 和 14。

感谢您的帮助,我已经为此工作了好几个小时,我知道一旦您尝试深入研究递归,事情就会变得混乱和混乱。

【问题讨论】:

我不确定您要做什么。显然您实际上并没有两个玩家,您只是根据一些逻辑进行选择。 Recursive() 应该返回什么? 这看起来不像是我自然会使用递归的东西。 返回一个玩家可以获得的最大总和。我不确定如何真正添加玩家,因为我无法向该方法添加另一个参数 它不需要递归,空列表在布尔表达式中计算为False,因此您可以使用while 循环。 我的意思是你实际上并没有创建一个供两个人玩的游戏,它只是一个查找玩家可以获得的最高分数的函数。 【参考方案1】:

您的函数根据此输入返回 19,它应该这样做。你从来没有真正考虑过第二个玩家的行为,你只是假设第二个玩家会选择让玩家 1 最大化他们的分数的数字。你应该使用

takeLeftSide = A[0] + (sum(A[1:]) - Recursive(A[1:]))
takeRightSide = A[-1] + (sum(A[:-1]) - Recursive(A[:-1]))

相当于

takeLeftSide = sum(A) - Recursive(A[1:])
takeRightSide = sum(A) - Recursive(A[:-1])

但更容易理解。

这样做是说玩家可以从 1 端获得一个数字,然后其他玩家无法从剩余的数字中获得所有分数。 如果您需要更多信息,或者我不清楚,请查看the minimax algorithm.

【讨论】:

感谢您的回复。我在 [1,2,3] 的输入集上尝试了这个。我相信这个问题的答案应该是 4,因为玩家 1 可以拿 3,玩家 2 拿 2,然后玩家 1 剩下 1,并且 3 + 1 = 4,但是我得到了 5 这个解决方案的回报。不过,我现在正在尝试通读 minimax 算法,所以谢谢 对不起,我犯了一个小错误。玩家从他们选择的数字中获得分数,然后从对手没有获得的剩余数字中获得所有分数。我不小心包含了玩家两次选择的值。我已经编辑了我的答案。 这看起来很不错。对此,我真的非常感激。同时,我将仔细检查更多列表,但到目前为止感谢您 好吧,我不想强​​迫你重新看这个,但我尝试了另一种情况:[20,1,15,9,19]。 P1: 20 P2: 19 P1: 20 + 9 (因为无论如何 P2 都会得到 15) P2: 19 + 15 P1: 29 + 1 我得到 30 的输出,而我真的相信我应该得到 34,来自P2的选择。看起来对吗? P1 在这种情况下得 30 分,P2 得 34 分。目前递归 A 将告诉您 P1 获得的点数。如果你想知道第二个玩家得到多少分,那么你可以从总分中减去 P1 的分数。 p1_score = Recursive(A); p2_score = sum(A) - p1_score; print("Player 1 scored %d points, and player 2 scored %d points."%(p1_score, p2_score))【参考方案2】:

为了让两个玩家“同样聪明”,他们应该在每一轮中使用相同的方法(算法)来选择一方。

为了对其进行编程,您应该根据给定列表开发用于选择一侧(行前或行后)的算法,并为不同的玩家每次调用此算法。

为了演示,我选择了一个比较原始的贪心算法,总是选择两者中较大的一个:

def pick_greedy(A):
    if len(A) == 0:
        result = 0
    elif len(A) == 1:
        result = A.pop()
    elif A[0] > A[-1]:
        result = A[0]
        A = A[1:]
    else:
        result = A[-1]
        A = A[:-1] 
    return A, result


if __name__ == '__main__':
    A = [5,8,1,3,6]
    x = 0
    y = 0
    while A:
        A, _ = pick_greedy(A)
        x += _
        A, _ = pick_greedy(A)
        y += _
        print "Player 1: ; Player 2: ".format(x, y)

输出

Player 1: 6; Player 2: 5
Player 1: 14; Player 2: 8
Player 1: 15; Player 2: 8

【讨论】:

【参考方案3】:

这是我尝试过的代码(使用您的输入)。几乎和你的一模一样。

def r(a):
    if len(a) == 1:
        return a[0]
    takeLeft = a[0] + max(r(a[1:-1]), r(a[2:]))
    takeRight = a[-1] + max(r(a[:-2]), r(a[1:-1]))
    return max(takeLeft, takeRight)

正确答案是 19。

原因是这样的:假设我们正在寻找最高的 POSSIBLE 分数(不是两个人都使用最优策略的那个),那么游戏将如下所示:

P1: 6          Leftover: [5,8,1,3]
P2: 3          Leftover: [5,8,1]
P1: 6 + 5      Leftover: [8,1]
P2: 3 + 1      Leftover: [8]
P1: 6 + 5 + 8  Leftover: []

所以 P1 有 19 个,P2 有 4 个。

编辑:这是使用给定(稍作更改)递归代码时在最佳情况下发生的情况

def r(a):
    if len(a) == 1:
        return a[0]
    if len(a) == 2 or len(a) == 3:
        if a[0] > a[-1]:
            return True
        return False
    takeLeft = a[0] + max(r(a[1:-1]), r(a[2:]))
    takeRight = a[-1] + max(r(a[:-2]), r(a[1:-1]))
    if takeLeft > takeRight:
        return True
    return False

def game(a):
    p1 = 0
    p2 = 0
    p1Turn = True
    while len(a) > 0:
        isLeft = r(a)
        nextValue = -1
        if isLeft: #if it's from the left
            nextValue = a[0]
            a = a[1:]
        else: #if it's from the right
            nextValue = a[-1]
            a = a[:-1]
        if p1Turn:
            p1 += nextValue
        else:
            p2 += nextValue
        p1Turn = not p1Turn
    print "P1:", p1, "P2:", p2

以下是游戏在此场景中的表现:

P1: 6         Leftover: [5, 8, 1, 3]
P2: 5         Leftover: [8, 1, 3]
P1: 6 + 8     Leftover: [1, 3]
P2: 5 + 3     Leftover: [1]
P1: 6 + 8 + 1 Leftover: []

P1: 15 P2: 8

【讨论】:

感谢您的评论,我已经编辑了我的帖子,因为我应该更清楚。事实上,我应该寻找一个玩家的最高分,其中两个玩家都同样聪明,并使用最佳策略来获得最高分。很抱歉造成混乱。 不是想质疑你的答案,但是为什么在P2的第一回合,他们会选择5?难道(理论上)他们不想选择 3 以免打开 P1 选择的 8 吗?我本以为会考虑到策略,并且您想减少自己的积分以防止对手获得更多收益。想法? 你是对的。我想我只是想使用他的递归代码。现在编辑我的描述。 第二步 P2 不会取 5。他会选择 3,然后选择 8。【参考方案4】:

您当前实施的一个问题是,玩家 1 应该获得 min 次调用,而不是最大值(玩家 2 将获得这些调用的最大值)。如果您进行该更改,您将获得预期的结果:12

takeLeftSide  = A[0]  + min(Recursive(A[1:-1]), Recursive(A[2:len(A)]))
takeRightSide = A[-1] + min(Recursive(A[0:-2]), Recursive(A[1:-1]))

您当前实现的其他三个问题:(a)随着输入列表的大小增长非常慢; (b) 如果输入列表有偶数个值,则引发IndexError; (c) 如果输入列表为空,则引发 RuntimeError

但这里有一种不同的方式来思考这个问题:

如果列表中有偶数个值,玩家 1 可以通过决定是取所有具有 偶数索引的值还是所有具有 奇数索引。

如果初始列表中有奇数个值,玩家 1 只需要在左还是右之间做出决定。然后,最初的选择会为玩家 2 创建一个大小相等的列表情况,后者将回退到上述策略。

因此,玩家 1 可以预测玩家 2 的动作,并且应该相应地做出初始的左右选择。

def f(xs):
    evens = sum(xs[i] for i in range(0, len(xs), 2))
    odds  = sum(xs[i] for i in range(1, len(xs), 2))
    if len(xs) % 2 == 0:
        return max(evens, odds)
    else:
        lft = xs[0]
        rgt = xs[-1]
        return max(
            lft + min(odds, evens - lft),
            rgt + min(odds, evens - rgt),
        )

if __name__ == '__main__':
    from random import randint
    tests = [
        (0,     []),
        (5,     [5]),
        (30,    [20, 1, 15, 9, 19]),
        (12,    [5, 8, 1, 3, 6]),
        ('big', [randint(0, 100) for _ in xrange(0, 99999)]),
    ]
    for exp, vals in tests:
        print exp, f(vals)

与递归实现不同,这种方法是O(N),因此具有能够处理非常大的列表的优势。

【讨论】:

以上是关于带有列表的 Python 基于轮次的递归的主要内容,如果未能解决你的问题,请参考以下文章

Python中的递归函数不会更改列表[重复]

python 中的“yield”关键字是如何真正起作用的,尤其是当它带有递归时?

递归遍历带有列表的嵌套字典,并替换匹配的值

python 生成器和递归

查找列表中的最小元素(递归) - Python

Python列表递归更改