Schnellster Python-Code, um eine Reihe von erfolgreichen Wörtern in diesem Spiel zu finden

14

Dies ist ein Wortspiel aus einem Satz Aktivitätskarten für Kinder. Unterhalb der Regeln befindet sich Code, um das beste Triplet mit / usr / share / dict / words zu finden. Ich dachte, es sei ein interessantes Optimierungsproblem und frage mich, ob die Leute Verbesserungen finden können.

Regeln

  1. Wählen Sie jeweils einen Buchstaben aus den folgenden Sätzen.
  2. Wählen Sie ein Wort mit den gewählten Buchstaben (und allen anderen).
  3. Bewerte das Wort.
    • Jeder Buchstabe aus dem gewählten Satz erhält die Nummer, die mit dem Satz angezeigt wird (Wiederholungen eingeschlossen).
    • AEIOU 0 zählen
    • Alle anderen Buchstaben sind -2
  4. Wiederholen Sie die obigen Schritte 1 bis 3 (keine weiteren Buchstaben in Schritt 1) ​​noch zweimal.
  5. Die Endnote ist die Summe der drei Wortnoten.

Setzt

(Set 1 ergibt 1 Punkt, Set 2 ergibt 2 Punkte usw.)

  1. LTN
  2. RDS
  3. GBM
  4. KWK
  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)

Die Matrix-Version ist das, was ich gefunden habe, nachdem ich eine in reinem Python (mit Wörterbüchern und unabhängig voneinander bewerteten Wörtern) und eine andere in Numpy geschrieben habe, aber mit Indexierung statt Matrix-Multiplikation.

Die nächste Optimierung wäre, die Vokale vollständig aus der Wertung zu entfernen (und eine modifizierte ord()Funktion zu verwenden), aber ich frage mich, ob es noch schnellere Ansätze gibt.

EDIT : hinzugefügt timeit.timeit Code

BEARBEITEN : Ich füge ein Kopfgeld hinzu, das ich für jede Verbesserung gebe, die ich am meisten mag (oder möglicherweise für mehrere Antworten, aber ich muss mehr Ansehen aufbauen, wenn das der Fall ist).

thouis
quelle
3
Übrigens, ich habe den Code geschrieben, um meinem Achtjährigen drei Wörter zu geben, die er sich merken sollte, als er das Spiel gegen seine Mutter spielte. Jetzt weiß ich, was Xylopyrographie bedeutet.
2
Das ist eine lustige Frage. Ich denke, Sie könnten die Wahrscheinlichkeit erhöhen, dass Sie Antworten erhalten, wenn Sie Folgendes bereitstellen: (1) Einen Link zu einer Online-Wortliste, sodass jeder mit demselben Datensatz arbeitet. (2) Stellen Sie Ihre Lösung in eine einzige Funktion. (3) Führen Sie diese Funktion mit dem time-it-Modul aus, um die Timings anzuzeigen. (4) Stellen Sie sicher, dass das Laden von Wörterbuchdaten außerhalb der Funktion erfolgt, damit die Festplattengeschwindigkeit nicht geprüft wird. Die Benutzer können dann den vorhandenen Code als Rahmen für den Vergleich ihrer Lösungen verwenden.
Ich werde umschreiben, um timeit zu verwenden, aber für faire Vergleiche müsste ich meine eigene Maschine verwenden (was ich gerne für Leute mache, die Lösungen posten). Eine Wortliste sollte auf den meisten Systemen verfügbar sein, aber wenn nicht, gibt es hier einige: wordlist.sourceforge.net
1
Faire Vergleiche sind möglich, wenn jeder Benutzer Ihre Lösung und alle anderen veröffentlichten Lösungen auf seinem eigenen Computer miteinander vergleicht. Plattformübergreifend wird es einige Unterschiede geben, aber im Allgemeinen funktioniert diese Methode.
1
Hm, in diesem Fall frage ich mich, ob dies die richtige Seite ist. Ich denke SO wäre die beste Lösung gewesen.
Joey

Antworten:

3

Durch die Idee von Keith, die bestmögliche Punktzahl für jedes Wort vorab zu berechnen, gelang es mir, die Ausführungszeit auf meinem Computer auf etwa 0,7 Sekunden zu verkürzen (unter Verwendung einer Liste von 75.288 Wörtern).

Der Trick besteht darin, die zu spielenden Wortkombinationen anstelle aller ausgewählten Buchstabenkombinationen durchzugehen. Wir können alle außer einigen Wortkombinationen ignorieren (203 unter Verwendung meiner Wortliste), da sie keine höhere Punktzahl erzielen können, als wir bereits gefunden haben. Fast die gesamte Ausführungszeit wird für die Vorberechnung von Wortnoten aufgewendet.

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

Dies gibt die Lösung ['KNICKKNACK', 'RAZZMATAZZ', 'POLYSYLLABLES']mit einer Punktzahl von 95 zurück. Mit den Wörtern aus Keiths Lösung, die der Wortliste hinzugefügt wurden, erhalte ich dasselbe Ergebnis wie er. Mit Thouis '"Xylopyrographie" bekomme ich ['XYLOPYROGRAPHY', 'KNICKKNACKS', 'RAZZMATAZZ']eine Punktzahl von 105.

Flornbeben
quelle
5

Hier ist eine Idee - Sie können vermeiden, viele Wörter zu überprüfen, indem Sie feststellen, dass die meisten Wörter schreckliche Noten haben. Angenommen, Sie haben ein ziemlich gutes Punktespiel gefunden, das Ihnen 50 Punkte einbringt. Dann muss jedes Spiel mit mehr als 50 Punkten ein Wort von mindestens ceil (51/3) = 17 Punkten haben. Daher kann jedes Wort, das unmöglich 17 Punkte generiert, ignoriert werden.

Hier ist ein Code, der die oben genannten Aufgaben ausführt. Wir berechnen die bestmögliche Punktzahl für jedes Wort im Wörterbuch und speichern sie in einem Array, das nach Punktzahl indiziert ist. Dann verwenden wir dieses Array, um nur Wörter zu überprüfen, die die erforderliche Mindestpunktzahl haben.

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)

Die Mindestpunktzahl steigt schnell auf 100, was bedeutet, dass wir nur Wörter mit mehr als 33 Punkten berücksichtigen müssen, was einen sehr kleinen Bruchteil der Gesamtsumme darstellt (my /usr/share/dict/wordshat 208662 gültige Wörter, von denen nur 1723 Punkte = 0,8% sind). Läuft in ca. einer halben Stunde auf meinem Rechner und generiert:

(('MAXILLOPREMAXILLARY', 'KNICKKNACKED', 'ZIGZAGWISE'), 101)
Keith Randall
quelle
Nett. Ich werde das zur Matrix-Lösung hinzufügen (Wörter entfernen, da ihre Punktzahl zu niedrig ist), aber das ist bedeutend besser als jede der reinen Python-Lösungen, die ich mir ausgedacht habe.
du bist
1
Ich bin mir nicht sicher, ob ich jemals gesehen habe, dass so viele für Schleifen verschachtelt sind.
Peter Olson
1
Durch die Kombination Ihrer Idee mit der Matrixbewertung (und einer engeren Obergrenze für die bestmögliche Punktzahl) wird die Zeit auf meinem Computer auf etwa 80 Sekunden (von ungefähr einer Stunde) verkürzt. Code hier
du bist
Ein guter Teil dieser Zeit liegt in der Vorberechnung der bestmöglichen Ergebnisse, die viel schneller erzielt werden könnten.
Du bist