Code python le plus rapide pour trouver un ensemble de mots gagnants dans ce jeu

14

Il s'agit d'un jeu de mots à partir d'un jeu de cartes d'activités pour les enfants. Sous les règles se trouve le code pour trouver le meilleur triplet en utilisant / usr / share / dict / words. Je pensais que c'était un problème d'optimisation intéressant et je me demandais si les gens pouvaient trouver des améliorations.

Règles

  1. Choisissez une lettre dans chacun des ensembles ci-dessous.
  2. Choisissez un mot en utilisant les lettres choisies (et toutes les autres).
  3. Marquez le mot.
    • Chaque lettre de l'ensemble choisi obtient le numéro indiqué avec l'ensemble (répétitions incluses).
    • AEIOU compter 0
    • Toutes les autres lettres sont -2
  4. Répétez les étapes 1 à 3 ci-dessus (pas de réutilisation des lettres à l'étape 1) deux fois de plus.
  5. Le score final est la somme des scores de trois mots.

Ensembles

(set 1 marque 1 point, set 2 marque 2 points, etc.)

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

Code:

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)

La version matricielle est ce que j'ai trouvé après avoir écrit une en python pur (en utilisant des dictionnaires et en marquant chaque mot indépendamment), et une autre en numpy mais en utilisant l'indexation plutôt que la multiplication matricielle.

La prochaine optimisation serait de supprimer entièrement les voyelles de la notation (et d'utiliser une ord()fonction modifiée ), mais je me demande s'il existe des approches encore plus rapides.

EDIT : code timeit.timeit ajouté

EDIT : J'ajoute une prime, que je donnerai à l'amélioration que j'aime le plus (ou éventuellement à plusieurs réponses, mais je devrai accumuler un peu plus de réputation si c'est le cas).

tu
la source
3
BTW, j'ai écrit le code pour donner à mon enfant de huit ans trois mots à mémoriser quand il a joué le jeu contre sa mère. Maintenant, je sais ce que signifie la xylopyrographie.
2
C'est une question amusante. Je pense que vous pourriez augmenter la probabilité d'obtenir des réponses si vous fournissez les éléments suivants: (1) Un lien vers une liste de mots en ligne pour que tout le monde travaille avec le même ensemble de données. (2) Mettez votre solution dans une seule fonction. (3) Exécutez cette fonction à l'aide du module time-it pour afficher les horaires. (4) Assurez-vous de mettre le chargement des données du dictionnaire en dehors de la fonction afin de ne pas tester la vitesse du disque. Les gens peuvent ensuite utiliser le code existant comme cadre pour comparer leurs solutions.
Je vais réécrire pour utiliser timeit, mais pour des comparaisons équitables, je devrais utiliser ma propre machine (ce que je suis heureux de faire pour les personnes qui publient des solutions). Une liste de mots devrait être disponible sur la plupart des systèmes, mais sinon, il y a plusieurs ici: wordlist.sourceforge.net
1
Des comparaisons équitables peuvent être effectuées si chaque utilisateur chronomètre votre solution et toute autre solution publiée par rapport à la sienne sur sa propre machine. Il y aura quelques différences multiplateforme, mais en général cette méthode fonctionne.
1
Hm, dans ce cas, je me demande si c'est le bon site. Je pense que SO aurait été la meilleure solution.
Joey

Réponses:

3

En utilisant l'idée de Keith de précalculer le meilleur score possible pour chaque mot, j'ai réussi à réduire le temps d'exécution à environ 0,7 seconde sur mon ordinateur (en utilisant une liste de 75 288 mots).

L'astuce consiste à parcourir les combinaisons de mots à jouer au lieu de toutes les combinaisons de lettres choisies. Nous pouvons ignorer toutes les combinaisons de mots sauf quelques-unes (203 en utilisant ma liste de mots) car ils ne peuvent pas obtenir un score plus élevé que celui que nous avons déjà trouvé. Presque tout le temps d'exécution est consacré au précalcul des scores de mots.

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()

Cela renvoie la solution ['KNICKKNACK', 'RAZZMATAZZ', 'POLYSYLLABLES']avec un score de 95. Avec les mots de la solution de Keith ajoutés à la liste de mots, j'obtiens le même résultat que lui. Avec la "xylopyrographie" de thouis ajoutée, j'obtiens ['XYLOPYROGRAPHY', 'KNICKKNACKS', 'RAZZMATAZZ']un score de 105.

tremblement de terre
la source
5

Voici une idée - vous pouvez éviter de vérifier beaucoup de mots en remarquant que la plupart des mots ont des scores terribles. Disons que vous avez trouvé un jeu de score assez bon qui vous rapporte 50 points. Ensuite, tout jeu avec plus de 50 points doit avoir un mot d'au moins ceil (51/3) = 17 points. Ainsi, tout mot qui ne peut pas générer 17 points peut être ignoré.

Voici un code qui fait ce qui précède. Nous calculons le meilleur score possible pour chaque mot du dictionnaire et le stockons dans un tableau indexé par score. Ensuite, nous utilisons ce tableau pour vérifier uniquement les mots qui ont le score minimum requis.

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)

Le score minimum atteint rapidement 100, ce qui signifie que nous n'avons qu'à prendre en compte plus de 33 points, ce qui représente une très petite fraction du total global (mon /usr/share/dict/wordscompte 208662 mots valides, dont seulement 1723 sont 33+ points = 0,8%). Fonctionne en une demi-heure environ sur ma machine et génère:

(('MAXILLOPREMAXILLARY', 'KNICKKNACKED', 'ZIGZAGWISE'), 101)
Keith Randall
la source
Agréable. J'ajouterai cela à la solution matricielle (en supprimant les mots car leur score tombe trop bas), mais c'est nettement mieux que toutes les solutions python pures que j'avais trouvées.
tu
1
Je ne suis pas sûr d'avoir déjà vu autant de boucles imbriquées auparavant.
Peter Olson
1
La combinaison de votre idée avec la notation matricielle (et une limite supérieure plus stricte sur le meilleur score possible) réduit le temps à environ 80 secondes sur ma machine (environ une heure). code ici
tu
Une bonne partie de ce temps est consacrée au précalcul des meilleurs scores possibles, qui pourraient être beaucoup plus rapides.
tu