Algoritmo para classificar palavras para níveis de dificuldade do forca como “Fácil”, “Médio” ou “Difícil”


114

Qual é um bom algoritmo para determinar a "dificuldade" de uma palavra em um jogo da forca, de modo que o jogo possa selecionar palavras que correspondam a um nível de dificuldade especificado?

A dificuldade parece estar relacionada ao número de suposições necessárias, à frequência relativa de uso de letras (por exemplo, palavras com muitas letras incomuns podem ser mais difíceis de adivinhar) e, potencialmente, ao comprimento da palavra.

Existem também alguns fatores subjetivos para (tentar) compensar, como a probabilidade de uma palavra estar no vocabulário do jogador e ser reconhecida, permitindo passar de uma estratégia de adivinhação baseada apenas nas frequências das letras para uma adivinhação baseada na lista de palavras correspondentes conhecidas.

Minha tentativa por enquanto está abaixo em rubi. Alguma sugestão sobre como melhorar a categorização?

def classify_word(w)
  n = w.chars.to_a.uniq.length # Num. unique chars in w
  if n < 5 and w.length > 4
    return WordDifficulty::Easy
  end
  if n > w.length / 2
    return WordDifficulty::Hard
  else
    return WordDifficulty::Medium
  end
end

Estou escrevendo um jogo da forca que gostaria que meus filhos brincassem; Estou muito velho para tentar "lição de casa", e pode ser por isso que a pergunta está recebendo tantos votos negativos ... As palavras são tiradas aleatoriamente de grandes bancos de dados de palavras, que incluem muitas palavras obscuras, e estão sendo filtradas pelo nível de dificuldade determinado para a palavra.

Respostas:


91

1. Introdução

Esta é uma maneira de abordar esse problema sistematicamente: se você tem um algoritmo que joga bem o enforcado, pode considerar a dificuldade de cada palavra como o número de suposições erradas que seu programa aceitaria se adivinhasse essa palavra.

2. Além da estratégia da forca

Há uma ideia que está implícita em algumas das outras respostas e comentários, de que a estratégia ideal para o solucionador seria basear suas decisões na frequência das letras em inglês ou na frequência das palavras em algum corpus. Essa é uma ideia sedutora, mas não está bem certa. O solucionador se sai melhor se modelar com precisão a distribuição de palavras escolhidas pelo setter , e um setter humano pode muito bem estar escolhendo palavras com base em sua raridade ou evitar letras usadas com frequência. Por exemplo, embora Eé a letra mais frequentemente utilizados em Inglês, se o setter escolhe sempre a partir das palavras JUGFUL, RHYTHM, SYZYGY, e ZYTHUM, em seguida, um solucionador perfeito não começar por adivinhar E!

A melhor abordagem para modelar o setter depende do contexto, mas acho que algum tipo de inferência indutiva bayesiana funcionaria bem em um contexto onde o solucionador joga muitos jogos contra o mesmo setter ou contra um grupo de setters semelhantes.

3. Um algoritmo da forca

Aqui, delinearei um solucionador que é muito bom (mas longe de ser perfeito). Ele modela o setter escolhendo palavras uniformemente de um dicionário fixo. É um algoritmo guloso : a cada estágio ele adivinha a letra que minimiza o número de erros, ou seja, palavras que não contêm o palpite. Por exemplo, se nenhuma suposição foi feita até agora, e as palavras possíveis são DEED, DEADe DARE, então:

  • se você adivinhar Dou E, não há erros;
  • se você adivinhar A, há um miss ( DEED);
  • se você adivinhar R, há dois erros ( DEEDe DEAD);
  • se você adivinhar qualquer outra letra, há três erros.

Portanto, Dou Eé um bom palpite nesta situação.

(Obrigado ao Coronel Panic nos comentários por apontar que as suposições corretas são gratuitas no carrasco - esqueci-me totalmente disso na minha primeira tentativa!)

4. Implementação

Esta é uma implementação desse algoritmo em 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. Resultados de exemplo

Usando essa estratégia, é possível avaliar a dificuldade de adivinhar cada palavra de uma coleção. Aqui, considero as palavras de seis letras no dicionário do meu 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))

As palavras mais fáceis de adivinhar neste dicionário (junto com a sequência de adivinhações necessárias para que o solucionador as adivinhe) são as seguintes:

>>> 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 as palavras mais difíceis são estas:

>>> 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')]

A razão de serem difíceis é porque, depois de adivinhar -UZZLE, você ainda tem sete possibilidades restantes:

>>> ' '.join(sorted(w for w in six_letter_words if w.endswith('uzzle')))
'buzzle guzzle muzzle nuzzle puzzle tuzzle wuzzle'

6. Escolha da lista de palavras

É claro que, ao preparar listas de palavras para seus filhos, você não começaria com o dicionário do sistema do seu computador, mas sim com uma lista de palavras que acha que eles provavelmente conhecerão. Por exemplo, você pode dar uma olhada nas listas do Wikcionário das palavras mais usadas em vários corpora em inglês.

Por exemplo, entre as 1.700 palavras de seis letras nas 10.000 palavras mais comuns no Project Gutenberg em 2006 , as dez mais difíceis são estas:

[(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 é um personagem da Saga Forsyte de John Galsworthy ; a lista de palavras foi convertida para minúsculas, então não foi possível remover nomes próprios rapidamente.)


21

Uma maneira realmente simples seria calcular uma pontuação com base na falta de vogais na palavra, o número de letras únicas e a comunhão de cada letra:

letters = 'etaoinshrdlcumwfgypbvkjxqz'
vowels = set('aeiou')

def difficulty(word):
    unique = set(word)
    positions = sum(letters.index(c) for c in word)

    return len(word) * len(unique) * (7 - len(unique & vowels)) * positions

words = ['the', 'potato', 'school', 'egypt', 'floccinaucinihilipilification']

for word in words:
    print difficulty(word), word

E a saída:

432 the
3360 potato
7200 school
7800 egypt
194271 floccinaucinihilipilification

Você poderia então pontuar as palavras com:

        score < 2000   # Easy
 2000 < score < 10000  # Medium
10000 < score          # Hard

9

Você pode usar o Método de Monte Carlo para estimar a dificuldade de uma palavra:

  • Simule um jogo adivinhando uma letra aleatória a cada vez, ponderada pela frequência da letra em seu idioma de destino, e conte quantas tentativas seu jogador randomizado levou para chegar a uma solução. Observe que, como cada estimativa elimina uma letra, esse processo é finito e retorna um número de 1 a 26, inclusive.
  • Repita este processo 2*Nvezes, onde Nestá o número de letras exclusivas em sua palavra,
  • Calcule a pontuação calculando a média dos resultados das 2*Ncorridas,
  • Determine o nível de complexidade: pontuações abaixo de dez indicam uma palavra fácil e pontuações acima de dezesseis indicam uma palavra difícil; tudo o mais é médio.


3

Um tempo atrás eu escrevi um solucionador de enforcamento usando o algoritmo óbvio: dado um dicionário inicial de todas as palavras possíveis, em cada turno nós escolhemos a letra que ocorre com o maior número de palavras restantes no dicionário, em seguida, removemos as palavras não correspondentes (dependendo do resposta) do dicionário.

O algoritmo não é tão simples assim, já que geralmente há várias letras, cada uma delas ocorrendo no mesmo número de palavras no dicionário. Nesse caso, a escolha da letra pode fazer uma diferença significativa em quantos palpites são necessários para uma palavra. Escolhemos os máximos em que a informação resultante sobre a colocação daquela letra (se está de fato na palavra) fornece o máximo de informação sobre o sistema (a carta com a entropia de informação máxima ). por exemplo, se as duas palavras restantes possíveis são 'enciclopédia' e 'enciclopédica', a letra 'c' tem a mesma probabilidade de aparecer como e, n, y, l, o, p, e, d, i (ou seja, é garantido estar na palavra), mas devemos perguntar sobre 'c' primeiro, uma vez que tem uma entropia de informação diferente de zero.

Fonte (C ++, GPL) está aqui

O resultado de tudo isso é uma lista de palavras, com o número de suposições necessárias para cada uma: dificuldade.txt (630KB). A palavra mais difícil para esse algoritmo encontrar é "vontade" (com 14 tentativas erradas); o i e duplo l são adivinhados muito rapidamente, mas as opções incluem bill, dill, fill, gill, hill, kill, mill, pill, rill, till, will, e a partir de então a única opção é adivinhar cada letra em virar. Um tanto contra-intuitivamente, palavras mais longas são adivinhadas muito mais rapidamente (simplesmente não há muitas delas para escolher).

Claro, em um jogo humano de carrasco, a psicologia (e a amplitude do vocabulário) desempenham um papel muito maior do que esse algoritmo explica ...


3

Apenas faça! Jogue forca contra a palavra. Conte quantas perdas (ou seja, palpites incorretos) são necessários para vencer.

Você precisará de uma estratégia para jogar. Aqui está uma estratégia humana (ish). No dicionário, elimine todas as palavras que não se encaixam nas revelações até agora. Adivinhe a letra mais frequente entre as palavras restantes.

Se sua estratégia for aleatória, você pode definir sua medida como o número esperado de perdas e estimá-lo empiricamente.


Outra estratégia determinística, de um bot da forca que escrevi alguns anos atrás. Adivinhe a letra que minimiza o número de palavras restantes no caso de a estimativa estar incorreta (ou seja, otimizar o pior caso). Hoje não gosto dessa estratégia por ser muito mecânica, prefiro a acima.


2

Primeiro, é claro, você geraria uma lista de letras exclusivas. Em seguida, classifique por frequência (em inglês ou qualquer outro idioma - existem listas para isso ), com letras menos frequentes tendo uma dificuldade maior.

Em seguida, você precisa decidir se combina as pontuações adicionando, multiplicando ou usando algum outro esquema.


1

Você está sendo reprovado porque está nos pedindo para construir um algoritmo muito complexo para você.

Por que você simplesmente não cria três arrays (fácil, médio e difícil) e preenche cada um com cerca de cem palavras? Isso levaria cerca de 20 minutos.

Eu prometo que seus filhos ficarão entediados com o enforcado muito antes de queimarem algumas centenas de jogos ...: D


1

Bem, potencialmente pode haver muitas coisas envolvidas:

  1. Como todos disseram, a frequência das letras individuais;
  2. O comprimento de uma palavra definitivamente deve contar, mas não de forma linear - uma palavra longa pode fazer com que suposições aleatórias acertem as letras, enquanto uma palavra curta pode ser difícil de obter;
  3. Além disso, as próprias palavras devem ser consideradas - "bipartido" pode ser uma palavra para pessoas no SO, mas talvez não para pessoas não técnicas.

Na verdade, você poderia tentar coevoluir várias estratégias , metade delas para decidir o valor de uma palavra e a outra metade para tentar ganhar o jogo. O último grupo tentará maximizar a pontuação enquanto o primeiro tentará minimizar a pontuação. Depois de um tempo, pode haver um padrão e então a metade para decidir o valor de uma palavra pode fornecer alguns parâmetros de referência.


1

Comece com uma lista de palavras e inicie uma pesquisa no Google para cada uma. Deixe o O número de acessos servir como um proxy (grosso) da dificuldade do termo.

Em uma versão refinada, você agruparia palavras por um sinônimo Relation Based on a Thesaurus e determinaria a palavra mais difícil de uma categoria contando os resultados de pesquisas no Google.

Levando a noção de n-gramas Um passo adiante, a dificuldade de uma palavra poderia ser avaliada pela frequência de suas sílabas em prosa. Depende da qualidade das estatísticas de sílaba, é claro. Você provavelmente teria que diferenciar entre lexemes e palavras funcionais (determinantes, conjunções etc.) e normalizar pelo número de sílabas na palavra (parece exagero enquanto escrevo ...).


0

Gosto da ideia de construir um algoritmo que aprende e muda dependendo dos usuários. No início, você pode implementar qualquer um dos algoritmos sugeridos para fazer a lista, então, conforme mais pessoas jogam, você atribui um peso a cada uma das palavras dependendo do número de suposições (que também é continuamente rastreado e calculado ) Isso evita que palavras complexas, mas populares, sejam classificadas como difíceis, mas que sejam conhecidas pelas pessoas.


0

Calcule o valor de cada letra de uma palavra em pontos do Scrabble: E = 1, D = 2, V = 4, X = 8 e assim por diante. Some-os e divida pelo número de letras para obter um valor médio de letra e use-o para pontuar a palavra. Calcule a média de cada palavra em um grande dicionário e determine os pontos de quebra entre os quartis. Chame as palavras do quartil inferior de "fácil", as palavras dos dois quartis intermediários de "médio" e as palavras do quartil superior de "difícil".

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.