Python 如何解决这个问题;智囊团;猜谜游戏?

Python 如何解决这个问题;智囊团;猜谜游戏?,python,algorithm,Python,Algorithm,您将如何创建一个算法来解决以下难题“智囊团” 你的对手从六种颜色中选择了四种不同的颜色(黄色、蓝色、绿色、红色、橙色、紫色)。你必须猜出他们选择了哪一个,顺序是什么。每次猜测后,你的对手都会告诉你,你猜到的颜色中有多少(但不是哪一种)在正确的位置[“黑色”]是正确的颜色,有多少(但不是哪一种)在错误的位置[“白色”]。游戏结束时,你猜对了(4个黑人,0个白人) 例如,如果你的对手选择了(蓝色、绿色、橙色、红色),你猜(黄色、蓝色、绿色、红色),你将得到一个“黑色”(红色),两个白色(蓝色和绿色)

您将如何创建一个算法来解决以下难题“智囊团”

你的对手从六种颜色中选择了四种不同的颜色(黄色、蓝色、绿色、红色、橙色、紫色)。你必须猜出他们选择了哪一个,顺序是什么。每次猜测后,你的对手都会告诉你,你猜到的颜色中有多少(但不是哪一种)在正确的位置[“黑色”]是正确的颜色,有多少(但不是哪一种)在错误的位置[“白色”]。游戏结束时,你猜对了(4个黑人,0个白人)

例如,如果你的对手选择了(蓝色、绿色、橙色、红色),你猜(黄色、蓝色、绿色、红色),你将得到一个“黑色”(红色),两个白色(蓝色和绿色)。你猜得到的分数相同(蓝色、橙色、红色、紫色)

我感兴趣的是您将选择什么算法,以及(可选)如何将其转换为代码(最好是Python)。我对以下编码解决方案感兴趣:

  • 清晰(容易理解)
  • 简明的
  • 高效(猜得快)
  • 有效(解决难题的猜测次数最少)
  • 灵活(可以轻松回答有关算法的问题,例如,最坏的情况是什么?)
  • 一般(可以很容易地适应除智囊团以外的其他类型的谜题)
  • 我对一个非常有效但效率不高的算法感到满意(前提是它不仅实现得很差!);然而,一个非常高效的算法是没有用的


    我已经发布了我自己的(详细的)Python解决方案,但这绝不是唯一或最好的方法,所以请发布更多!我不期待有一篇文章;)

    关键工具:熵、贪婪性、分枝定界;Python、生成器、itertools、装饰非装饰图案

    在回答这个问题时,我想建立一种有用函数的语言来探索这个问题。我将介绍这些函数,描述它们及其意图。最初,它们有大量的文档,使用doctest测试小型嵌入式单元测试;作为一种实现测试驱动开发的出色方式,我对这种方法论的评价再高不过了。但是,它不能很好地转换为StackOverflow,因此我将不以这种方式介绍它

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

    我需要一个评分函数。最初,它返回一个元组(黑色、白色),但如果使用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)
    
    为了使我的解决方案更具概括性,我将特定于智囊团问题的任何内容作为关键字参数传入。因此,我创建了一个函数,创建这些参数一次,并使用**kwargs语法传递它。这还允许我在以后需要时轻松添加新属性。注意,我允许猜测包含重复,但限制对手选择不同的颜色;要改变这个,我只需要改变下面的G。(如果我想在对手的秘密中允许重复,我还需要更改得分函数。)

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

    我决定探索一种使用贪婪熵方法的求解器。在每一步中,它都会计算从每个可能的猜测中获得的信息,并选择信息量最大的猜测。随着可能性的增加,这将严重扩展(二次),但让我们尝试一下!首先,我需要一种方法来计算一组概率的熵(信息)。这只是-∑对数。但是,为了方便起见,我将允许未规范化的输入,即不加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装饰取消装饰模式和内置的max方法来完成这项工作:

    def bestDecision(V, G, score):
      return max((decisionEntropy(V, g, score), g in V, g) for g in G)[2]
    
    现在我需要做的就是反复调用这个函数,直到猜到正确的结果。我研究了这个算法的许多实现,直到找到一个似乎正确的。我的几个函数将以不同的方式处理此问题:一些函数列举所有可能的决策序列(对手可能做出的每个猜测一个),而另一些函数只对树中的一条路径感兴趣(如果对手已经选择了一个秘密,而我们只是试图找到解决方案)。我的解决方案是一个“懒惰树”,树的每一部分都是一个生成器,可以进行评估,也可以不进行评估,这样用户就可以避免他们不需要的昂贵计算。我还使用了另外两个命名的DTU
    def entropy(P):
      total = sum(P)
      return -sum(p*math.log(p, 2) for p in (v/total for v in P if v))
    
    def decisionEntropy(V, g, score):
      return entropy(collections.Counter(score(gi, g) for gi in V).values())
    
    def bestDecision(V, G, score):
      return max((decisionEntropy(V, g, score), g in V, g) for g in G)[2]
    
    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
    
    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]
    
    def solutionLengthDistribution(**kwargs):
      return collections.Counter(len(s) for s in allSolutions(**kwargs))
    
    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))
    
    def lowerBoundOnWorstCaseSolution(**kwargs):
      for steps in itertools.count(1):
        if solutionExists(maxsteps=steps, **kwargs):
          return steps
    
    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)))
    
    def score(this, that):
        '''Simple "Master Mind" scoring function'''
        exact = len([x for x,y in zip(this, that) if x==y])
        ### Calculating "other" (white pegs) goes here:
        ### ...
        ###
        return (exact,other)
    
    other = 0
    x = sorted(this)   ## Implicitly converts to a list!
    y = sorted(that)
    while len(x) and len(y):
        if x[0] == y[0]:
            other += 1
            x.pop(0)
            y.pop(0)
        elif x[0] < y[0]:
            x.pop(0)
        else:
            y.pop(0)
    other -= exact
    
    other = 0
    counters = dict()
    for i in this:
        counters[i] = counters.get(i,0) + 1
    for i in that:
        if counters.get(i,0) > 0:
            other += 1
            counters[i] -= 1
    other -= exact
    
    from itertools import product, tee
    from random import choice
    
    COLORS = 'red ', 'green', 'blue', 'yellow', 'purple', 'pink'#, 'grey', 'white', 'black', 'orange', 'brown', 'mauve', '-gap-'
    HOLES = 4
    
    def random_solution():
        """Generate a random solution."""
        return tuple(choice(COLORS) for i in range(HOLES))
    
    def all_solutions():
        """Generate all possible solutions."""
        for solution in product(*tee(COLORS, HOLES)):
            yield solution
    
    def filter_matching_result(solution_space, guess, result):
        """Filter solutions for matches that produce a specific result for a guess."""
        for solution in solution_space:
            if score(guess, solution) == result:
                yield solution
    
    def score(actual, guess):
        """Calculate score of guess against actual."""
        result = []
        #Black pin for every color at right position
        actual_list = list(actual)
        guess_list = list(guess)
        black_positions = [number for number, pair in enumerate(zip(actual_list, guess_list)) if pair[0] == pair[1]]
        for number in reversed(black_positions):
            del actual_list[number]
            del guess_list[number]
            result.append('black')
        #White pin for every color at wrong position
        for color in guess_list:
            if color in actual_list:
                #Remove the match so we can't score it again for duplicate colors
                actual_list.remove(color)
                result.append('white')
        #Return a tuple, which is suitable as a dictionary key
        return tuple(result)
    
    def minimal_eliminated(solution_space, solution):
        """For solution calculate how many possibilities from S would be eliminated for each possible colored/white score.
        The score of the guess is the least of such values."""
        result_counter = {}
        for option in solution_space:
            result = score(solution, option)
            if result not in result_counter.keys():
                result_counter[result] = 1
            else:
                result_counter[result] += 1
        return len(solution_space) - max(result_counter.values())
    
    def best_move(solution_space):
        """Determine the best move in the solution space, being the one that restricts the number of hits the most."""
        elim_for_solution = dict((minimal_eliminated(solution_space, solution), solution) for solution in solution_space)
        max_elimintated = max(elim_for_solution.keys())
        return elim_for_solution[max_elimintated]
    
    def main(actual = None):
        """Solve a game of mastermind."""
        #Generate random 'hidden' sequence if actual is None
        if actual == None:
            actual = random_solution()
    
        #Start the game of by choosing n unique colors
        current_guess = COLORS[:HOLES]
    
        #Initialize solution space to all solutions
        solution_space = all_solutions()
        guesses = 1
        while True:
            #Calculate current score
            current_score = score(actual, current_guess)
            #print '\t'.join(current_guess), '\t->\t', '\t'.join(current_score)
            if current_score == tuple(['black'] * HOLES):
                print guesses, 'guesses for\t', '\t'.join(actual)
                return guesses
    
            #Restrict solution space to exactly those hits that have current_score against current_guess
            solution_space = tuple(filter_matching_result(solution_space, current_guess, current_score))
    
            #Pick the candidate that will limit the search space most
            current_guess = best_move(solution_space)
            guesses += 1
    
    if __name__ == '__main__':
        print max(main(sol) for sol in all_solutions())
    
    import random
    
    
    def main():
    
        userAns = raw_input("Enter your tuple, and I will crack it in six moves or less: ")
        play(ans=eval("("+userAns+")"),guess=(0,0,0,0),previousGuess=[])
    
    def play(ans=(6,1,3,5),guess=(0,0,0,0),previousGuess=[]):
    
        if(guess==(0,0,0,0)):
           guess = genGuess(guess,ans)
        else:
            checker = -1
            while(checker==-1):
                guess,checker = genLogicalGuess(guess,previousGuess,ans)
    
        print guess, ans
    
    
        if not(guess==ans):
            previousGuess.append(guess)
    
            base = check(ans,guess)
    
            play(ans=ans,guess=base,previousGuess=previousGuess)
    
        else:
            print "Found it!"
    
    
    
    
    
    def genGuess(guess,ans):
        guess = []
        for i in range(0,len(ans),1):
            guess.append(random.randint(1,6))
    
        return tuple(guess)
    
    def genLogicalGuess(guess,previousGuess,ans):
        newGuess = list(guess)
        count = 0
    
        #Generate guess
    
        for i in range(0,len(newGuess),1):
            if(newGuess[i]==-1):
                newGuess.insert(i,random.randint(1,6))
                newGuess.pop(i+1)
    
    
        for item in previousGuess:
            for i in range(0,len(newGuess),1):
                if((newGuess[i]==item[i]) and (newGuess[i]!=ans[i])):
                    newGuess.insert(i,-1)
                    newGuess.pop(i+1)
                    count+=1
    
        if(count>0):
            return guess,-1
        else:
            guess = tuple(newGuess)
            return guess,0
    
    
    def check(ans,guess):
        base = []
        for i in range(0,len(zip(ans,guess)),1):
            if not(zip(ans,guess)[i][0] == zip(ans,guess)[i][1]):
                base.append(-1)
            else:
                base.append(zip(ans,guess)[i][1])
    
        return tuple(base)
    
    main()
    
    import random
    from itertools import izip, imap
    
    digits = 4
    fmt = '%0' + str(digits) + 'd'
    searchspace = tuple([tuple(map(int,fmt % i)) for i in range(0,10**digits)])
    
    def compare(a, b, imap=imap, sum=sum, izip=izip, min=min):
        count1 = [0] * 10
        count2 = [0] * 10
        strikes = 0
        for dig1, dig2 in izip(a,b):
            if dig1 == dig2:
                strikes += 1
            count1[dig1] += 1
            count2[dig2] += 1
        balls = sum(imap(min, count1, count2)) - strikes
        return (strikes, balls)
    
    def rungame(target, strategy, verbose=True, maxtries=15):
        possibles = list(searchspace)
        for i in xrange(maxtries):
            g = strategy(i, possibles)
            if verbose:
                print "Out of %7d possibilities.  I'll guess %r" % (len(possibles), g),
            score = compare(g, target)
            if verbose:
                print ' ---> ', score
            if score[0] == digits:
                if verbose:
                    print "That's it.  After %d tries, I won." % (i+1,)
                break
            possibles = [n for n in possibles if compare(g, n) == score]
        return i+1
    
    def strategy_allrand(i, possibles):
        return random.choice(possibles)
    
    if __name__ == '__main__':
        hidden_code = random.choice(searchspace)
        rungame(hidden_code, strategy_allrand)
    
    Out of   10000 possibilities.  I'll guess (6, 4, 0, 9)  --->  (1, 0)
    Out of    1372 possibilities.  I'll guess (7, 4, 5, 8)  --->  (1, 1)
    Out of     204 possibilities.  I'll guess (1, 4, 2, 7)  --->  (2, 1)
    Out of      11 possibilities.  I'll guess (1, 4, 7, 1)  --->  (3, 0)
    Out of       2 possibilities.  I'll guess (1, 4, 7, 4)  --->  (4, 0)
    That's it.  After 5 tries, I won.
    
    # SET UP
    import random
    import itertools
    colors = ('red', 'orange', 'yellow', 'green', 'blue', 'indigo', 'violet', 'ultra')
    
    # ONE FUNCTION REQUIRED
    def EvaluateCode(guess, secret_code):
        key = []
        for i in range(0, 4):
            for j in range(0, 4):
                if guess[i] == secret_code[j]:
                    key += ['black'] if i == j else ['white']    
        return key
    
    # MAIN CODE
    # choose secret code
    secret_code = random.sample(colors, 4)
    print ('(shh - secret code is: ', secret_code, ')\n', sep='')
    # create the full list of permutations
    full_code_list = list(itertools.permutations(colors, 4))
    N_guess = 0
    while True:
        N_guess += 1
        print ('Attempt #', N_guess, '\n-----------', sep='')
        # make a random guess
        guess = random.choice(full_code_list)
        print ('guess:', guess)
        # evaluate the guess and get the key
        key = EvaluateCode(guess, secret_code)
        print ('key:', key)
        if key == ['black', 'black', 'black', 'black']:
            break
        # remove codes from the code list that don't match the key
        full_code_list2 = []
        for i in range(0, len(full_code_list)):
            if EvaluateCode(guess, full_code_list[i]) == key:
                full_code_list2 += [full_code_list[i]]
        full_code_list = full_code_list2
        print ('N remaining: ', len(full_code_list), '\n', full_code_list, '\n', sep='')
    print ('\nMATCH after', N_guess, 'guesses\n')