栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 面试经验 > 面试问答

如何解决“策划者”猜谜游戏?

面试问答 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

如何解决“策划者”猜谜游戏?

关键工具:熵,贪婪,分支定界;Python,生成器,itertools,decorate-unecorate模式

在回答这个问题时,我想建立一种有用的函数语言来探讨这个问题。我将介绍这些功能,并描述它们及其意图。最初,这些工具具有广泛的文档,并且使用doctest对小型嵌入式单元测试进行了测试;我不能高度称赞这种方法是实现测试驱动开发的绝妙方法。但是,它不能很好地转换为StackOverflow,因此我不会以这种方式呈现。

首先,我将需要几个标准模块和 将来的 导入(我使用Python 2.6)。

from __future__ import division # No need to cast to float when dividingimport collections, itertools, math

我将需要一个计分功能。最初,这返回了一个元组(黑色,白色),但是如果我使用namedtuple,我发现输出会更加清晰:

Pegs = collections.namedtuple('Pegs', 'black white')def mastermindScore(g1,g2):  matching = len(set(g1) & set(g2))  blacks = sum(1 for v1, v2 in itertools.izip(g1,g2) if v1 == v2)  return Pegs(blacks, matching-blacks)

为了使我的解决方案具有通用性,我将与Mastermind问题相关的任何内容作为关键字参数传入。因此,我制作了一个函数,可以一次创建这些参数,并使用**
kwargs语法传递它。这也使我可以在以后需要时轻松添加新属性。注意,我允许猜测包含重复,但限制对手选择不同的颜色。要更改此设置,我只需要在下面更改G。(如果我想允许重复对手的秘密,我也需要更改计分功能。)

def mastermind(colours, holes):  return dict(    G= set(itertools.product(colours,repeat=holes)),    V= set(itertools.permutations(colours, holes)),    score       = mastermindScore,    endstates   = (Pegs(holes, 0),))def mediumGame():    return mastermind(("Yellow", "Blue", "Green", "Red", "Orange", "Purple"), 4)

有时,我需要根据将函数应用于集合中每个元素的结果对集合进行 分区
。例如,可以通过函数n%2将数字1..10划分为偶数和奇数(奇数为1,偶数为0)。以下函数返回这样的分区,该分区实现为从函数调用结果到给出该结果的元素集的映射(例如{0:偶数,1:赔率})。

def partition(S, func, *args, **kwargs):  partition = collections.defaultdict(set)  for v in S: partition[func(v, *args, **kwargs)].add(v)  return partition

我决定探索一种使用 贪婪熵方法
的求解器。在每个步骤中,它都会计算可以从每个可能的猜测中获得的信息,并选择信息最丰富的猜测。随着可能性的增加,这会严重(按比例)扩展,但让我们尝试一下!首先,我需要一种方法来计算一组概率的熵(信息)。这只是-∑p
log p。但是,为方便起见,我将允许未规范化的输入,即不加1:

def entropy(P):  total = sum(P)  return -sum(p*math.log(p, 2) for p in (v/total for v in P if v))

那么我将如何使用此功能?好吧,对于给定的一组可能性V和给定的猜测g,我们从该猜测中获得的信息只能来自评分函数:更具体地说,该评分函数如何划分我们的可能性。我们想做出一个猜测,以在其余的可能性中最好地区分-
将它们分成最大数量的小集合-
因为这意味着我们离答案更近了。这正是上面的熵函数所要赋予的数字:大量小集合的得分将高于少数大集合的得分。我们需要做的就是深入研究。

def decisionEntropy(V, g, score):  return entropy(collections.Counter(score(gi, g) for gi in V).values())

当然,在任何给定步骤中,我们实际上将拥有一组剩余的可能性V和我们可以做出的一组可能的猜测G,我们将需要选择使熵最大化的猜测。另外,如果几个猜测具有相同的熵,则最好选择一个也可能是有效的解决方案。这保证了该方法将终止。我使用标准的python
decorate-undecorate模式以及内置的max方法来执行此操作:

def bestDecision(V, G, score):  return max((decisionEntropy(V, g, score), g in V, g) for g in G)[2]

现在,我要做的就是重复调用此函数,直到猜出正确的结果为止。我经历了该算法的多种实现,直到发现一个似乎正确的实现。我的几个职能部门希望以不同的方式来实现这一目标:一些职能部门列举所有可能的决策顺序(每个对手可能做出的猜测),而其他职能只对穿过树的单个路径感兴趣(如果对手已经选择了)一个秘密,而我们只是试图达成解决方案)。我的解决方案是“懒树”,其中树的每个部分都是可以评估与否的生成器,从而使用户可以避免不需要的昂贵计算。我还最终使用了另外两个namedtuple,再次是为了使代码清晰。

Node = collections.namedtuple('Node', 'decision branches')Branch = collections.namedtuple('Branch', 'result subtree')def lazySolutionTree(G, V, score, endstates, **kwargs):  decision = bestDecision(V, G, score)  branches = (Branch(result, None if result in endstates else        lazySolutionTree(G, pV, score=score, endstates=endstates))   for (result, pV) in partition(V, score, decision).iteritems())  yield Node(decision, branches) # Lazy evaluation

以下函数根据提供的评分函数评估通过该树的单个路径:

def solver(scorer, **kwargs):  lazyTree = lazySolutionTree(**kwargs)  steps = []  while lazyTree is not None:    t = lazyTree.next() # evaluate node    result = scorer(t.decision)    steps.append((t.decision, result))    subtrees = [b.subtree for b in t.branches if b.result == result]    if len(subtrees) == 0:      raise Exception("No solution possible for given scores")    lazyTree = subtrees[0]  assert(result in endstates)  return steps

现在,可以使用它来构建Mastermind的交互式游戏,在该游戏中,用户可以对计算机的猜测进行评分。玩转这会发现一些有趣的事情。例如,信息最丰富的第一种猜测的形式为(黄色,黄色,蓝色,绿色),而不是(黄色,蓝色,绿色,红色)。仅使用一半的可用颜色即可获得额外的信息。这也适用于6色3孔策划者(黄色,蓝色,绿色)和8色5孔策划者(黄色,黄色,蓝色,绿色,红色)。

但是,仍有许多问题无法通过交互式求解器轻松解决。例如,贪婪熵方法最多需要多少步骤?多少输入采取了这么多步骤?为了使回答这些问题更加容易,我首先提供了一个简单的函数,该函数将上面的惰性树变成通过该树的一组路径,即,针对每个可能的秘密,列出猜测和分数。

def allSolutions(**kwargs):  def solutions(lazyTree):    return ((((t.decision, b.result),) + solution  for t in lazyTree for b in t.branches  for solution in solutions(b.subtree)) if lazyTree else ((),))  return solutions(lazySolutionTree(**kwargs))

找到最坏的情况很容易找到最长的解决方案:

def worstCaseSolution(**kwargs):  return max((len(s), s) for s in allSolutions(**kwargs)) [1]

事实证明,该求解器将始终以5个步骤或更少的步骤完成。五步!我知道,小时候玩Mastermind的时间通常比这更长。但是,由于创建了此求解器并进行了尝试,我极大地改进了我的技术,即使您没有时间在每一步上计算熵理想猜测值,也确实可以实现5步目标;)

求解器将采取5个步骤的可能性有多大?它会以1步或2步完成吗?为了找出答案,我创建了另一个简单的小函数来计算解长度分布:

def solutionLengthDistribution(**kwargs):  return collections.Counter(len(s) for s in allSolutions(**kwargs))

对于贪婪的熵方法,允许重复:7个案例分2个步骤;55个案例分3个步骤;229例,分4步;69个案例最多需要5个步骤。

当然,不能保证贪婪的熵方法将最坏情况下的步骤数减到最少。我的通用语言的最后一部分是一种算法,用于确定给定最坏情况下的边界是否有 任何
解决方案。这将告诉我们贪婪的熵是否理想。为此,我采用了分支定界策略:

def solutionExists(maxsteps, G, V, score, **kwargs):  if len(V) == 1: return True  partitions = [partition(V, score, g).values() for g in G]  maxSize = max(len(P) for P in partitions) ** (maxsteps - 2)  partitions = (P for P in partitions if max(len(s) for s in P) <= maxSize)  return any(all(solutionExists(maxsteps-1,G,s,score) for l,s in      sorted((-len(s), s) for s in P)) for i,P in  sorted((-entropy(len(s) for s in P), P) for P in partitions))

这绝对是一个复杂的功能,因此需要更多说明。第一步是像以前一样,根据猜测后的剩余分数对其余解决方案进行分区,但是这次我们不知道我们要做出什么样的猜测,因此我们存储所有分区。现在,我们
可以
递归到其中的每一个,有效地枚举整个可能的决策树,但这将花费很长的时间。相反,我观察到,如果在这一点上没有分区将其余解决方案划分为n个以上的集,那么在以后的任何步骤中也都不会存在此类分区。如果我们还有k步,那意味着我们最多可以区分n
k-1解决方案,然后再用完猜测(在最后一步,我们必须始终正确猜测)。因此,我们可以丢弃任何包含得分的分区,这些得分映射到的解决方案不止这些。这是接下来的两行代码。

最后的代码行进行递归,使用Python的所有函数进行清晰说明,并首先尝试最大熵的决策,以期在肯定的情况下最大程度地减少运行时间。它也首先递归到分区的最大部分,因为如果决策错误,这很可能会很快失败。再次,我使用标准的decorate-
unecorate模式,这一次包装了Python的 排序 函数。

def lowerBoundonWorstCaseSolution(**kwargs):  for steps in itertools.count(1):    if solutionExists(maxsteps=steps, **kwargs):      return steps

通过以越来越多的步骤反复调用solutionExists,我们获得了在最坏情况下Mastermind解决方案所需的步骤数量的严格下限:5个步骤。贪婪的熵方法确实是最佳的。

出于好奇,我发明了另一个猜谜游戏,我将其昵称为“
twoD”。在这种情况下,您尝试猜测一对数字。在每一步中,您都会被告知答案是否正确,猜中的数字不小于密码中对应的数字以及数字是否大于零。

Comparison = collections.namedtuple('Comparison', 'less greater equal')def twoDScorer(x, y):  return Comparison(all(r[0] <= r[1] for r in zip(x, y)),         all(r[0] >= r[1] for r in zip(x, y)),         x == y)def twoD():  G = set(itertools.product(xrange(5), repeat=2))  return dict(G = G, V = G, score = twoDScorer,   endstates = set(Comparison(True, True, True)))

对于这个游戏,贪婪的熵方法在最坏的情况下为五个步骤,但在最坏的情况下为四个步骤,则可能有更好的解决方案,这证实了我的直觉,即近视贪婪仅是巧合策划者的理想选择。更重要的是,这表明我的语言具有多大的灵活性:对于这个新的猜谜游戏,所有相同的方法都可以像对Mastermind一样工作,这使我可以用最少的额外编码来探索其他游戏。

性能如何?显然,以Python实现时,该代码的运行速度不会很快。我还放弃了一些可能的优化,以使用清晰的代码。

一种便宜的优化方法是,首先观察到,大多数猜测基本上都是相同的:(黄色,蓝色,绿色,红色)与(蓝色,红色,绿色,黄色)或(橙色,黄色,红色)确实没有区别。
,紫色)。这大大减少了我们在第一步中需要考虑的猜测数量,否则将是游戏中最昂贵的决定。

但是,由于此问题的运行时增长率很高,因此即使进行了这种优化,我也无法解决8色5孔Mastermind问题。取而代之的是,我将算法移植到C
++,使通用结构保持不变,并采用按位运算来提高关键内部循环中的性能,从而加快了多个数量级的速度。我把这留给读者练习:)

附录,2018年: 事实证明,贪婪熵方法对于8色4孔Mastermind问题也不是最佳选择,当算法最多需要6个步骤时,最坏情况下需要7步!



转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/633610.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号