O código python mais rápido para encontrar um conjunto de palavras vencedoras neste jogo

14

Este é um jogo de palavras de um conjunto de cartões de atividade para crianças. Abaixo das regras está o código para encontrar o melhor tripleto usando / usr / share / dict / words. Eu pensei que era um problema interessante de otimização e estou me perguntando se as pessoas podem encontrar melhorias.

Regras

  1. Escolha uma letra de cada um dos conjuntos abaixo.
  2. Escolha uma palavra usando as letras escolhidas (e outras).
  3. Marque a palavra.
    • Cada letra do conjunto escolhido obtém o número mostrado com o conjunto (repetições incluídas).
    • AEIOU contagem 0
    • Todas as outras letras são -2
  4. Repita as etapas 1 a 3 acima (sem reutilizar letras na etapa 1) mais duas vezes.
  5. Pontuação final é a soma das pontuações de três palavras.

Conjuntos

(definir 1 pontuação 1 ponto, definir 2 pontuação 2 pontos etc.)

  1. LTN
  2. RDS
  3. GBM
  4. CHP
  5. FWV
  6. YKJ
  7. QXZ

Código:

from itertools import permutations
import numpy as np

points = {'LTN' : 1,
          'RDS' : 2,
          'GBM' : 3,
          'CHP' : 4,
          'FWV' : 5,
          'YKJ' : 6,
          'QXZ' : 7}

def tonum(word):
    word_array = np.zeros(26, dtype=np.int)
    for l in word:
        word_array[ord(l) - ord('A')] += 1
    return word_array.reshape((26, 1))

def to_score_array(letters):
    score_array = np.zeros(26, dtype=np.int) - 2
    for v in 'AEIOU':
        score_array[ord(v) - ord('A')] = 0
    for idx, l in enumerate(letters):
        score_array[ord(l) - ord('A')] = idx + 1
    return np.matrix(score_array.reshape(1, 26))

def find_best_words():
    wlist = [l.strip().upper() for l in open('/usr/share/dict/words') if l[0].lower() == l[0]]
    wlist = [l for l in wlist if len(l) > 4]
    orig = [l for l in wlist]
    for rep in 'AEIOU':
        wlist = [l.replace(rep, '') for l in wlist]
    wlist = np.hstack([tonum(w) for w in wlist])

    best = 0
    ct = 0
    bestwords = ()
    for c1 in ['LTN']:
        for c2 in permutations('RDS'):
            for c3 in permutations('GBM'):
                for c4 in permutations('CHP'):
                    for c5 in permutations('FWV'):
                        for c6 in permutations('YJK'):
                            for c7 in permutations('QZX'):
                                vals = [to_score_array(''.join(s)) for s in zip(c1, c2, c3, c4, c5, c6, c7)]
                                ct += 1
                                print ct, 6**6
                                scores1 = (vals[0] * wlist).A.flatten()
                                scores2 = (vals[1] * wlist).A.flatten()
                                scores3 = (vals[2] * wlist).A.flatten()
                                m1 = max(scores1)
                                m2 = max(scores2)
                                m3 = max(scores3)
                                if m1 + m2 + m3 > best:
                                    print orig[scores1.argmax()], orig[scores2.argmax()], orig[scores3.argmax()], m1 + m2 + m3
                                    best = m1 + m2 + m3
                                    bestwords = (orig[scores1.argmax()], orig[scores2.argmax()], orig[scores3.argmax()])
    return bestwords, best


if __name__ == '__main__':
    import timeit
    print timeit.timeit('print find_best_words()', 'from __main__ import find_best_words', number=1)

A versão matricial é a que eu criei depois de escrever uma em python puro (usando dicionários e marcando cada palavra independentemente) e outra em numpy, mas usando indexação em vez de multiplicação de matrizes.

A próxima otimização seria remover completamente as vogais da pontuação (e usar uma ord()função modificada ), mas me pergunto se existem abordagens ainda mais rápidas.

EDIT : adicionado código timeit.timeit

EDIT : Estou adicionando uma recompensa, que darei a qualquer melhoria que eu mais gostar (ou possivelmente várias respostas, mas terei que acumular mais reputação, se for esse o caso).

tu és
fonte
3
BTW, eu escrevi o código para dar três palavras aos meus oito anos para memorizar quando ele jogou o jogo contra sua mãe. Agora eu sei o que significa xilopirografia.
2
Esta é uma pergunta divertida. Acho que você pode ter mais chances de obter respostas se fornecer o seguinte: (1) Um link para uma lista de palavras on-line para que todos trabalhem com o mesmo conjunto de dados. (2) Coloque sua solução em uma única função. (3) Execute essa função usando o módulo time-it para mostrar os tempos. (4) Certifique-se de colocar o carregamento dos dados do dicionário fora da função para que não testemos a velocidade do disco. As pessoas podem usar o código existente como uma estrutura para comparar suas soluções.
Vou reescrever para usar o timeit, mas, para comparações justas, tenho que usar minha própria máquina (o que fico feliz em fazer para as pessoas que postam soluções). Uma lista de palavras deve estar disponível na maioria dos sistemas, mas, se não houver, existem várias aqui: wordlist.sourceforge.net
1
Comparações justas podem ser feitas se cada usuário cronometrar sua solução e quaisquer outras soluções postadas contra as próprias em sua própria máquina. Haverá algumas diferenças entre plataformas, mas, em geral, esse método funciona.
1
Humm, nesse caso, eu me pergunto se esse é o site correto. Eu acho que SO teria sido o melhor ajuste.
JoeyAbr

Respostas:

3

Usando a idéia de Keith de pré-computar a melhor pontuação possível para cada palavra, consegui reduzir o tempo de execução para cerca de 0,7 segundos no meu computador (usando uma lista de 75.288 palavras).

O truque é passar pelas combinações de palavras a serem reproduzidas, em vez de todas as combinações de letras escolhidas. Podemos ignorar todas, exceto algumas combinações de palavras (203 usando minha lista de palavras), porque elas não conseguem uma pontuação maior do que a que já encontramos. Quase todo o tempo de execução é gasto na pré-computação de pontuações de palavras.

Python 2.7:

import collections
import itertools


WORDS_SOURCE = '../word lists/wordsinf.txt'

WORDS_PER_ROUND = 3
LETTER_GROUP_STRS = ['LTN', 'RDS', 'GBM', 'CHP', 'FWV', 'YKJ', 'QXZ']
LETTER_GROUPS = [list(group) for group in LETTER_GROUP_STRS]
GROUP_POINTS = [(group, i+1) for i, group in enumerate(LETTER_GROUPS)]
POINTS_IF_NOT_CHOSEN = -2


def best_word_score(word):
    """Return the best possible score for a given word."""

    word_score = 0

    # Score the letters that are in groups, chosing the best letter for each
    # group of letters.
    total_not_chosen = 0
    for group, points_if_chosen in GROUP_POINTS:
        letter_counts_sum = 0
        max_letter_count = 0
        for letter in group:
            if letter in word:
                count = word.count(letter)
                letter_counts_sum += count
                if count > max_letter_count:
                    max_letter_count = count
        if letter_counts_sum:
            word_score += points_if_chosen * max_letter_count
            total_not_chosen += letter_counts_sum - max_letter_count
    word_score += POINTS_IF_NOT_CHOSEN * total_not_chosen

    return word_score

def best_total_score(words):
    """Return the best score possible for a given list of words.

    It is fine if the number of words provided is not WORDS_PER_ROUND. Only the
    words provided are scored."""

    num_words = len(words)
    total_score = 0

    # Score the letters that are in groups, chosing the best permutation of
    # letters for each group of letters.
    total_not_chosen = 0
    for group, points_if_chosen in GROUP_POINTS:
        letter_counts = []
        # Structure:  letter_counts[word_index][letter] = count
        letter_counts_sum = 0
        for word in words:
            this_word_letter_counts = {}
            for letter in group:
                count = word.count(letter)
                this_word_letter_counts[letter] = count
                letter_counts_sum += count
            letter_counts.append(this_word_letter_counts)

        max_chosen = None
        for letters in itertools.permutations(group, num_words):
            num_chosen = 0
            for word_index, letter in enumerate(letters):
                num_chosen += letter_counts[word_index][letter]
            if num_chosen > max_chosen:
                max_chosen = num_chosen

        total_score += points_if_chosen * max_chosen
        total_not_chosen += letter_counts_sum - max_chosen
    total_score += POINTS_IF_NOT_CHOSEN * total_not_chosen

    return total_score


def get_words():
    """Return the list of valid words."""
    with open(WORDS_SOURCE, 'r') as source:
        return [line.rstrip().upper() for line in source]

def get_words_by_score():
    """Return a dictionary mapping each score to a list of words.

    The key is the best possible score for each word in the corresponding
    list."""

    words = get_words()
    words_by_score = collections.defaultdict(list)
    for word in words:
        words_by_score[best_word_score(word)].append(word)
    return words_by_score


def get_winning_words():
    """Return a list of words for an optimal play."""

    # A word's position is a tuple of its score's index and the index of the
    # word within the list of words with this score.
    # 
    # word played: A word in the context of a combination of words to be played
    # word chosen: A word in the context of the list it was picked from

    words_by_score = get_words_by_score()
    num_word_scores = len(words_by_score)
    word_scores = sorted(words_by_score, reverse=True)
    words_by_position = []
    # Structure:  words_by_position[score_index][word_index] = word
    num_words_for_scores = []
    for score in word_scores:
        words = words_by_score[score]
        words_by_position.append(words)
        num_words_for_scores.append(len(words))

    # Go through the combinations of words in lexicographic order by word
    # position to find the best combination.
    best_score = None
    positions = [(0, 0)] * WORDS_PER_ROUND
    words = [words_by_position[0][0]] * WORDS_PER_ROUND
    scores_before_words = []
    for i in xrange(WORDS_PER_ROUND):
        scores_before_words.append(best_total_score(words[:i]))
    while True:
        # Keep track of the best possible combination of words so far.
        score = best_total_score(words)
        if score > best_score:
            best_score = score
            best_words = words[:]

        # Go to the next combination of words that could get a new best score.
        for word_played_index in reversed(xrange(WORDS_PER_ROUND)):
            # Go to the next valid word position.
            score_index, word_chosen_index = positions[word_played_index]
            word_chosen_index += 1
            if word_chosen_index == num_words_for_scores[score_index]:
                score_index += 1
                if score_index == num_word_scores:
                    continue
                word_chosen_index = 0

            # Check whether the new combination of words could possibly get a
            # new best score.
            num_words_changed = WORDS_PER_ROUND - word_played_index
            score_before_this_word = scores_before_words[word_played_index]
            further_points_limit = word_scores[score_index] * num_words_changed
            score_limit = score_before_this_word + further_points_limit
            if score_limit <= best_score:
                continue

            # Update to the new combination of words.
            position = score_index, word_chosen_index
            positions[word_played_index:] = [position] * num_words_changed
            word = words_by_position[score_index][word_chosen_index]
            words[word_played_index:] = [word] * num_words_changed
            for i in xrange(word_played_index+1, WORDS_PER_ROUND):
                scores_before_words[i] = best_total_score(words[:i])
            break
        else:
            # None of the remaining combinations of words can get a new best
            # score.
            break

    return best_words


def main():
    winning_words = get_winning_words()
    print winning_words
    print best_total_score(winning_words)

if __name__ == '__main__':
    main()

Isso retorna a solução ['KNICKKNACK', 'RAZZMATAZZ', 'POLYSYLLABLES']com uma pontuação de 95. Com as palavras da solução de Keith adicionadas à lista de palavras, obtenho o mesmo resultado que ele. Com a "xilopirografia" de thyis adicionada, recebo ['XYLOPYROGRAPHY', 'KNICKKNACKS', 'RAZZMATAZZ']uma pontuação de 105.

flornquake
fonte
5

Aqui está uma idéia - você pode evitar a verificação de muitas palavras observando que a maioria das palavras tem pontuações horríveis. Digamos que você tenha encontrado uma jogada de pontuação muito boa, com 50 pontos. Então, qualquer jogada com mais de 50 pontos deve ter uma palavra de pelo menos teto (51/3) = 17 pontos. Portanto, qualquer palavra que não possa gerar 17 pontos pode ser ignorada.

Aqui está um código que faz o acima. Calculamos a melhor pontuação possível para cada palavra no dicionário e a armazenamos em uma matriz indexada por pontuação. Em seguida, usamos essa matriz para verificar apenas as palavras com a pontuação mínima exigida.

from itertools import permutations
import time

S={'A':0,'E':0,'I':0,'O':0,'U':0,
   'L':1,'T':1,'N':1,
   'R':2,'D':2,'S':2,
   'G':3,'B':3,'M':3,
   'C':4,'H':4,'P':4,
   'F':5,'W':5,'V':5,
   'Y':6,'K':6,'J':6,
   'Q':7,'X':7,'Z':7,
   }

def best_word(min, s):
    global score_to_words
    best_score = 0
    best_word = ''
    for i in xrange(min, 100):
        for w in score_to_words[i]:
            score = (-2*len(w)+2*(w.count('A')+w.count('E')+w.count('I')+w.count('O')+w.count('U')) +
                      3*w.count(s[0])+4*w.count(s[1])+5*w.count(s[2])+6*w.count(s[3])+7*w.count(s[4])+
                      8*w.count(s[5])+9*w.count(s[6]))
            if score > best_score:
                best_score = score
                best_word = w
    return (best_score, best_word)

def load_words():
    global score_to_words
    wlist = [l.strip().upper() for l in open('/usr/share/dict/words') if l[0].lower() == l[0]]
    score_to_words = [[] for i in xrange(100)]
    for w in wlist: score_to_words[sum(S[c] for c in w)].append(w)
    for i in xrange(100):
        if score_to_words[i]: print i, len(score_to_words[i])

def find_best_words():
    load_words()
    best = 0
    bestwords = ()
    for c1 in permutations('LTN'):
        for c2 in permutations('RDS'):
            for c3 in permutations('GBM'):
            print time.ctime(),c1,c2,c3
                for c4 in permutations('CHP'):
                    for c5 in permutations('FWV'):
                        for c6 in permutations('YJK'):
                            for c7 in permutations('QZX'):
                                sets = zip(c1, c2, c3, c4, c5, c6, c7)
                                (s1, w1) = best_word((best + 3) / 3, sets[0])
                                (s2, w2) = best_word((best - s1 + 2) / 2, sets[1])
                                (s3, w3) = best_word(best - s1 - s2 + 1, sets[2])
                                score = s1 + s2 + s3
                                if score > best:
                                    best = score
                                    bestwords = (w1, w2, w3)
                                    print score, w1, w2, w3
    return bestwords, best


if __name__ == '__main__':
    import timeit
    print timeit.timeit('print find_best_words()', 'from __main__ import find_best_words', number=1)

A pontuação mínima chega rapidamente a 100, o que significa que precisamos considerar apenas 33+ palavras em pontos, que é uma fração muito pequena do total geral (a minha /usr/share/dict/wordspossui 208662 palavras válidas, apenas 1723 das quais 33+ são pontos = 0,8%). É executado em cerca de meia hora na minha máquina e gera:

(('MAXILLOPREMAXILLARY', 'KNICKKNACKED', 'ZIGZAGWISE'), 101)
Keith Randall
fonte
Agradável. Acrescentarei isso à solução da matriz (removendo as palavras, pois a pontuação delas cai muito baixa), mas isso é significativamente melhor do que qualquer uma das soluções python puras que eu já havia encontrado.
thouis
1
Não sei se já vi tantos aninhados para loops antes.
Peter Olson
1
A combinação da sua ideia com a pontuação da matriz (e um limite superior mais apertado na melhor pontuação possível) reduz o tempo para cerca de 80 segundos na minha máquina (aproximadamente uma hora). código aqui
thyis
Boa parte desse tempo está na pré-computação das melhores pontuações possíveis, que poderiam ser feitas muito mais rapidamente.
thouis