【问题标题】:Memory error while solving an anagram解字谜时出现记忆错误
【发布时间】:2014-11-09 13:46:29
【问题描述】:

我正在尝试解决以下问题:

字谜是一种文字游戏,是重新排列单词或短语的字母以产生新单词或短语的结果,使用所有原始字母恰好一次;例如,管弦乐队 = carthorse。使用http://www.puzzlers.org/pub/wordlists/unixdict.txt 处的单词列表,编写一个程序,找出共享相同字符且包含最多单词的单词集。

即使文件大小只有 1000 字节,它也会失败。同样每次创建新列表时,为什么 Python 会将旧列表保留在内存中?我收到以下错误。

l=list(map(''.join, itertools.permutations(i)))

给我:

MemoryError

这是我的代码:

import itertools
def anagram():
    f=open('unixdict.txt')
    f2=open('result_anagram.txt','w')
    words = f.read(1000).split('\n')
    for i in words:
        l=[]
        l=list(map(''.join, itertools.permutations(i)))
        l.remove(i)
        for anagram in l:
            if l==i:
                f2.write(i + "\n")
    return True

anagram()

根据建议将上述代码更改为。但仍然出现内存错误。

import itertools

def anagram():
    f=open('unixdict.txt')
    f2=open('result_anagram.txt','w')
    words = set(line.rstrip('\n') for line in f)
    for i in words:
        l= map(''.join, itertools.permutations(i))
        l =(x for x in l if x!=i)
        for anagram in l:
            if anagram in words:
                f2.write(i + "\n")
    return True

anagram()

内存错误 [22.2s完成]

【问题讨论】:

  • 你为什么要创建一个列表l = [] 然后重新分配l = list(map...? 还有list(map(''.join, itertools.permutations(i))) 到底在做什么?
  • 是的,删除了列表功能。谢谢

标签: python memory anagram


【解决方案1】:

无论你做什么,这个程序都会非常低效。

但是你可以修复这个MemoryError,这样它就会永远运行而不是失败。

首先,请注意,一个 12 个字母的单词有 479,001,600 个排列。将所有这些存储在内存中将占用超过 2GB 的内存。那么,你如何解决这个问题?只是不要将它们全部存储在内存中。将迭代器保留为迭代器,而不是制作列表,然后您一次只需要适合一个,而不是全部。

这里有一个问题:您实际上在if l==i: 行中使用了该列表。但显然这是一个错误。字符串列表不可能等于单个字符串。您不妨用raise TypeError 替换该行,此时您可以替换整个循环并更快地失败。 :)

认为你想要的是if anagram in words:。在这种情况下,您不需要 l,除了在 for 循环中,这意味着您可以安全地将其保留为惰性迭代器:

for i in words:
    l = map(''.join, itertools.permutations(i))
    l = (x for x in l if x != i)
    for anagram in l:
        if anagram in words:
            f2.write(i + "\n")

我在这里假设 Python 3.x,否则 list 调用是完全没有必要的。如果您使用的是 2.x,请将 map 替换为 itertools.imap


作为旁注,f.read(1000) 通常会在末尾获得一个额外单词的一部分,而在下一个循环中获得剩余部分。试试readlines。虽然没有论据没用,但有论据非常有用:

从流中读取并返回行列表。可以指定 hint 来控制读取的行数:如果到目前为止所有行的总大小(以字节/字符为单位)超过 hint,则不会再读取行。

因此,f.readlines(1000) 将让您一次读取大约 1K 的缓冲区,而不会获得部分行。当然现在,你不必在换行符上split,而必须rstrip他们:

words = [line.rstrip('\n') for line in f.readlines(1000)]

但是,您还有另一个问题。如果您一次只阅读大约 100 个单词,那么找到字谜的机会非常渺茫。例如,orchestra 在字典中不会靠近 carthorse,因此除非您记住整个文件,否则无法找到。但这应该没问题;像 web2 这样的典型 Unix 字典有大约 200K 行;您可以轻松地将其读入内存并将其保留为set,而不会影响您的 2GB。所以:

words = set(line.rstrip('\n') for line in f)

另外,请注意,您正在尝试打印字典中具有字谜的每个单词(多次,如果它有多个字谜)。即使使用有效的算法,这也需要很长时间——并且会产生比你想要的更多的数据。一个更有用的程序可能是接受一个输入单词(例如,通过inputsys.argv[1])并仅输出该单词的字谜。


最后:

即使在使用 l 作为生成器之后,它也占用了太多的关闭时间,尽管没有因内存错误而失败。你能解释一下单词作为一个集合而不是一个列表的重要性吗? 【137.4s完成】才200字节,你之前提过,但是如何用set来克服呢?

正如我在顶部所说的,“无论你做什么,这个程序都会非常低效。”

为了找到一个 12 个字母的单词的字谜,您需要经历 4.79 亿个排列,并根据大约 20 万个单词的字典检查每个排列,因此 479M * 200K = 95 万亿 检查每个单词。有两种方法可以改善这一点,第一种涉及为作业使用正确的数据结构,第二种涉及为作业使用正确的算法。

将要迭代的事物集合从列表更改为生成器(惰性可迭代),将占用线性空间(479M 字符串)的东西转换为占用恒定空间的东西(一些固定大小的迭代器状态,加上一个字符串一次)。类似地,将要检查的单词集合从列表更改为集合会将需要线性时间(将字符串与列表中的每个元素进行比较)的东西变成需要恒定时间的东西(散列字符串,然后查看是否有任何内容)具有该哈希值的集合)。因此,这消除了您的问题中的 * 200K 部分。

但是您仍然遇到了问题的479M 部分。你不能用更好的数据结构来解决这个问题。相反,您必须重新考虑问题。如何在不尝试所有排列的情况下检查单词的任何排列是否与其他单词匹配?

嗯,当且仅当 X 和 Y 具有相同的字母时,单词 X 的某些排列匹配单词 Y。 X 中字母的顺序无关紧要。如果集合相同,则至少有一个匹配的排列(或恰好一个,取决于您如何计算重复字母),如果没有,则正好为 0。因此,不要遍历单词中的所有排列抬头,只看它的集合。但是是否有重复确实很重要,所以你不能在这里只使用set。您可以使用某种多集 (collections.Counter) 作品……或者,在效率损失很小并且在简单性上获得很大收益的情况下,您可以对字母进行排序。毕竟,如果两个单词以任意顺序具有相同的字母,那么当它们都被排序时,它们具有相同的字母顺序。

当然你需要知道哪些词是字谜,而不仅仅是有字谜,所以你不能只在一组字母中查找它集,您必须在将字母集映射到单词的字典中查找它。例如,像这样:

lettersets = collections.defaultdict(set)
for word in words:
    lettersets[''.join(sorted(word))].add(word)

所以现在,要查找单词的字谜,您所要做的就是:

anagrams = lettersets[''.join(sorted(word))]

不仅简单易读,而且是常数时间。

如果你真的想打印出所有单词的所有字谜的海量列表……嗯,这也很容易:

for _, words in lettersets.items():
    for word in words:
        print('{} is an anagram of {}'.format(word, ', '.join(words - {word})))

现在,不是花费 479M*200K 时间来查找一个单词的字谜,或者 479M*200K*200K 时间来查找所有单词的所有字谜,而是需要恒定时间来查找一个单词的字谜,或者 200K 时间来查找所有单词的所有字谜。 (当然,在开始创建映射时增加了 200K 的设置时间,但预先花费 200K 时间来节省 200K,更不用说 479M*200K,每次查找的时间是明显的胜利。)

当您想要查找部分字谜或句子字谜时,事情会变得有些棘手,但您希望遵循相同的基本原则:找到可以让您在恒定或对数时间内而不是线性或更糟糕的是,找到不需要您通过指数或阶乘数量的候选者的算法。

【讨论】:

  • 谢谢,我只是没有意识到 12 个字母的单词会有这么多排列。也了解了发电机的重要性。是的,想用单词做字谜
  • 即使在使用 l 作为生成器之后,它也占用了太多的停机时间,尽管没有因内存错误而失败。你能解释一下单词作为一个集合而不是一个列表的重要性吗? 【137.4s完成】才200字节,你之前提过,但是如何用set的word来克服呢?
【解决方案2】:
import urllib

def anagram():
    f=urllib.urlopen('http://www.puzzlers.org/pub/wordlists/unixdict.txt')
    words = f.read().split('\n')

    d={''.join(sorted(x)):[]  for x in words}   #create dic with empty list as default
    for x in words:
        d[''.join(sorted(x))].append(x)

    max_len= max( len(v) for k,v in d.iteritems())

    for k,v in d.iteritems():
        if len(v)>=max_len:
            print v

anagram()

输出:

['abel', 'able', 'bale', 'bela', 'elba']
['alger', 'glare', 'lager', 'large', 'regal']
['angel', 'angle', 'galen', 'glean', 'lange']
['evil', 'levi', 'live', 'veil', 'vile']
['caret', 'carte', 'cater', 'crate', 'trace']
['elan', 'lane', 'lean', 'lena', 'neal']

在 5.7 秒内完成

【讨论】:

  • 又好又短,真棒
【解决方案3】:

这里有一个解决问题的提示:如果两个字符串具有相同的字母集合,则它们是彼此的字谜。您可以对单词进行排序(将例如“orchestra”转换为“acehorst”),然后只看到两个单词具有相同的排序顺序。如果是这样,那么原始单词一定是彼此的字谜,因为它们具有所有相同的字母(以不同的顺序)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-11-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-06-13
    相关资源
    最近更新 更多