1.はじめに
この問題に体系的に取り組む方法を次に示します。絞首刑に上手く機能するアルゴリズムがある場合、各単語の難易度を、その単語を推測する場合にプログラムがとる誤った推測の数とすることができます。
2.絞首刑戦略は別として
他のいくつかの回答とコメントには暗示的である考えがあります。ソルバーの最適な戦略は、英語での文字の頻度、またはコーパスでの単語の頻度に基づいて決定することです。これは魅惑的なアイデアですが、正しくありません。ソルバーは、セッターによって選択された単語の分布を正確にモデル化し、人間のセッターが希少性や頻繁に使用される文字の回避に基づいて単語を選択している場合に最適です。が、例えば、E
英語で最も頻繁に使用される文字である、セッターは常に言葉から選択した場合はJUGFUL
、RHYTHM
、SYZYGY
、およびZYTHUM
、その後、完璧なソルバーは推測して起動しませんE
!
セッターをモデル化するための最良のアプローチはコンテキストに依存しますが、ある種のベイズ帰納推論は、ソルバーが同じセッターまたは同様のセッターのグループに対して多くのゲームをプレイするコンテキストでうまく機能すると思います。
3.絞首刑アルゴリズム
ここでは、かなり良い(しかし完璧とはほど遠い)ソルバーの概要を説明します。固定辞書から一律に単語を選択するセッターをモデル化します。これは貪欲なアルゴリズムです。各段階で、ミスの数を最小限にする文字、つまり、推測を含まない単語を推測します。例えば、全くの推測では、これまで行われていない、と可能な単語がある場合DEED
、DEAD
そしてDARE
その後、:
D
またはを推測した場合E
、ミスはありません。
- 推測すると
A
、1つのミス(DEED
);
- 推測すると
R
、2つのミスがあります(DEED
およびDEAD
);
- 他の文字を推測すると、3つのミスがあります。
したがって、この状況ではどちらD
かE
が適切な推測です。
(ハングマンでは正しい推測が自由であることを指摘してくれたコメントのパニック大佐に感謝します。最初の試みでこれを完全に忘れていました!)
4.実装
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.結果の例
この戦略を使用すると、コレクション内の各単語を推測する難しさを評価できます。ここで、システム辞書の6文字の単語について考えます。
>>> 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))
この辞書で推測する最も簡単な単語は、(ソルバーが推測するために必要な一連の推測と合わせて)次のとおりです。
>>> 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')]
そして最も難しい言葉はこれらです:
>>> 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')]
これらが難しい理由は-UZZLE
、を推測した後でも、7つの可能性が残っているためです。
>>> ' '.join(sorted(w for w in six_letter_words if w.endswith('uzzle')))
'buzzle guzzle muzzle nuzzle puzzle tuzzle wuzzle'
6.単語リストの選択
もちろん、子供用の単語リストを作成するときは、コンピューターのシステム辞書から始めるのではなく、子供が知っていると思われる単語のリストから始めます。たとえば、さまざまな英語のコーパスで最も頻繁に使用される単語のウィクショナリー一覧をご覧ください。
たとえば、2006年現在のProject Gutenbergの最も一般的な10,000の単語のうち1,700の6文字の単語のうち、最も難しい10の単語は次のとおりです。
[(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は、John GalsworthyによるForsyte Sagaのキャラクターです。ワードリストは小文字に変換されているため、適切な名前をすばやく削除することはできませんでした。)
f(w) = (# unique letters) * (7 - # vowels) * (sum of the positions of unique letters in a list, ordered by frequency)
。そこから、関数の範囲を3つのセグメントに分割し、それらを難易度と呼ぶことができます。