需要一个单词打包算法的帮助。

17
我有一个由字母子列表组成的列表,每个子列表中的字母数量可能不同。该列表和子列表都是有序的。可以使用此结构通过选择数字X,在每个子列表中的位置X取一个字母,并按顺序连接它们来生成单词。如果数字X大于子列表的长度,则会循环回到开头。
给定一组单词,我需要找到一种方式将它们打包成此类最小可能的结构(即具有最短的子列表)。当然,必须有与最长单词中的字母数相同的子列表,并且较短的单词将用空格填充。
如果描述问题不是非常清楚,我不是计算机科学专业毕业生,敬请谅解。为了举一个简单的例子:假设我需要打包下面这些单词[ 'a ', 'an', 'if', 'is', 'in', 'on', 'of', 'i '],我可以使用以下结构:
[  
    [ 'i', 'o', 'a' ],  
    [ 's', 'n', 'f', ' ' ]  
]
这将使我能够生成以下单词:
0: is  
1: on  
2: af*  
3: i  
4: os*  
5: an  
6: if  
7: o *  
8: as*  
9: in  
10: of  
11: a
例如,如果您选择位置10,则单词“of”是通过连接第一个子列表中索引10%3(=1)处的字母和第二个子列表中索引10%4(=2)处的字母生成的。
到目前为止,我最好的尝试涉及使用汉明距离矩阵来首先放置最“连接”的单词,然后是它们最近的邻居,以达到在每次插入时最小化更改的目标。这是一次完全直觉的尝试,我觉得肯定有更好/更智能的方法来解决这个问题。
澄清: 这是我正在尝试解决的实际问题,约束条件如下: 1. 每个子列表的字符数应该在100或更少的范围内。 2. 键空间应尽可能小(即,误导性单词的数量应最少)。大约以百万级别的键空间是边缘的。
我不知道是否可能有一个好的解决方案。例如,对于我现在拥有的算法,我可以在150万个选项的键空间中插入大约200个单词(只是随机的英文单词)。我想做得比那更好。

只是好奇,你在哪里找到这个问题的? - Nikita Rybak
你需要最优解还是“好的启发式”就足够了? - Svante
@Nikita:这是我想出来的... :) 它是我正在工作的一个项目的一部分。 @Svante:俗话说得好,过犹不及。我很乐意听取任何能改进我当前解决方案的建议。 - szx
使用模数函数可以创建“虚假”单词(用星号标记的单词)。你是如何处理它的?创建多少个虚假单词是否重要? - Dr. belisarius
键空间中两个单词之间的平均距离应该尽可能小。如果您所说的结构大小是指子列表中字符的数量,那么我的上限将是每个子列表约100个字符。同样,这是出于实际原因,可能是不可能的,但是有可能吗? - szx
显示剩余4条评论
4个回答

3
好的,你说你对次优解感兴趣,那我给你提供一个。这取决于字母表大小。例如,对于 26 个数组大小将略大于 100(无论要编码的单词数量如何)。
众所周知,如果有两个不同的质数 a 和 b,以及非负整数 k 和 l(k < a,l < b),则可以找到数字 n,使得 n % a == k 和 n % b == l。 例如,对于(a = 7,b = 13,k = 6,l = 3),可以取 n = 7 * 13 + 7 * 3 + 13 * 6。n % 7 == 6 并且 n % 13 == 3。
对于任意数量的质数整数,同样成立。
您可以像这样初始化数组。
['a', 'b', 'c', ... 'z', 'z', 'z', 'z', ...]   # array size = 29
['a', 'b', 'c', ... 'z', 'z', 'z', 'z', ...]   # array size = 31
['a', 'b', 'c', ... 'z', 'z', 'z', 'z', ...]   # array size = 37
['a', 'b', 'c', ... 'z', 'z', 'z', 'z', ...]   # array size = 41
...

现在,假设你的单词是“geek”。你需要一个数字X,满足X % 29 == 6X % 31 == 4X % 37 == 4X % 41 == 10。正如上面所示,你总是可以找到这样的X。
因此,如果你有26个字母的字母表,你可以创建宽度为149的矩阵(请参见质数列表),并用它来编码任何单词。

很好的答案,但它引出了一个我没有指定的限制,因为我没有一个明确定义的“足够好”的解决方案的准则: 给定一组数百个单词,任意两个单词之间的平均距离需要尽可能小。多小?理想情况下,不同位置的数量除以第一个数组的大小应该在数百或数千的范围内。使用这种解决方案,可能的位置数量会非常快地增加,对于六个字母(或更多)的单词来说变得不切实际。 - szx
@szx 给定一组几百个单词,任意两个单词之间的平均距离需要尽可能小。您能澄清一下吗?我认为我们不会选择要编码的集合:该集合已经给定。 - Nikita Rybak
请参阅我上面的帖子和澄清:给定一组单词(将包含大约几百个单词)。可安排产生这些单词的字母以不同的配置方式(即不同的索引、每个子列表的字母数),其“效率”不同。所谓“两个单词之间的距离”,是指在给定集合中分隔单词的虚词数量,应尽量少,使信噪比最大化。 - szx

2
我们可以通过不将列表长度设为质数,而是将质数与列表关联来改进 Nikita Rybek 的答案。这使我们不必使子列表比必要的更长,从而保持质数较小,这意味着更小的键空间和更有效的打包。使用此方法和下面的代码,我将一个由58,110(小写)单词组成的列表压缩为 464 个字符。有趣的是,只有字母 'alex' 出现在单词的第 21 个字母中。键空间高达 33 位数,但并不严格需要使用质数,相关数字只需互质即可。这可能可以减少。
import itertools
import operator
import math

# lifted from Alex Martelli's post at https://dev59.com/snI95IYBdhLWcg3w-DH0
def erat2( ):
    D = {  }
    yield 2
    for q in itertools.islice(itertools.count(3), 0, None, 2):
        p = D.pop(q, None)
        if p is None:
            D[q*q] = q
            yield q
        else:
            x = p + q
            while x in D or not (x&1):
                x += p
            D[x] = p


# taken from http://en.literateprograms.org/Extended_Euclidean_algorithm_(Python)
def eea(u, v):
    u1 = 1; u2 = 0; u3 = u
    v1 = 0; v2 = 1; v3 = v
    while v3 != 0:
        q = u3 / v3
        t1 = u1 - q * v1
        t2 = u2 - q * v2
        t3 = u3 - q * v3
        u1 = v1; u2 = v2; u3 = v3
        v1 = t1; v2 = t2; v3 = t3
    return u1, u2, u3

def assign_moduli(lists):
    used_primes = set([])
    unused_primes = set([])
    moduli = [0]*len(lists)
    list_lens = [len(lst) for lst in lists]
    for i, length in enumerate(list_lens):
        for p in erat2():
            if length <= p and p not in used_primes:
                used_primes.add(p)
                moduli[i] = p
                break
            elif p not in used_primes:
                unused_primes.add(p)
    return moduli



class WordEncoder(object):
    def __init__(self):
        self.lists = [[]] # the list of primedlists
        self.words = {} # keys are words, values are number that retrieves word
        self.moduli = [] # coprime moduli that are used to assign unique keys to words

    def add(self, new_words):
        added_letter = False # flag that we need to rebuild the keys
        for word in new_words:
            word = word.rstrip() # a trailing blank space could hide the need for a key rebuild
            word_length, lists_length = len(word), len(self.lists)
            # make sure we have enough lists
            if word_length > lists_length:
                self.lists.extend([' '] for i in xrange(word_length - lists_length))
            # make sure that each letter is in the appropriate list
            for i, c in enumerate(word):
                if c in self.lists[i]: continue
                self.lists[i].append(c)
                added_letter = True
            self.words[word] = None
        # now we recalculate all of the keys if necessary
        if not added_letter:
            return self.words
        else:
            self._calculate_keys()

    def _calculate_keys(self):
        # were going to be solving a lot of systems of congruences
        # these are all of the form x % self.lists[i].modulus == self.lists[i].index(word[i]) with word padded out to 
        # len(self.lists). We will be using the Chinese Remainder Theorem to do this. We can do a lot of the calculations
        # once before we enter the loop because the numbers that we need are related to self.lists[i].modulus and not
        # the indexes of the necessary letters
        self.moduli = assign_moduli(self.lists)
        N  = reduce(operator.mul, self.moduli)
        e_lst = []
        for n in self.moduli:
             r, s, dummy = eea(n, N/n)
             e_lst.append(s * N / n)
        lists_len = len(self.lists)
        #now we begin the actual recalculation 
        for word in self.words:
             word += ' ' * (lists_len - len(word))
             coords = [self.lists[i].index(c) for i,c in enumerate(word)]
             key = sum(a*e for a,e in zip(coords, e_lst)) % N  # this solves the system of congruences
             self.words[word.rstrip()] = key

class WordDecoder(object):
    def __init__(self, lists):
       self.lists = lists
       self.moduli = assign_moduli(lists)

    def decode(self, key):
        coords = [key % modulus for modulus in self.moduli]
        return ''.join(pl[i] for pl, i in zip(self.lists, coords))    


with open('/home/aaron/code/scratch/corncob_lowercase.txt') as f:
    wordlist = f.read().split()

encoder = WordEncoder()
encoder.add(wordlist)

decoder = WordDecoder(encoder.lists)

for word, key in encoder.words.iteritems():
    decoded = decoder.decode(key).rstrip()
    if word != decoded:
        print word, decoded, key
        print "max key size: {0}. moduli: {1}".format(reduce(operator.mul, encoder.moduli), encoder.moduli)
        break
else:
    print "it works"
    print "max key size: {0}".format(reduce(operator.mul, encoder.moduli))
    print "moduli: {0}".format(encoder.moduli)
    for i, l in enumerate(encoder.lists):
        print "list {0} length: {1}, {2} - \"{3}\"".format(i, len(l), encoder.moduli[i] - len(l), ''.join(sorted(l)))
    print "{0} words stored in {1} charachters".format(len(encoder.words), sum(len(l) for l in encoder.lists))

但在szx的算法中,数字被列表长度除,而不是与列表相关联的另一个数字除。我理解得对吗? - Nikita Rybak
我引用一句话。如果数字X大于子列表的长度,它将环绕回来。 - Nikita Rybak
请注意,在实践中,我们根本不需要存储列表:我们可以轻松地通过数字和“虚拟”索引长度来确定字符,而无需使用额外的内存。因此,这使得宽度为0的列表成为可能 :) - Nikita Rybak
@aaronsterling:不幸的是,如此大的密钥空间行不通。如果我描述一下这个项目会更容易理解:它是一个物理艺术装置,由齿轮传动系统组成。每个齿轮对应一个子列表,齿上印有字母。目标是当旋转到特定位置时,特定的齿(例如,每个齿轮上指向上方的齿)将组成一个单词。通过从一个位置移动到另一个位置,可以重新创建文本(尚未确定,但将具有数百个独特的单词)。由于RPM存在限制,因此存在密钥空间问题。 - szx
@szx。不幸的是,我的代数书现在都在另一个大陆上,而我却在一个岛上。与任何熟悉基本环论的数学家交谈,他们至少可以指出你正确的方向。 - aaronasterling
显示剩余3条评论

0

我不确定我完全理解你的问题,但我以前偶然发现过 prezip。Prezip 是一种利用许多单词共享的公共前缀来压缩已排序单词集的方法。

由于您没有提到任何排序约束条件,我建议创建一个您想要的排序单词集。然后执行类似于 prezip 的操作。结果是一个压缩且已排序的单词集,可以通过索引引用。


0

天啊,有没有一个标记为“算法”的问题不会得到“trie”响应?看起来“trie”是新的“jquery”:可以解决任何问题 :) - Nikita Rybak
@Nikita 他正在尝试高效地存储单词,这正是 Trie 的用途之一:http://en.wikipedia.org/wiki/Trie#Dictionary_representation - fortran
MySQL 也被用于高效地存储单词,我想知道为什么没有人提供它 :) 还有 LZW! - Nikita Rybak
谢谢,实际上我确实看了 tries(字典树),但是我还没有完全想出如何将其应用于我的问题。这不仅涉及到高效存储单词,还有此问题所施加的独特限制,我还不太确定如何解决。 - szx

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接