我没有真正需要改进它,它只是为了好玩。现在它在大约200K字的列表上花了大约一秒钟。
我已经尝试过尽可能地优化它(使用生成器代替列表推导产生了很大的不同),并且我已经没有想法了。
你有吗?
#!/usr/bin/env python
# let's cheat at scrabble
def count_letters(word):
count = {}
for letter in word:
if letter not in count: count[letter] = 0
count[letter] += 1
return count
def spellable(word, rack):
word_count = count_letters(word)
rack_count = count_letters(rack)
return all( [word_count[letter] <= rack_count[letter] for letter in word] )
score = {"a": 1, "c": 3, "b": 3, "e": 1, "d": 2, "g": 2,
"f": 4, "i": 1, "h": 4, "k": 5, "j": 8, "m": 3,
"l": 1, "o": 1, "n": 1, "q": 10, "p": 3, "s": 1,
"r": 1, "u": 1, "t": 1, "w": 4, "v": 4, "y": 4,
"x": 8, "z": 10}
def score_word(word):
return sum([score[c] for c in word])
def word_reader(filename):
# returns an iterator
return (word.strip() for word in open(filename))
if __name__ == "__main__":
import sys
if len(sys.argv) == 2:
rack = sys.argv[1].strip()
else:
print """Usage: python cheat_at_scrabble.py <yourrack>"""
exit()
words = word_reader('/usr/share/dict/words')
scored = ((score_word(word), word) for word in words if set(word).issubset(set(rack)) and len(word) > 1 and spellable(word, rack))
for score, word in sorted(scored):
print str(score), '\t', word
答案 0 :(得分:14)
不要偏离基本代码,这里有一些相当简单的优化:
首先,将您的读者改为:
def word_reader(filename, L):
L2 = L+2
# returns an iterator
return (word.strip() for word in open(filename) \
if len(word) < L2 and len(word) > 2)
并将其命名为
words = word_reader('/usr/share/dict/words', len(rack))
这给我所有建议的更改带来了最大的改进。在我们走得太远之前,它会消除太长或太短的单词。请记住,word
在我的比较中未提取新行字符。我假设'\ n'行分隔符。此外,列表中的最后一个单词可能存在问题,因为它可能在其末尾没有新行,但在我的计算机上,最后一个单词是études,无论如何都无法找到我们的方法。当然,您可以事先从原始字典创建自己的字典,删除那些无效的字典:那些长度不合适或者字母超出a-z的字母。
接下来,Ferran为机架组建议了一个变量,这是一个好主意。但是,你也可以从每个单词中得到一个相当大的减速。使用这些套装的目的是清除许多根本没有任何镜头的镜头,从而加快速度。但是,我发现在调用spellable之前检查单词的第一个字母是否在机架中是否更快:
rackset = frozenset(rack)
scored = [(score_word(word), word) for word in words if word[0] in rackset \
and spellable(word, rack)]
然而,这必须伴随着对可转换的改变。我将其更改为以下内容:
def spellable(word, rack):
return all( [rack.count(letter) >= word.count(letter) \
for letter in set(word)] )
即使没有上一步的更改,也比现有的更快。
通过上面的三个更改,代码比我的简单测试快了大约3倍。
选择更好的算法
因为你真正在做的是寻找字谜,所以使用anagram字典是有道理的。 anagram字典将每个单词放在字典中,如果它们是字谜,则将它们分组。例如,'take'和'skate'是彼此的字谜,因为它们在排序时都等于'aekst'。我创建了一个anagram字典作为文本文件,其格式在每一行构成一个条目。每个条目都有anagrams的排序版本的排序版本,然后是anagrams本身。对于我正在使用该条目的示例
aekst skate takes
然后我可以采用机架字母的组合,并对anagram字典中的每一个进行二分搜索,看看是否有匹配。对于7个字母的机架,最多有120个独特的拼字游戏有效字母组合。执行二进制搜索是O(log(N))所以这将非常快。
我将算法分为两部分。第一个是anagram字典,第二个是实际的拼字游戏作弊程序。
Anagram词典创建者代码
f = open('/usr/share/dict/words')
d = {}
lets = set('abcdefghijklmnopqrstuvwxyz\n')
for word in f:
if len(set(word) - lets) == 0 and len(word) > 2 and len(word) < 9:
word = word.strip()
key = ''.join(sorted(word))
if key in d:
d[key].append(word)
else:
d[key] = [word]
f.close()
anadict = [' '.join([key]+value) for key, value in d.iteritems()]
anadict.sort()
f = open('anadict.txt','w')
f.write('\n'.join(anadict))
f.close()
拼字游戏作弊代码
from bisect import bisect_left
from itertools import combinations
from time import time
def loadvars():
f = open('anadict.txt','r')
anadict = f.read().split('\n')
f.close()
return anadict
scores = {"a": 1, "c": 3, "b": 3, "e": 1, "d": 2, "g": 2,
"f": 4, "i": 1, "h": 4, "k": 5, "j": 8, "m": 3,
"l": 1, "o": 1, "n": 1, "q": 10, "p": 3, "s": 1,
"r": 1, "u": 1, "t": 1, "w": 4, "v": 4, "y": 4,
"x": 8, "z": 10}
def score_word(word):
return sum([scores[c] for c in word])
def findwords(rack, anadict):
rack = ''.join(sorted(rack))
foundwords = []
for i in xrange(2,len(rack)+1):
for comb in combinations(rack,i):
ana = ''.join(comb)
j = bisect_left(anadict, ana)
if j == len(anadict):
continue
words = anadict[j].split()
if words[0] == ana:
foundwords.extend(words[1:])
return foundwords
if __name__ == "__main__":
import sys
if len(sys.argv) == 2:
rack = sys.argv[1].strip()
else:
print """Usage: python cheat_at_scrabble.py <yourrack>"""
exit()
t = time()
anadict = loadvars()
print "Dictionary loading time:",(time()-t)
t = time()
foundwords = set(findwords(rack, anadict))
scored = [(score_word(word), word) for word in foundwords]
scored.sort()
for score, word in scored:
print "%d\t%s" % (score,word)
print "Time elapsed:", (time()-t)
anagram词典创建者在我的机器上大约需要半秒钟。当字典已经创建时,运行拼字游戏作弊程序比OP的代码快 15x ,比上述更改后的OP代码快5倍。此外,加载字典的启动时间比实际搜索机架中的单词要大得多,因此这是一次进行多次搜索的更好方法。
答案 1 :(得分:2)
您可以使用/ usr / dict / share / words字典排序的事实,允许您跳过字典中的大量单词而不考虑它们。
例如,假设字典单词以“A”开头,而您在机架中没有“A”。您可以在单词列表中对以“B”开头的第一个单词进行二进制搜索,并跳过其间的所有单词。在大多数情况下,这会产生大差异 - 你可能会跳过一半的话。
答案 2 :(得分:2)
import trie
def walk_trie(trie_node, rack, path=""):
if trie_node.value is None:
yield path
for i in xrange(len(rack)):
sub_rack = rack[:i] + rack[i+1:]
if trie_node.nodes.has_key(rack[i]):
for word in walk_trie(trie_node.nodes[rack[i]], sub_rack, path+rack[i]):
yield word
if __name__ == "__main__":
print "Generating trie... "
# You might choose to skip words starting with a capital
# rather than lower-casing and searching everything. Capitalised
# words are probably pronouns which aren't allowed in Scrabble
# I've skipped words shorter than 3 characters.
all_words = ((line.strip().lower(), None) for line in open("/usr/share/dict/words") if len(line.strip()) >= 3)
word_trie = trie.Trie(mapping=all_words)
print "Walking Trie... "
print list(walk_trie(word_trie.root, "abcdefg"))
生成trie需要一段时间,但是一旦生成,获取单词列表应该比循环遍历列表快得多。
如果有人知道一种序列化特里的方法,那将是一个很好的补充。
只是为了证明生成特里是花费时间......
ncalls tottime percall cumtime percall filename:lineno(function)
98333 5.344 0.000 8.694 0.000 trie.py:87(__setitem__)
832722 1.849 0.000 1.849 0.000 trie.py:10(__init__)
832721 1.501 0.000 1.501 0.000 {method 'setdefault' of 'dict' objects}
98334 1.005 0.000 1.730 0.000 scrabble.py:16(<genexpr>)
1 0.491 0.491 10.915 10.915 trie.py:82(extend)
196902 0.366 0.000 0.366 0.000 {method 'strip' of 'str' objects}
98333 0.183 0.000 0.183 0.000 {method 'lower' of 'str' objects}
98707 0.177 0.000 0.177 0.000 {len}
285/33 0.003 0.000 0.004 0.000 scrabble.py:4(walk_trie)
545 0.001 0.000 0.001 0.000 {method 'has_key' of 'dict' objects}
1 0.001 0.001 10.921 10.921 {execfile}
1 0.001 0.001 10.920 10.920 scrabble.py:1(<module>)
1 0.000 0.000 0.000 0.000 trie.py:1(<module>)
1 0.000 0.000 0.000 0.000 {open}
1 0.000 0.000 0.000 0.000 trie.py:5(Node)
1 0.000 0.000 10.915 10.915 trie.py:72(__init__)
1 0.000 0.000 0.000 0.000 trie.py:33(Trie)
1 0.000 0.000 10.921 10.921 <string>:1(<module>)
1 0.000 0.000 0.000 0.000 {method 'split' of 'str' objects}
1 0.000 0.000 0.000 0.000 trie.py:1(NeedMore)
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}
答案 3 :(得分:0)
您可以将更多列表转换为生成器:
all( [word_count[letter] <= rack_count[letter] for letter in word] )
...
sum([score[c] for c in word])
到
all( word_count[letter] <= rack_count[letter] for letter in word )
...
sum( score[c] for c in word )
在循环中,不是在每次迭代中创建rask集,而是可以提前创建它,它可以是冻结集。
rack_set = frozenset(rack)
scored = ((score_word(word), word) for word in words if set(word).issubset(rask_set) and len(word) > 1 and spellable(word, rack))
使用rack_count字典也可以这样做。它不需要在每次迭代时创建。
rack_count = count_letters(rack)
答案 4 :(得分:0)
更好地整理数据。您可以使用这些字母计数向量(以及“向量”)预先构建树结构,而不是通过线性字典进行读取和比较,并将其保存到文件中。