1. Introduzione
Ecco un modo per affrontare questo problema in modo sistematico: se hai un algoritmo che interpreta bene l'impiccato, puoi considerare la difficoltà di ogni parola come il numero di ipotesi sbagliate che il tuo programma prenderebbe se indovinasse quella parola.
2. A parte la strategia dell'impiccato
C'è un'idea che è implicita in alcune altre risposte e commenti, che la strategia ottimale per il risolutore sarebbe quella di basare le proprie decisioni sulla frequenza delle lettere in inglese, o sulla frequenza delle parole in qualche corpus. Questa è un'idea seducente, ma non è del tutto corretta. Il risolutore fa meglio se modella accuratamente la distribuzione delle parole scelte dall'incastonatore , e un incastonatore umano potrebbe scegliere le parole in base alla loro rarità o evitare le lettere usate di frequente. Ad esempio, anche se E
è la lettera più frequentemente usato in inglese, se il setter sceglie sempre dalle parole JUGFUL
, RHYTHM
, SYZYGY
, e ZYTHUM
, quindi un risolutore perfetto non inizia da indovinare E
!
L'approccio migliore per modellare il setter dipende dal contesto, ma immagino che una sorta di inferenza induttiva bayesiana funzionerebbe bene in un contesto in cui il risolutore gioca molte partite contro lo stesso setter o contro un gruppo di setter simili.
3. Un algoritmo dell'impiccato
Qui descriverò un risolutore che è abbastanza buono (ma tutt'altro che perfetto). Modella l'incastonatore scegliendo le parole in modo uniforme da un dizionario fisso. È un algoritmo goloso : in ogni fase indovina la lettera che riduce al minimo il numero di errori, cioè le parole che non contengono l'ipotesi. Ad esempio, se finora non sono state fatte ipotesi e le parole possibili sono DEED
, DEAD
e DARE
, allora:
- se indovini
D
o E
, non ci sono errori;
- se indovini
A
, c'è un errore (DEED
);
- se indovini
R
, ci sono due errori ( DEED
e DEAD
);
- se indovini un'altra lettera, ci sono tre errori.
Quindi o D
o E
è una buona ipotesi in questa situazione.
(Grazie al colonnello Panic nei commenti per aver sottolineato che le ipotesi corrette sono libere nel boia: me ne sono completamente dimenticato al primo tentativo!)
4. Implementazione
Ecco un'implementazione di questo algoritmo in Python:
from collections import defaultdict
from string import ascii_lowercase
def partition(guess, words):
"""Apply the single letter 'guess' to the sequence 'words' and return
a dictionary mapping the pattern of occurrences of 'guess' in a
word to the list of words with that pattern.
>>> words = 'deed even eyes mews peep star'.split()
>>> sorted(list(partition('e', words).items()))
[(0, ['star']), (2, ['mews']), (5, ['even', 'eyes']), (6, ['deed', 'peep'])]
"""
result = defaultdict(list)
for word in words:
key = sum(1 << i for i, letter in enumerate(word) if letter == guess)
result[key].append(word)
return result
def guess_cost(guess, words):
"""Return the cost of a guess, namely the number of words that don't
contain the guess.
>>> words = 'deed even eyes mews peep star'.split()
>>> guess_cost('e', words)
1
>>> guess_cost('s', words)
3
"""
return sum(guess not in word for word in words)
def word_guesses(words, wrong = 0, letters = ''):
"""Given the collection 'words' that match all letters guessed so far,
generate tuples (wrong, nguesses, word, guesses) where
'word' is the word that was guessed;
'guesses' is the sequence of letters guessed;
'wrong' is the number of these guesses that were wrong;
'nguesses' is len(guesses).
>>> words = 'deed even eyes heel mere peep star'.split()
>>> from pprint import pprint
>>> pprint(sorted(word_guesses(words)))
[(0, 1, 'mere', 'e'),
(0, 2, 'deed', 'ed'),
(0, 2, 'even', 'en'),
(1, 1, 'star', 'e'),
(1, 2, 'eyes', 'en'),
(1, 3, 'heel', 'edh'),
(2, 3, 'peep', 'edh')]
"""
if len(words) == 1:
yield wrong, len(letters), words[0], letters
return
best_guess = min((g for g in ascii_lowercase if g not in letters),
key = lambda g:guess_cost(g, words))
best_partition = partition(best_guess, words)
letters += best_guess
for pattern, words in best_partition.items():
for guess in word_guesses(words, wrong + (pattern == 0), letters):
yield guess
5. Risultati di esempio
Utilizzando questa strategia è possibile valutare la difficoltà di indovinare ogni parola di una raccolta. Qui considero le parole di sei lettere nel mio dizionario di sistema:
>>> words = [w.strip() for w in open('/usr/share/dict/words') if w.lower() == w]
>>> six_letter_words = set(w for w in words if len(w) == 6)
>>> len(six_letter_words)
15066
>>> results = sorted(word_guesses(six_letter_words))
Le parole più facili da indovinare in questo dizionario (insieme alla sequenza di ipotesi necessarie al risolutore per indovinarle) sono le seguenti:
>>> from pprint import pprint
>>> pprint(results[:10])
[(0, 1, 'eelery', 'e'),
(0, 2, 'coneen', 'en'),
(0, 2, 'earlet', 'er'),
(0, 2, 'earner', 'er'),
(0, 2, 'edgrew', 'er'),
(0, 2, 'eerily', 'el'),
(0, 2, 'egence', 'eg'),
(0, 2, 'eleven', 'el'),
(0, 2, 'enaena', 'en'),
(0, 2, 'ennead', 'en')]
e le parole più dure sono queste:
>>> pprint(results[-10:])
[(12, 16, 'buzzer', 'eraoiutlnsmdbcfg'),
(12, 16, 'cuffer', 'eraoiutlnsmdbpgc'),
(12, 16, 'jugger', 'eraoiutlnsmdbpgh'),
(12, 16, 'pugger', 'eraoiutlnsmdbpcf'),
(12, 16, 'suddle', 'eaioulbrdcfghmnp'),
(12, 16, 'yucker', 'eraoiutlnsmdbpgc'),
(12, 16, 'zipper', 'eraoinltsdgcbpjk'),
(12, 17, 'tuzzle', 'eaioulbrdcgszmnpt'),
(13, 16, 'wuzzer', 'eraoiutlnsmdbpgc'),
(13, 17, 'wuzzle', 'eaioulbrdcgszmnpt')]
Il motivo per cui sono difficili è perché dopo aver indovinato -UZZLE
, hai ancora sette possibilità:
>>> ' '.join(sorted(w for w in six_letter_words if w.endswith('uzzle')))
'buzzle guzzle muzzle nuzzle puzzle tuzzle wuzzle'
6. Scelta della lista di parole
Ovviamente quando prepari elenchi di parole per i tuoi figli non inizieresti con il dizionario di sistema del tuo computer, inizieresti con un elenco di parole che ritieni possano conoscere. Ad esempio, potresti dare un'occhiata agli elenchi di Wikizionario delle parole più usate in vari corpora inglesi.
Ad esempio, tra le 1.700 parole di sei lettere nelle 10.000 parole più comuni nel Progetto Gutenberg del 2006 , le dieci più difficili sono queste:
[(6, 10, 'losing', 'eaoignvwch'),
(6, 10, 'monkey', 'erdstaoync'),
(6, 10, 'pulled', 'erdaioupfh'),
(6, 10, 'slaves', 'erdsacthkl'),
(6, 10, 'supper', 'eriaoubsfm'),
(6, 11, 'hunter', 'eriaoubshng'),
(6, 11, 'nought', 'eaoiustghbf'),
(6, 11, 'wounds', 'eaoiusdnhpr'),
(6, 11, 'wright', 'eaoithglrbf'),
(7, 10, 'soames', 'erdsacthkl')]
(Soames Forsyte è un personaggio della Saga Forsyte di John Galsworthy ; l'elenco di parole è stato convertito in lettere minuscole quindi non è stato possibile per me rimuovere rapidamente i nomi propri.)
f(w) = (# unique letters) * (7 - # vowels) * (sum of the positions of unique letters in a list, ordered by frequency)
. Da lì, puoi semplicemente dividere l'intervallo della funzione in tre segmenti e chiamare quelli le tue difficoltà.