【问题标题】:Algorithm for classifying words for hangman difficulty levels as "Easy","Medium", or "Hard"将hangman难度级别的单词分类为“简单”、“中”或“难”的算法
【发布时间】:2013-04-19 20:27:43
【问题描述】:

有什么好的算法可以确定刽子手游戏的单词“难度”,以便游戏可以选择匹配指定难度级别的单词?

难度似乎与所需的猜测次数、字母使用的相对频率(例如,带有许多不常见字母的单词可能更难猜测)以及单词的长度有关。

还有一些主观因素需要(尝试)进行补偿,例如某个单词在玩家词汇表中的可能性,并且可以被识别,从而允许从仅基于字母频率的猜测策略转变为基于猜测的策略在已知匹配词列表中。

我现在的尝试是在 ruby​​ 下面。关于如何改进分类有什么建议吗?

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

我正在写一个我想让我的孩子们玩的刽子手游戏;我太老了,不能尝试“家庭作业”,这可能就是为什么这个问题会收到如此多的反对票...... 单词是从包含许多晦涩单词的大型单词数据库中随机抽取的,并根据为单词确定的难度级别进行过滤。

【问题讨论】:

  • 为什么投反对票?这是一个体面的问题。我会做一个难度函数,比如f(w) = (# unique letters) * (7 - # vowels) * (sum of the positions of unique letters in a list, ordered by frequency)。从那里,您可以将函数的范围分成三个部分,并将它们称为您的困难。
  • 我建议您对此进行网络搜索 - 可能有算法或字典旨在计算/报告单词的复杂性。我知道有更长的文字。
  • 相关:youtube.com/watch?v=bBLm9P-ph6U(QI XL - 刽子手中最难猜的词)
  • 无论你做什么,一定要包括 EXTINCTIONSPECTROPHOTOPOLERISCOPEOCCULOGRAVOGYROKYNETOMETER。
  • 对于可能不熟悉Ruby的用户,也许你想解释一下你的方法的第一行是做什么的? n = w.chars.to_a.uniq.length 计算唯一字母的个数吗?

标签: algorithm


【解决方案1】:

1。简介

这里有一个系统地解决这个问题的方法:如果你有一个能很好地扮演刽子手的算法,那么你可以将每个单词的难度作为你的程序在猜测该单词时的错误猜测次数。

2。除了刽子手策略

在其他一些答案和 cmets 中隐含了一个想法,即求解器的最佳策略是根据英语字母的频率或某些语料库中单词的频率做出决定。这是一个诱人的想法,但它并不完全正确。如果求解器准确地模拟 setter 选择的单词的分布,则求解器会做得最好,而人类 setter 很可能会根据单词的稀有性或避免常用字母来选择单词。例如,虽然E是英语中最常用的字母,但如果setter总是从JUGFULRHYTHMSYZYGYZYTHUM这几个词中进行选择,那么完美的求解器不会从猜测开始E!

对 setter 进行建模的最佳方法取决于上下文,但我想某种贝叶斯归纳推理在求解器与同一个 setter 或一组类似 setter 进行许多游戏的环境中会很好地工作。

3。刽子手算法

在这里,我将概述一个相当不错(但远非完美)的求解器。它将 setter 建模为从固定字典中统一选择单词。这是一个greedy algorithm:在每个阶段,它都会猜测使未命中次数最少的字母,即不包含猜测的单词。例如,如果到目前为止还没有猜到,可能的词是DEEDDEADDARE,那么:

  • 如果您猜到DE,则没有错过;
  • 如果你猜到了A,那就错了一个(DEED);
  • 如果您猜到R,则有两个未命中(DEEDDEAD);
  • 如果您猜到任何其他字母,则有 3 个未命中。

所以在这种情况下,DE 是一个不错的猜测。

(感谢Colonel Panic in comments 指出,刽子手中的正确猜测是免费的——我在第一次尝试时完全忘记了这一点!)

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。示例结果

使用此策略可以评估猜测集合中每个单词的难度。在这里,我考虑了我的系统词典中的六个字母的单词:

>>> 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之后,还有七种可能:

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

6。词表的选择

当然,在为您的孩子准备单词表时,您不会从计算机的系统词典开始,而是从您认为他们可能知道的单词列表开始。例如,您可能会查看各种英语语料库中的Wiktionary's lists of the most frequently used words

例如,10,000 most common words in Project Gutenberg as of 2006 的 1700 个六个字母的单词中,最难的十个是这些:

[(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 是 Forsyte Saga by John Galsworthy 中的一个字符;单词表已转换为小写,因此我无法快速删除专有名称。)

【讨论】:

  • 对常用单词列表的好调用。 invokeit.wordpress.com/frequency-word-lists 有英语和瑞典语,真好。
  • 我希望bingle 的评级比singletingle 更难 - bingle 是一个不太常见的词 并且 b 是一个不太常见的词信
  • 很酷的算法(感谢您在编写代码之前用英文解释!)。但我认为你应该尽量减少 不正确 猜测的数量。因此,如果字典是 [bat, bet, hat, hot, yum],我会猜测“T”(而不是 B、A 或 H)。如果我是对的,它不会花费我任何东西。如果我错了,那么就只剩下“yum”了。
  • 这是一个非常酷的算法,但我认为并不能反映人类玩家可能会采取的策略——人类不会知道每一个单词,而是会(概率地)识别最常见的单词,否则将尝试识别后缀和前缀(如 ion、ing),而失败则仅猜测常见字母(从元音开始,然后执行 t/r/s/n/等)。不知道如何编码,但值得考虑:)
  • 很好的分析。正如@Patashu 指出的那样,下一步要使它变得更好,而不是仅仅使用常用词词典,而是使用完整的词词典,但带有关于共性的注释,并简单地启发式地权衡单词的共性字母分发难度。但这只是可选的改进——这已经是一个很好的解决方案了。
【解决方案2】:

一个非常简单的方法是根据单词中元音的缺失、唯一字母的数量以及每个字母的共同度来计算分数:

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

还有输出:

432 the
3360 potato
7200 school
7800 egypt
194271 floccinaucinihilipilification

然后您可以使用以下方法对单词进行评分:

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

【讨论】:

  • 嗨搅拌机,你能告诉我神奇的数字 7 是干什么用的吗?为什么不是 6 或 50?如果我输入另一个任意号码会怎样?
  • @Pavan:真的没什么。所有单词的分数都会上移相同的数量。
  • 是的,当我在玩在线 python 执行器时,我注意到了这种转变。我注意到了一些事情,那就是当我输入诸如幻想之类的内容时,尽管幻想是一个拼写更正确的单词,因此憎恶的价值会低于幻想,因此在文字游戏中应该以较低的难度出现。这让我意识到难度是主观的,但它让我认为应该进行某种研究来概述哪些单词最难拼写,对吧?你能指点我这样的研究吗?
  • 或者至少像这样的研究会被称为什么,因为我很难找到一个单词集合,而第一次尝试拼写错误的人的百分比 - 这就是我现在的样子之后。
【解决方案3】:

您可以使用Monte Carlo Method来估计一个单词的难度:

  • 通过每次猜测一个随机字母来模拟游戏,根据字母在您的目标语言中的频率加权,并计算您的随机玩家需要多少次猜测才能得出一个解决方案。请注意,由于每次猜测都会消除一个字母,因此此过程是有限的,它会返回一个 1 到 26 之间的数字,包括 1 到 26。
  • 重复此过程2*N 次,其中N 是您单词中唯一 个字母的数量,
  • 通过平均2*N 运行的结果来计算分数,
  • 判断复杂度:分数低于10表示易词,16分以上表示难词;其他一切都是中等的。

【讨论】:

  • 我认为你应该只计算不正确的猜测。正确猜测不会受到惩罚。
  • 为什么会有这么多重复?我认为这种策略(像大多数随机策略一样)对于较短的词有更大的差异。
  • @ColonelPanic 我认为计算猜测的总数更好,因为它自然地将不同字母的数量合并到答案中。您可能对较短单词的方差较高是正确的。也许应该固定重复次数。不过,我认为 2N 会是一个不错的开始。
【解决方案4】:

之前围绕同一主题的类似讨论: Determine the difficulty of an english word

我喜欢链接末尾的答案^。对于儿童刽子手游戏,只需采用类似拼字游戏的方法即可。

为每个字母分配一个点值,然后将这些字母相加。

【讨论】:

  • 这一点,再加上在简单的关卡中避免使用稀有或晦涩的单词,似乎是目前的前进方向。我没有提到的一个问题是这些词是从巨大的字典中选择的,其中大部分必须根据定义是很少使用的词:-)
  • 点值可以工作,可能使用letter frequency。虽然,一些常用词实际上可能具有奇怪的高分值。
【解决方案5】:

不久前,我使用明显的算法编写了一个hangman求解器:给定一个包含所有可能单词的初始字典,在每一轮我们选择出现在字典中剩余最多单词中的字母,然后删除不匹配的单词(取决于响应)从字典中。

该算法并不像这样简单,因为通常有几个字母出现在字典中相同数量的单词中。在这种情况下,字母的选择会对一个单词需要多少次猜测产生重大影响。我们选择最大值,其中有关该字母位置的结果信息(如果确实在单词中)给出了有关系统的最大信息(最大的字母information entropy)。例如如果剩下的两个可能的词是“encyclopedia”和“encyclopedic”,则字母“c”与 e,n,y,l,o,p,e,d,i 出现的概率相同(即保证是在单词中),但我们应该首先询问“c”,因为它具有非零信息熵。

源代码(C++,GPL)是here

所有这些的结果是一个单词列表,每个单词需要猜测的次数:difficulty.txt (630KB)。这个算法最难找到的词是“will”(有 14 次失败的猜测); i 和 double l 被猜得很快,但接下来的选项包括 bill、dill、fill、gill、hill、kill、mill、pill、rill、till、will,从那时起,唯一的选择就是猜中的每个字母转动。有点违反直觉的是,更长的词被猜得更快(只是没有那么多可供选择)。

当然,在人类的刽子手游戏中,心理学(以及词汇量)所起的作用远比该算法所扮演的角色要大得多……

【讨论】:

    【解决方案6】:

    去做吧!玩刽子手反对这个词。数一数要打败多少次没收(即猜错)。

    你需要一个策略来玩。这是一个人类(ish)策略。从字典中,删除迄今为止不符合揭示的所有单词。猜猜剩下的单词中出现频率最高的字母。

    如果您的策略是随机的,您可以将您的衡量标准定义为预期的没收数量,并根据经验进行估计。


    另一个确定性策略,来自我几年前写的hangman bot。在猜测不正确的情况下,猜测使剩余单词数最小的字母(即优化最坏的情况)。今天我不喜欢这种过于机械化的策略,我更喜欢上面的那个。

    【讨论】:

    • 哈哈,我只是想提出同样的建议。但是一个严肃的版本:编写一个简单的机器人,使用一些简单的策略进行猜测,然后对字典中的单词运行一大堆。
    【解决方案7】:

    首先,当然,您会生成一个唯一字母列表。然后按频率排序(用英语或任何语言 - there are lists for this),频率越低的字母难度越高。

    然后您需要决定是通过加法、乘法还是使用其他方案来组合分数。

    【讨论】:

    • (实际上,您可能不需要按频率排序,只需将频率分数累加即可。虽然排序可能提供了更多信息——值得一试看看它是否对你有用。)
    • 您可能想以某种方式解释字母组合——即,如果有 Q,则几乎可以肯定有 U,而 U 更可能出现 Q。因此,例如,将 QU 视为来自频率 POV 的单个字母可能是有意义的。
    【解决方案8】:

    您之所以被否决,是因为您要求我们为您构建一个非常复杂的算法。

    您为什么不创建三个数组(简单、中等和困难)并用一百左右的单词填充每个数组?大约需要 20 分钟。

    我保证你的孩子在玩完几百场游戏之前早就厌倦了刽子手...... :D

    【讨论】:

    • 它不必那么复杂。例如。例如,看看 Blender 的评论。您的回答并没有真正解决核心问题,也不是特别有用。
    • “你为什么不创建三个数组(简单、中等和困难)并用一百左右的单词填充每个数组?”:也称为“通过假设问题是解决问题已经解决”的方法。
    • 批评了,谢谢...我想从学术的角度来看,你是绝对正确的,我的回答没有解决任何问题。但从实际的角度来看,即为您的孩子构建一个刽子手游戏的最简单方法,我的回答确实解决了它,既便宜又快速。
    • @PascalCuoq 或者您可以说这是“通过假设人类更擅长选择合适的列表而不是算法来解决问题”的方法。考虑到提问者想要一个儿童游戏,“帽子、猫、太阳”似乎更好地出现在简单列表中,而“木琴、nought、学校”则出现在困难列表中,即使这些可能以较少的猜测找到平均。
    • @PascalCuoq 如果您能侥幸逃脱,通过一个简单的解决方案绕过一个复杂的问题并没有错。为了好玩而构建复杂的算法也没有错,但简单的解决方案至少值得一提。
    【解决方案9】:

    嗯,可能涉及很多事情:

    1. 正如大家所说,单个字母的频率;
    2. 单词的长度肯定应该计算在内,但不是以线性方式计算的 - 长的单词可以随机猜测到字母,而短的单词可能很难得到;
    3. 此外,应该考虑这些词本身 - “bipartite”可能是 SO 上的人的词,但可能不适用于非技术人员。

    其实你可以try to co-evolve several strategies,一半是因为决定一个词的价值,一半是为了赢得比赛。后一组将尝试最大化分数,而第一组将尝试最小化分数。一段时间后可能会有一个模式,然后决定一个词的价值的一半可能会给你一些基准。

    【讨论】:

    【解决方案10】:

    从单词列表开始,然后为每个单词启动谷歌搜索。让点击次数作为术语难度的(粗略)代理。

    在精炼版本中,您可以根据同义词关系根据同义词对单词进行分组,并通过计算 google 搜索结果来确定类别中最难的单词。

    进一步了解 n-Gram 的概念 一个单词的难度可以通过其在散文中的音节频率来评估。当然,这取决于音节统计的质量。您可能必须区分词素和虚词(限定词、连词等)并根据单词中的音节数进行规范化(感觉就像我写的矫枉​​过正......)。

    【讨论】:

      【解决方案11】:

      我喜欢构建一种根据用户进行学习和更改的算法的想法。一开始,你可以执行任何建议的算法来得出这个列表,然后随着更多的人玩这个游戏,你根据猜测的数量为每个单词分配一个权重(这也是不断跟踪和计算的) )。这可以防止复杂但流行的单词被赋予困难的评级但为人们所熟知的问题。

      【讨论】:

        【解决方案12】:

        计算拼字游戏点中单词的每个字母的值:E=1、D=2、V=4、X=8 等等。将它们相加并除以字母数以获得平均字母值,并使用该值对单词进行评分。计算大型词典中每个单词的平均值,并确定四分位数之间的断点。将最低四分位数的单词称为“容易”,将两个中间四分位数的单词称为“中等”,将最高四分位数的单词称为“困难”。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2019-07-22
          • 2013-08-19
          • 1970-01-01
          • 2022-12-11
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-09-08
          相关资源
          最近更新 更多