使用 Python 内置数据类型成为一日蜂后
作弊者永远不会赢,但至少他们可以使用 Python。
像许多其他书呆子一样,我喜欢文字谜题。我并不总是很擅长,而且我并不总是有时间去做,但是当我做的时候,我真的很享受它们。
我最近发现了一个新的每日谜题,被称为“拼字蜜蜂”,《纽约时报》在线提供。这个想法很简单。有七个不同的字母,一个在圆圈的中心,六个在它周围。你的工作是用这七个字母尽可能多地组成不同的单词。每个单词必须至少有四个字母长,并且每个单词还必须包含中心字母。您可以根据需要多次使用每个字母。
因此,如果字母是“eoncylt”,中心字母是“y”,那么您可以创建的一些单词可能是“cyclone”、“eyelet”和“nylon”。
在线游戏会根据您从潜在词库中组成的单词数量给您评分。如果您全部找到,您将被授予“蜂后”身份。
我在这个谜题中做得相当好,但我从未设法找到所有隐藏的单词。尽管如此,我还是在少数情况下成为了蜂后。如何做到的?答案很简单。我作弊了。如何作弊?当然是使用 Python。
现在,在编程方面,作弊游戏不一定是首要任务。在与自己竞争的文字游戏中作弊可能是不健康的竞争的迹象。但是,这样做也提供了一个很好的方法来回顾一下您可以使用 Python 内置数据类型的各种方式,以及您可以轻松处理单词和文本的方式。
因此,在本文中,我将探讨多种您可以作弊的方式——是的,如果只是一天,也可以成为蜂后。
尝试所有组合首先,您可以简单地尝试用给定的字母形成所有可能的组合。您可能还记得高中数学课上的内容,“排列”和“组合”之间是有区别的。当您生成“排列”时,顺序很重要,但是当您生成“组合”时,顺序并不重要。
您可以使用 Python 的 itertools
模块轻松地看到这一点,它是标准库的一部分,具有名为 permutations
和 combinations
的函数。每个函数都接受一个可迭代的数据结构和您希望在每个结果列表中包含的项目数。例如
>>> list(itertools.combinations(['a', 'b', 'c', 'd'], 2))
[('a', 'b'), ('a', 'c'), ('a', 'd'), ('b', 'c'), ('b', 'd'),
↪('c', 'd')]
>>> list(itertools.permutations(['a', 'b', 'c', 'd'], 2))
[('a', 'b'),
('a', 'c'),
('a', 'd'),
('b', 'a'),
('b', 'c'),
('b', 'd'),
('c', 'a'),
('c', 'b'),
('c', 'd'),
('d', 'a'),
('d', 'b'),
('d', 'c')]
正如您所看到的,来自 combinations
的输出认为顺序不重要,因此只返回了 ('a', 'b')
。但是 permutations
认为顺序很重要,因此返回了 ('a', 'b')
和 ('b', 'a')
。
您可以使用它来生成所有可能的单词字母组合,然后筛选它,对吗?嗯,不完全是,原因有两个。首先,游戏允许您重复字母。其次,这些函数只允许您指定单个输出数量。
您可以使用 combinations_with_replacement
函数来解决第一个问题,该函数不仅名称很长,而且(顾名思义)还允许字母在输出中出现多次。例如
>>> list(itertools.combinations_with_replacement(['a', 'b',
↪'c', 'd'], 2))
[('a', 'a'),
('a', 'b'),
('a', 'c'),
('a', 'd'),
('b', 'b'),
('b', 'c'),
('b', 'd'),
('c', 'c'),
('c', 'd'),
('d', 'd')]
但是,您想要找到至少四个字母长的单词。让我们假设最长的可能单词是 12 个字母长。您可以使用 for
循环,将每次迭代的结果附加到列表中。但是,更符合 Python 风格的方式是使用列表推导式——甚至更好的是,嵌套列表推导式。以我的经验来看,推导式是新的 Python 开发人员最难使用的概念之一。但是,它们非常适合创建和转换序列,这正是这里发生的事情
>>> one_combination
for n in range(4, 13)
for one_combination in
itertools.combinations_with_replacement('abc', n)]
此代码迭代从 4 到 13 的范围,从而生成从 4 到 12 的整数。对于每个 n 值,您然后生成该长度的所有组合(带替换),并使用字母“a”、“b”和“c”。然后将结果作为列表输出。
这很棒,但至少缺少两件事。首先,您感兴趣的不是组合,而是单词。而且,您要搜索的不只是几个字母,而是七个字母——其中一个必须出现在单词中。
因此,让我们稍微加强一下推导式,以便获得所有单词
>>> all_letters = 'eoncylt'
>>> center_letter = 'y'
>>> [''.join(one_combination)
for n in range(4, 13)
for one_combination in
itertools.combinations_with_replacement(all_letters, n)
if center_letter in one_combination]
好消息是,这现在确实生成了所有可能为您提供答案的组合。坏消息是,它还生成了很多实际上不是单词的组合。实际上,根据我的统计,这创建了 31,788 个“单词”,包括“occccccylt”和“eeeyt”等杰作。
因此,是的,您可以通过逐个输入所有这些单词作为游戏的输入来成为蜂后。但不知何故,我认为输入 31,788 个单词会使作弊失去乐趣。
输入字典为了使作弊更有效率和乐趣,让我们尝试不同的策略。与其生成所有可能的字母组合,不如搜索那些正确的组合。您如何知道什么是正确的?当然是通过字典——而且 Linux 附带英语字典这一事实使这更容易。
实际上,尽管生成组合通常很有用,但更明智的策略可能是从字典开始,然后选择符合您需要的单词。
我在此示例中使用的字典位于 /usr/share/dict/american-english 中,它包含 102,401 个不同的单词,每个单词单独占一行。每个单词都单独占一行这一事实被证明是一个很大的优势,因为它意味着您可以(再次)创建列表推导式。在这种情况下,列表推导式的来源将不是来自 itertools
的迭代器,而是字典文件本身。在 Python 中迭代文件,每次迭代都会得到一行。以下是如何获取这些单词的方法
[one_word.strip()
for one_word in open(words_file)]
但是,请稍等。这将返回所有单词。您只对那些包含谜题中七个字母以及中心字母的单词感兴趣。
对此的一个解决方案是编写一个函数来检查单词是否符合您的需求。例如
>>> def is_legal(word, all_letters, center_letter):
word = word.strip()
if len(word) < 4:
return False
if center_letter not in word:
return False
for one_letter in word:
if one_letter not in all_letters:
return False
return True
该函数似乎也能正常工作
>>> is_legal('cy', all_letters, center_letter)
False
>>> is_legal('cycle', all_letters, center_letter)
True
>>> is_legal('hairbrush', all_letters, center_letter)
False
有了这个函数,您现在可以阅读字典并获取符合条件的单词
[one_word.strip()
for one_word in open(words_file)
if is_legal(one_word, all_letters, center_letter)]
请注意使用 strip
方法删除单词开头和结尾的空格,主要是因为您将从文件中的每个单词中获得换行符。因此,您还将在 is_legal
函数中使用 strip
,以确保您不必处理换行符。
那么,它有效吗?答案是肯定的,大部分情况下是有效的。在某些日子里,我的 Python 程序找到了一些游戏中没有的单词。而在其他日子里,游戏正在寻找 Linux 字典中没有的单词。但在大多数情况下,一切似乎都运行良好,尽管我每天都努力赢得游戏,但肯定有些日子我会放弃,运行我的程序,并为我的编程技能而不是我的文字游戏技能感到沾沾自喜。
结论谁说“作弊者永远不会赢”没有考虑到作弊可能会带来对编程和数据结构的更好理解。实际上,我经常告诉人们,良好编程的关键通常不是知道最佳算法,而是知道应用哪些库以及如何结合您使用的语言的优势,以便您可以尽可能少地工作。本文展示了如何解决一个简单的现实世界问题,该问题几乎不涉及您自己的代码。相反,理解问题、Python 的标准库及其数据结构结合起来给出了答案。