如果您熟悉一些重量级数据结构,那么可以在O(n)的时间内完成这个问题,不过我承认编写代码并不容易。 :-)
我们需要两个工具来解决这个问题。
第一个工具:广义后缀树。广义后缀树是一种数据结构,直观地说,它是一个包含两个字符串S和T的所有后缀的Trie树,但以一种更节省空间的方式表示。
第二个工具:最近公共祖先查询。最近公共祖先查询结构(或LCA查询)是围绕特定树构建的一种数据结构。它被设计为有效地回答形式为“给定树中的两个节点,它们的最低公共祖先是什么?”的查询。
重要的是,对于长度为m的两个字符串,广义后缀树可以在O(m)时间内构建,并且可以在O(m)时间内构建LCA查询,使所有查询的时间为O(1)。这些并不是显而易见的运行时间;当它们首次被发现时,所需的算法和数据结构是可出版的结果。
假设我们拥有这两个结构,我们可以构建第三个数据结构,这就是我们将用来得到最终算法的:
第三个工具:最长公共扩展查询。最长公共扩展查询数据结构(或LCE查询)是围绕两个字符串S和T构建的一种数据结构。它支持以下形式的查询:给定一个索引i到字符串S和一个索引j到字符串T,从索引i在S中和索引j在T中开始出现的最长字符串的长度是多少?
以这两个字符串为例:
S: OFFENSE
0123456
T: NONSENSE
01234567
如果从字符串S的索引3和字符串T的索引4开始进行LCE查询,答案将是字符串ENSE。另一方面,如果我们从字符串S的索引4和字符串T的索引0开始进行LCE查询,我们将得到字符串N。
更严格地说,LCE查询结构实际上并没有返回您在两个位置找到的实际字符串,而是返回它的长度。
可以在O(m)时间内建立一个针对长度为m的字符串S和T的LCE数据结构,以便每个查询需要O(1)的时间。实现此目标的技术涉及构建两个字符串的广义后缀树,然后在其上构建LCA数据结构。关键的洞见是,在后缀树中,字符串S的后缀i和字符串T的后缀j的LCE是它们的最低公共祖先(LCA)。
LCE结构对于解决此问题非常有用。为了说明这一点,让我们看一下示例字符串abbcbbd。现在,考虑该字符串及其反向字符串,如下所示:
S: abbcbbd
0123456
T: dbbcbba
0123456
一个字符串中的回文串可以分为两种形式。第一种是奇数长度的回文串,这样的回文串有一个中心字符c和从中心向前向后延伸的“半径”。例如,字符串bbcbb
是一个以字符c
为中心,半径为bb
的奇数长度回文串。
我们可以通过使用LCE查询来计算一个字符串中有多少个奇数长度的回文串。具体来说,需要在该字符串和其反转后的字符串上建立LCE查询结构。然后,对于原始字符串中的每个位置,在原始字符串中查询该位置与镜像字符串对应位置的LCE值。这将给出以该点为中心的最长奇数长度的回文串(更具体地说,它会给出半径的长度加1,因为这两个位置上的字符总是匹配的)。一旦我们知道了在字符串中以该位置为中心的最长奇数长度的回文串,我们就可以计算以该位置为中心的奇数长度回文串的数量:这将等于将更长的回文串切割掉前后字符所得到的所有短回文串的数量。
有了这个思路,我们可以如下计算一个字符串中所有奇数长度回文串的数量:
for i from 0 to length(S) - 1:
total += LCE(i, length(S) - 1 - i)
另一类回文是偶数长度的回文,它们没有中心,而是由两个相等的半径组成。我们可以使用LCE查询来查找它们,但不是查看某个位置
i
和其在反转字符串中对应的位置,而是查看原始字符串中的位置
i
和在反转字符串中对应于索引
i
-1的位置。这样可以实现:
for i from 1 to length(S) - 1:
total += LCE(i, length(S) - i)
总的来说,这个解决方案
- 从原始字符串和反转字符串构建了一个LCE查询结构。反转字符串需要O(m)时间,建立LCE查询结构需要O(m)时间。此步骤的总时间为O(m)。
- 对LCE结构进行2m-1次查询。每次查询需要O(1)时间。此步骤的总时间为O(m)。
我相当确定可以在不使用如此沉重的工具的情况下实现这个运行时,但至少说明了存在一种线性时间的解决方案。
希望这能有所帮助!