在Scrabble中不从一包字母中提取单词的可能性


27

假设您有一个带有n磁贴的袋子,每个磁贴上都有一个字母。有nA以字母“A”,瓷砖nB与“B”,等等,和n通配符”砖(我们有n=nA+nB++nZ+n)。假设您有一本单词数量有限的字典。您可以从袋子中挑选k块瓷砖,而无需更换。给定所选的k图块,您如何计算(或估计)从字典中形成零个单词的概率?

对于不熟悉Scrabble(TM)的用户,可以使用通配符来匹配任何字母。因此,单词[ BOOT ]可以用图块“ B”,“ *”,“ O”,“ T”“拼写”。

为了对问题的规模有一些了解,k很小,例如7,n大约为100,并且字典包含大约100,000个大小为k或更小的单词。

编辑: “形成单词”是指长度不超过的单词k。因此,如果单词[ A ]在字典中,那么即使从包中画出一个“ A”,也可以“形成一个单词”。如果可以假设字典中存在长度为1的单词,则可以大大简化通配符的问题。如果有的话,通配符的任何抽签会自动匹配长度为1个字的单词,因此,人们可以专注于没有通配符的情况。因此,问题的较滑形式在词典中没有1个字母的单词。

另外,我应该明确指出从书包中提取字母的顺序并不重要。人们不必按单词的“正确”顺序绘制字母。


难道不应该是“ 更换就拾取瓷砖”吗?非常有趣的问题。

哎呀 确实应该。
shabbychef

据我记得,Scrabble不允许使用一个字母的单词,因此至少可以解决部分问题;)
nico 2010年

1
@nico很好,但我认为这仅适用于游戏中期。1个字母的单词要么不需要一个人玩一个字母,要么允许一个人在板上的任何地方放置一个字母,这两个单词显然都是不可接受的。但是,我正在考虑开放的举动。实际上,对于那些熟悉Scrabble的人来说,这个问题可以紧凑地表述为:“第一个玩家必须通过的概率是多少?”
shabbychef

@nico谢谢您的澄清。从理论上讲,类似的问题与包含所有可能的两个字母的组合作为单词的字典有关:在这种情况下,任何2个或更多字母的手都会自动包含一个单词。@shabbychef对游戏中期的评论表明,最初的问题与大部分Scrabble无关,因为在游戏中期,除了您的7个字母外,您还可以使用一系列单词部分(前缀,后缀甚至中间部分)手。这极大地增加了说单词的机会。
Whuber

Answers:


14

这是对@vqv在此线程中发布的好文章的(长时间!)评论。它旨在获得明确的答案。他为简化字典做了艰苦的工作。剩下的就是充分利用它。他的结果表明,强力解决方案是可行的。毕竟,包括一个通配符,最多有的话可以用7个字符做,它看起来像小于1/10000人-比如,围绕一百万-意志无法包含一些有效词。 277=10,460,353,203

第一步是用通配符“?”扩充最小词典。22个字母以两个字母的单词出现(除c,q,v,z以外的所有字母)。将通配符附加到这22个字母并将它们添加到字典中:{a?,b?,d?,...,y?}现在已经存在。类似地,我们可以检查最小的三个字母的单词,从而导致一些其他单词出现在字典中。最后,我们添加“ ??” 到字典。删除导致的重复后,它包含342个最小单词。

一种行之有效的方法(确实使用很少的编码)是将这个问题视为代数方法。一个单词,被认为是一组无序的字母,只是一个单项式。例如,“ spats”是单项式。因此,该词典是单项词典的集合。看起来像aps2t

{a2,ab,ad,...,ozψ,wxψ,ψ2}

(在这里,为避免混淆,我为通配符写了)。ψ

当且仅当该单词将机架分开时,机架才包含有效单词。

一种更抽象但非常有力的说法是字典在多项式环R = Z [ a b z ψ ]中生成一个理想,并且带有有效词的机架在商中变为零环R / I,而没有有效字的齿条在商中保持非零。如果我们在R中形成所有机架的总和并在此商环中对其进行计算,则无词的机架数量等于商中不同单项式的数量。IR=Z[a,b,,z,ψ]R/IR

此外,中所有机架的总和很容易表达。令α = a + b + + z + ψ为字母表中所有字母的总和。 α 7包含用于每个机架一个单项。(作为一个额外的好处,它的系数计算每个机架的形成方式的数量,允许我们根据需要计算其可能性。)Rα=a+b++z+ψα7

作为一个简单的示例(以了解其工作原理),假设(a)我们不使用通配符,并且(b)从“ a”到“ x”的所有字母都被视为单词。这样,唯一无法形成单词的架子必须完全由y和z组成。我们以{ a b c x }一次生成的理想为模,计算,因此:α=(a+b+c++x+y+z)7{a,b,c,,x}

α0=1α1=a+b+c++x+y+zy+zmodIα2(y+z)(a+b++y+z)(y+z)2modIα7(y+z)6(a+b++y+z)(y+z)7modI.

我们可以从最终答案读出获得非单词机架的机会:每个系数都会计算出相应机架的绘制方式。例如,由于的系数等于21 ,所以有21种(在26 ^ 7种可能的方式中)绘制2个y和5个z的方式。y 2 z 5y7+7y6z+21y5z2+35y4z3+35y3z4+21y2z5+7yz6+z7y2z5

从基本计算来看,这显然是正确的答案。整个问题在于,无论字典的内容如何,​​此过程均有效。

请注意,在每个阶段降低理想模的功耗是如何减少计算的:这就是这种方法的捷径。(示例结束。)

多项式代数系统执行这些计算。例如,这是Mathematica代码:

alphabet =  a + b + c + d + e + f + g + h + i + j + k + l + m + n + o + 
            p + q + r + s + t + u + v + w + x + y + z + \[Psi];
dictionary = {a^2, a b, a d, a e, ..., w z \[Psi], \[Psi]^2};
next[pp_] := PolynomialMod[pp alphabet, dictionary];
nonwords = Nest[next, 1, 7];
Length[nonwords]

(可以通过@vqv的min.dict以直接的方式构造字典;我在这里放一行,表明它足够短,可以根据需要直接指定。)

输出(需要十分钟的计算时间)是577958。(注意:在此消息的早期版本中,我在准备字典时犯了一个小错误,获得了577940。我已经编辑了文本以反映我现在希望的内容。正确的结果!)比我预期的数百万少一点,但数量级相同。

为了计算获得这种支架的机会,我们需要考虑可以绘制支架的方式数量。正如我们在示例中看到的,这等于系数。绘制的机会了一些这样的机架是所有这些系数,通过将所有的字母很容易找到的等于1的总和:α7

nonwords /. (# -> 1) & /@ (List @@ alphabet)

答案等于1066056120,这样就有机会抽出10.1914%的机架,根据该机架无法形成有效的单词(如果所有字母的可能性相同)。

当字母的概率不同时,只需将每个字母替换为可绘制的机会即可:

tiles = {9, 2, 2, 4, 12, 2, 3, 2, 9, 1, 1, 4, 2, 6, 8, 2, 1, 6, 4, 6, 
         4, 2, 2, 1, 2, 1, 2};
chances = tiles / (Plus @@ tiles);
nonwords /. (Transpose[{List @@ alphabet, chances}] /. {a_, b_} -> a -> b)

输出为1.079877553303%,即确切的答案(尽管使用的是近似模型,但替换图)。往回看,花了四行输入数据(字母,字典和字母频率),只花了三行即可完成工作:描述如何取模的下一个幂,递归取第7幂,然后代入字母的概率。αI


+1毗邻词典,然后将其最小化是一个聪明的主意。代数超出了我的范围,但是感觉就像您是在计算多项式概率,而不是超几何。因此,概率是通过替换进行采样。我认为这可以解释您为何1.08%的回答比我估计的0.4%大得多。有没有一种方法可以修改您的方法来处理采样而无需更换?
vqv 2011年

2
@vqv是的。现在我们有了五十万个不带字的机架列表,可以很容易地(通过更改最后两行代码)来计算每个机架的机会(无需替换)并获得超几何结果。确切的答案等于349870667877/80678106432000 = 0.43366%。在N = 100K次试验中,您的SE为0.021%,因此您的答案应该介于0.38%和0.49%之间(双面99%CI)。我很高兴我们的回答同意!
ub

@whuber您可以使用“与朋友共处的单词(WWF)”图块分发来运行计算吗?我对0.4%的估计是基于WWF词典和WWF切片分布。我认为您在WWF词典中使用了Scrabble磁贴分布。
vqv 2011年

哎呀。确切的答案实际上是349870675899(由于字典中的错误,我的成绩是8022。)幸运的是,这没有实际区别。
whuber

@vqv我不熟悉各种图块分布。我直接从您的代码中复制了我的代码(并且使用了词典):-)。如果您的意思是osxreality.com/2010/01/01/…上的分布,那么我将获得1.15444%(带替换),0.43366%(不带替换)。第二个数字实际上与第8位有效数字的拼字游戏频率不同。
Whuber

14

在Scrabble及其变体中绘制一个不包含任何有效单词的机架非常困难。下面是我编写的R程序,用于估计最初的7排架子不含有效字词的可能性。它使用了蒙特卡洛方法和Words With Friends词典(我找不到以简单格式显示的官方Scrabble词典)。每个试验包括绘制一个7位数的架子,然后检查架子是否包含有效词。

最少的字

您不必扫描整个词典即可检查机架中是否包含有效单词。您只需要扫描包含 最少单词的最少词典即可。如果一个单词不包含其他单词作为子集,则该单词是最小的。例如,“ em”是一个最小的单词;“空”不是。这样做的目的是,如果机架包含单词x,则它还必须包含x的任何子集。换句话说:如果机架不包含最少的单词,则不包含单词。幸运的是,词典中的大多数单词不是最小的,因此可以将其消除。您还可以合并置换等效词。我能够将Words With Friends词典从172,820个减少到201个最小单词。

通过将机架和单词视为字母上的分布,可以轻松处理通配符。我们通过从另一个分配中减去一个分配来检查一个机架是否包含一个单词。这给了我们机架上缺少的每个字母的数量。如果这些数字的总和为通配符数,则单词在机架中。

蒙特卡洛方法的唯一问题是,我们感兴趣的事件非常罕见。因此,需要进行许多次试验才能获得具有足够小的标准误差的估计值。我运行了试验的程序(粘贴在底部), 并获得了0.004估计概率,即初始机架不包含有效字词。该估计的估计标准误差为0.0002。在我的Mac Pro上运行只花了几分钟,包括下载词典。N=100,000

我很想看看是否有人可以提出一种有效的精确算法。基于包含-排除的幼稚方法似乎可能涉及组合爆炸。

包含-排除

我认为这是一个不好的解决方案,但是无论如何这是一个不完整的草图。原则上,您可以编写一个程序进行计算,但是规范会很曲折。

我们希望计算的概率为 右侧概率内的事件是事件的并集: 其中是最小词典。我们可以使用包含-排除公式来扩展它。它涉及考虑上述事件的所有可能交集。令表示的幂集,即的所有可能子集的。然后

P(k-tile rack does not contain a word)=1P(k-tile rack contains a word).
P(k-tile rack contains a word)=P(xM{k-tile rack contains x}),
MP(M)MM
P(k-tile rack contains a word)=P(xM{k-tile rack contains x})=j=1|M|(1)j1SP(M):|S|=jP(xS{k-tile rack contains x})

最后要指定的是如何计算上方最后一行的概率。它涉及多元超几何。 是机架包含中每个单词的事件。由于通配符,这很难处理。我们将通过条件处理来考虑以下每种情况:机架不包含通配符,机架包含1个通配符,机架包含2个通配符,...

xS{k-tile rack contains x}
S

然后

P(xS{k-tile rack contains x})=w=0nP(xS{k-tile rack contains x}|k-tile rack contains w wildcards)×P(k-tile rack contains w wildcards).

我将在这里停止,因为要写出来的扩展是曲折的,根本没有启发性。编写一个计算机程序来完成它更容易。但是到现在,您应该看到包含-排除方法很棘手。它涉及词,每个词也非常复杂。对于词典,我认为高于。2|M|2|M|3.2×1060

扫描所有可能的机架

我认为这在计算上更容易,因为与最小单词的可能子集相比,可能的架子更少。我们相继减少了可能的的集合k-tile机架,直到获得不包含任何单词的机架。对于Scrabble(或Words With Friends),可能的7位数架子数量为数百亿个。应该用几十行R代码来计算不包含可能单词的单词数。但是我认为您应该能够比列举所有可能的机架做得更好。例如,“ aa”是一个最小的单词。这立即消除了所有包含多个“ a”的机架。您可以用其他话重复。对于现代计算机,内存不应该成为问题。一个7位数的拼字游戏机架需要少于7个字节的存储空间。在最坏的情况下,我们将使用几千兆字节来存储所有可能的机架,但是我也不认为这是个好主意。有人可能想对此进行更多考虑。

蒙特卡洛R程序

# 
#  scrabble.R
#  
#  Created by Vincent Vu on 2011-01-07.
#  Copyright 2011 Vincent Vu. All rights reserved.
# 

# The Words With Friends lexicon
# http://code.google.com/p/dotnetperls-controls/downloads/detail?name=enable1.txt&can=2&q=
url <- 'http://dotnetperls-controls.googlecode.com/files/enable1.txt'
lexicon <- scan(url, what=character())

# Words With Friends
letters <- c(unlist(strsplit('abcdefghijklmnopqrstuvwxyz', NULL)), '?')
tiles <- c(9, 2, 2, 5, 13, 2, 3, 4, 8, 1, 1, 4, 2, 5, 8, 2, 1, 6, 5, 7, 4, 
           2, 2, 1, 2, 1, 2)
names(tiles) <- letters

# Scrabble
# tiles <- c(9, 2, 2, 4, 12, 2, 3, 2, 9, 1, 1, 4, 2, 6, 8, 2, 1, 6, 4, 6, 4, 
#            2, 2, 1, 2, 1, 2)


# Reduce to permutation equivalent words
sort.letters.in.words <- function(x) {
  sapply(lapply(strsplit(x, NULL), sort), paste, collapse='')
}

min.dict <- unique(sort.letters.in.words(lexicon))
min.dict.length <- nchar(min.dict)

# Find all minimal words of length k by elimination
# This is held constant across iterations:
#   All words in min.dict contain no other words of length k or smaller
k <- 1
while(k < max(min.dict.length))
{
  # List all k-letter words in min.dict
  k.letter.words <- min.dict[min.dict.length == k]

  # Find words in min.dict of length > k that contain a k-letter word
  for(w in k.letter.words)
  {
    # Create a regexp pattern
    makepattern <- function(x) {
      paste('.*', paste(unlist(strsplit(x, NULL)), '.*', sep='', collapse=''), 
            sep='')
    }
    p <- paste('.*', 
               paste(unlist(strsplit(w, NULL)), 
                     '.*', sep='', collapse=''), 
               sep='')

    # Eliminate words of length > k that are not minimal
    eliminate <- grepl(p, min.dict) & min.dict.length > k
    min.dict <- min.dict[!eliminate]
    min.dict.length <- min.dict.length[!eliminate]
  }
  k <- k + 1
}

# Converts a word into a letter distribution
letter.dist <- function(w, l=letters) {
  d <- lapply(strsplit(w, NULL), factor, levels=l)
  names(d) <- w
  d <- lapply(d, table)
  return(d)
}

# Sample N racks of k tiles
N <- 1e5
k <- 7
rack <- replicate(N,
                  paste(sample(names(tiles), size=k, prob=tiles), 
                        collapse=''))

contains.word <- function(rack.dist, lex.dist)
{
  # For each word in the lexicon, subtract the rack distribution from the 
  # letter distribution of the word.  Positive results correspond to the 
  # number of each letter that the rack is missing.
  y <- sweep(lex.dist, 1, rack.dist)

  # If the total number of missing letters is smaller than the number of 
  # wildcards in the rack, then the rack contains that word
  any(colSums(pmax(y,0)) <= rack.dist[names(rack.dist) == '?'])
}

# Convert rack and min.dict into letter distributions
min.dict.dist <- letter.dist(min.dict)
min.dict.dist <- do.call(cbind, min.dict.dist)
rack.dist <- letter.dist(rack, l=letters)

# Determine if each rack contains a valid word
x <- sapply(rack.dist, contains.word, lex.dist=min.dict.dist)

message("Estimate (and SE) of probability of no words based on ", 
        N, " trials:")
message(signif(1-mean(x)), " (", signif(sd(x) / sqrt(N)), ")")

哇...很好的跟进。
马特·帕克

我有点惊讶,它减少到201个单词。尽管对于第一个单词,我们的内部规则接受“ I”和“ A”作为单词,这可能会进一步减少最小单词的数量。我希望看到有人破产了包含-排除分析,这应该是很
麻烦的

@shabbychef词典中没有1个字母的单词。最少的单词是2和3个字母的单词。这是最小字长的完整分布:2:73,3:86,4:31,5:9,6:2。6个字母的单词是:GLYCYL和SYZYGY。
vqv 2011年

@shabbychef我更新了答案,以包括确切的包含-排除方法的草图。比毛病还差。
vqv 2011年

做得好!我喜欢这个问题,对于一个有足够背景的人,这个问题可以用一个句子来提出,它带来了蒙特卡洛,包含-排除,DAG,搜索树,多项式代数,并且您的模拟得到了@理论的证实。 ub 干杯!
shabbychef 2011年

7

Srikant是正确的:蒙特卡洛研究是必经之路。有两个原因。首先,答案取决于强烈的字典的结构。两个极端是(1)词典包含每个可能的单字母单词。在这种情况下,未抽出或更多字母的单词的机会为零。(2)词典包含由单个字母组成的单词(例如,“ a”,“ aa”,“ aaa” )。不容易确定字母中没有单词的机会,而且很明显是非零的。任何确定的封闭式答案都必须包含整个字典结构,并且这将是一个非常糟糕且冗长的公式。1k

第二个原因是MC确实可行:您只需要正确地做。上一段提供了一个线索:不要只是随机地生成单词并进行查找;相反,请先分析字典并利用其结构。

一种方式将字典中的单词表示为树。该树植根于空符号,并在每个字母上一直向下分支。它的叶子(当然)是单词本身。但是,我们也可以将每个单词的所有非平凡置换也插入到树中每个单词最多)。由于不必存储所有这些排列,因此可以有效地完成此操作。只需要添加树中的边缘。叶子保持不变。实际上,可以通过坚持要求树按字母顺序排列来进一步简化。k!1

换句话说,要确定字典中是否包含字符的多集,请先按排序顺序排列元素,k然后在由原始词典中单词的已排序代表构成的树中寻找已排序的“单词”。实际上,这将比原始树小,因为它合并了所有与排序等效的词集,例如{停止,发布,匹配,选择,定位}。实际上,在英语词典中,无论如何都不会遇到此类单词,因为首先会找到“ so”。让我们看看实际情况。排序的多集是“ opst”;“ o”将分支到仅包含字母{o,p,...,z}的所有单词,“ p”将分支到仅包含{o,p,...,z}的所有单词,并且最多一个“ o”,最后“ s”将分支到叶“ so”!(我假设所有可能的候选词“ o”,“ op”,“

需要对通配符进行修改:我会让你们中间的程序员类型考虑一下。它不会增加字典的大小(实际上应该减小字典的大小);它会稍微减慢树的遍历速度,但不会以任何基本方式更改它。在任何包含单字母单词的词典中,例如英语(“ a”,“ i”),都没有复杂性:通配符的存在意味着您可以形成单词!(这暗示最初的问题可能听起来并不有趣。)

结果是,单个字典查找需要(a)对字母的多集进行排序,以及(b)遍历不超过个树的边缘。运行时间为。如果您巧妙地按排序的顺序生成随机多集(我可以想到几种有效的方法),则运行时间将减少为。将此乘以迭代次数即可得到总运行时间。kkO(klog(k))O(k)

我敢打赌,您可以在几秒钟内用一个真正的Scrabble集和一百万次迭代来进行这项研究。


@whuber树是一个很好的主意(赞成该主意),但是它不需要很多内存吗?我猜这取决于字典的多样性,但是我猜想一个合理多样的字典将需要很多树。例如,对于所有不包含单词的词,“ b”树将以字母“ b”开头,而不是“ a”其中有一个“ a”。类似地,对于没有“ a”和“ b”但具有“ c”的单词,“ c”树将以字母“ c”开头。我提议的直接方法似乎更简单,因为它需要一次性遍历字典中的所有单词,不是吗?

1
@Srikant:与开始缓存整个字典相比,树可能需要更少的RAM。您是否真的在担心几兆字节的RAM?顺便说一句,只有一棵树,没有很多:它们都植根于空词。据我所知,您的方法需要在每次迭代中多次搜索字典(最多搜索7个!),这使@shabbychef感到不切实际。如果您可以详细说明算法,那么请记住在其中编写“看看是否可以形成单词”的算法:这隐藏了许多重要的细节!
Whuber

@whuber:我意识到发表评论后只有一棵树的事实。调整我的方法-我同意我的蒙特卡洛提议是模糊的,您的回答将使您了解如何在这种情况下实际实施蒙特卡洛。我实际上是说直接方法(请参阅我的答案)实际上可能更简单,因为该方法需要对字典进行一次操作,而蒙特卡洛方法则需要在树上进行数千次迭代。只是想知道这些方法的相对优点。

@Srikant我拒绝评论您的直接方法,因为我怀疑它得到了错误的答案。它似乎没有考虑字典结构:即单词之间的子集关系。例如,对于包含所有可能的一个字母的单词的所有词典,您的公式是否会得到零的正确答案?
Whuber

@whuber hmmm好点。也许,我回答的是错误的问题!

2

蒙特卡洛方法

快速而肮脏的方法是进行蒙特卡洛研究。次绘制图块,每绘制图块,看看是否可以形成单词。用表示形成单词的。期望的概率为:kmkmw

1mwm

直接进场

让字典中的单词的数量由下式给出。令为构成字的方式数量。令单词所需的字母数用(即单词需要个“ a”字母等等)。用表示我们可以用所有图块形成的单词数。Stssthsthma,mb,...,mzsthmaN

N=(nk)

ts=(nama)(nbmb)...(nzmz)

(包括通配符的影响比较棘手。我暂时推迟该问题。)

因此,所需的概率为:

1stsN

快速而肮脏的方法可能没有那么快!该词典可能包含100,000个单词,并且搜索给定图块的匹配项可能会导致编码灾难。
shabbychef

@shabbychef这很适合拼写检查程序。例如,请参见n3labs.com/pdf/lexicon-squeeze.pdf

@shabbychef Reg monte-carlo-如果对字典进行排序,则匹配应该相当快,不是吗?无论如何,我之前概述的直接方法都是有缺陷的。我修好了它。我以前的解决方案中的问题是,同一单词可以以多种方式形成(例如,“ bat”,“ b * t”等)。

1
@shabbychef经过进一步思考,我同意您的看法,蒙特卡洛方法将行不通。一个问题是您需要弄清楚可以使用k个图块实际形成哪些单词,第二个问题是可以使用k个图块形成多个单词。从k个图块计算这些组合可能不是那么容易。

1
@Srikant谢谢。您的公式似乎假设您必须使用所有k个字母组成单词,但是我不认为这就是OP的要求。(无论如何,这都不是Scrabble的播放方式。)使用这种隐式假设,您处在正确的轨道上,但是您需要修改算法:您不必为字典中彼此排列的单词重复计算。例如,您不得在公式中同时减去t_ {stop}和t_ {post}。(这是易于实现的修改。)
whuber
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.