带有列表的 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 基于轮次的递归的主要内容,如果未能解决你的问题,请参考以下文章