高效计算字符串划分概率之和

145 阅读4分钟

在统计项目中,我们需要迭代字符串集合的所有可能划分方式,并对每个划分运行一个简单的计算。具体来说,每个可能的子串都有一个与之相关的概率,我们需要计算所有划分中,划分中子串概率乘积之和。

例如,如果字符串是“abc”,则有子串“a”、“b”、“c”、“ab”、“bc”和“abc”的概率。该字符串有四种可能的划分方式:“abc”、“ab|c”、“a|bc”和“a|b|c”。算法需要找到每个划分中组成部分概率的乘积,然后对四个结果进行求和。

目前,我们已经编写了一个 python 迭代器,它使用整数的二进制表示来表示划分(例如,上面示例中的 00、01、10、11),并简单地遍历这些整数。不幸的是,对于长度超过 20 个字符的字符串,这种方法非常慢。

  1. 解决方案

2.1 动态规划

def dynProgSolution(text, probs):
    probUpTo = [1]
    for i in range(1, len(text)+1):
        cur = sum(v*probs[text[k:i]] for k, v in enumerate(probUpTo))
        probUpTo.append(cur)
    return probUpTo[-1]

print dynProgSolution(
    'abc',
    {'a': 0.1, 'b': 0.2, 'c': 0.3,
     'ab': 0.4, 'bc': 0.5, 'abc': 0.6}
)
  • 动态规划的复杂度是 O(N2),因此对于 N=20 的情况,它可以轻松解决问题。
  • 动态规划的工作原理是:你将乘以 probs['a']*probs['b'] 的所有内容也将乘以 probs['ab']。
  • 由于乘法和加法的分配律,你可以将这两个值相加,并将这个单独的和乘以其所有延续。
  • 对于每个可能的最后一个子串,它通过将该概率乘以之前所有路径的概率之和来添加以该结尾的所有拆分的和。(欢迎更好的表述。)

2.2 并行化

  • 首先,进行性能分析以找到瓶颈。
  • 如果瓶颈只是大量可能的划分,我建议使用并行化,可能通过多进程实现。如果这还不够,你可能需要研究一下 Beowulf 集群。
  • 如果瓶颈只是计算速度慢,可以尝试使用 C 语言进行扩展。通过 ctypes 完成此操作非常简单。
  • 此外,我不太确定你如何存储划分,但你很可能会通过使用一个字符串和一个后缀数组来很好地减少内存消耗。如果你的瓶颈是交换和/或缓存未命中,那可能是一个很大的胜利。

2.3 备忘录技巧

  • 你的子串将被更长的字符串反复使用,因此使用记忆技巧缓存值似乎是一个显而易见的方法。这只是一个时间空间权衡。最简单的实现是使用字典来缓存计算值。对每个字符串计算进行字典查找;如果它不在字典中,则计算并添加它。后续调用将使用预先计算的值。如果字典查找比计算快,你很幸运。
  • 我意识到你正在使用 Python,但顺便说一下,如果你在 Perl 中这样做,你甚至不必编写任何代码;内置的 Memoize 模块将为你完成缓存!

2.4 优化乘法和加法

  • 通过计算的微小重构,你可能会减少计算量,尽管我不确定它是否会改变现状。核心思想如下:
  • 考虑一个较长的字符串,例如“abcdefghik”,长度为 10,为了明确起见不失一般性。在朴素的方法中,你将 p(a) 乘以 9 个尾部的所有划分,p(ab) 乘以 8 个尾部的所有划分,等等;特别是 p(a) 和 p(b) 将与 p(ab) 一样乘以完全相同的 8 个尾部划分 —— 其中 3 个乘积和两个和。因此,将它分解为:
(p(ab) + p(a) * p(b)) * (partitions of the 8-tail)
  • 我们已经减少了这部分的 2 次乘法和 1 次求和,并节省了 1 次乘积和 1 次求和,只包含对于一个拆分点就在“b”右边的所有划分。当涉及到拆分点就在“c”右边的划分时,
(p(abc) + p(ab) * p(c) + p(a) * (p(b)*p(c)+p(bc)) * (partitions of the 7-tail)
  • 节省的计算量会增加,部分归功于内部重构 —— 当然,必须小心不要重复计算。我认为这种方法可以推广 —— 从中点开始,并考虑所有在该处拆分的划分,分别为左右两部分(和递归)进行乘法和求和;然后添加所有没有在该处拆分的划分,例如在示例中,“ef”在一起而不是分开的部分 —— 所以将“ef”作为新的“超级字母”X,“折叠”所有概率,你剩下的字符串短一个,为“abcdXghik”(当然,该字符串中子串的概率直接映射到原始字符串的概率,例如,新字符串中的 p(cdXg) 正好是原始字符串中的 p(cdefg))。