回文树|介绍与实现

我们遇到了各种各样的问题,比如字符串中最大长度的回文,回文子串的数量,以及更多关于回文子串的有趣问题。大多数回文子串问题都有一些dpo(n) 2. )解(n是给定字符串的长度)或者我们有一个复杂的算法,比如 马纳彻算法 在线性时间内解决回文问题。

null

在本文中,我们将研究一种有趣的数据结构,它将以更简单的方式解决上述所有类似问题。这种数据结构是由 米哈伊尔·鲁宾奇 .

Features of Palindromic Tree : Online query and updation
                               Easy to implement                               
                               Very Fast

回文树的结构

回文树的实际结构是 接近有向图 它实际上是两棵树的合并结构,它们共享一些公共节点(为了更好地理解,请参见下图)。树节点通过存储索引来存储给定字符串的回文子字符串。 此树由两种类型的边组成: 1) 插入边(加权边) 2) 最大回文后缀(未加权)

插入边缘: 从节点插入边 U 五、 有点分量 十、 意味着节点v是通过在u处字符串的前端和末端插入x形成的。由于u已经是回文,因此节点v处生成的字符串也将是回文。 十、 每个边缘都将是一个单独的字符。因此,一个节点最多可以有26条插入边(考虑较低的字母字符串)。我们将使用 橙色 在我们的绘画表现中的这一优势。

最大回文后缀边: 由于名称本身表明,对于节点,此边将指向其最大回文后缀字符串节点。我们不会将完整字符串本身视为最大回文后缀,因为这毫无意义(自循环)。为了简单起见,我们将其称为后缀边(后缀边是指除完整字符串外的最大后缀)。很明显,每个节点只有一个后缀边,因为我们不会在树中存储重复的字符串。我们将使用 蓝色虚线边缘 因为它的图像表现。

17380071_1339816332769500_1143056574_o

根节点及其约定: 此树/图数据结构将包含 2根虚拟节点 更确切地说,它被认为是两棵单独的树的根,它们被连接在一起。 根-1 将是一个虚拟节点,用于描述 长度=-1 (从实现的角度,你可以很容易地推断出我们为什么使用它)。 根-2 将是一个节点,它将描述 长度=0 .

根-1有一个与自身相连的后缀边(自循环),对于任何长度为-1的假想字符串,其最大回文后缀也将是假想的,因此这是合理的。现在,Root-2的后缀边也将连接到Root-1,因为对于空字符串(长度为0),没有长度小于0的真正回文后缀字符串。

17439531_1339816346102832_1608791293_n

构建回文树

要构建回文树,我们只需在字符串中逐个插入字符,直到到达其结尾,插入完成后,我们将使用回文树,它将包含给定字符串的所有不同回文子串。我们需要确保的是,每次插入一个新字符时,我们的回文树都能保持上述特征。让我们看看怎样才能做到这一点。

假设我们得到了一个字符串 s 有长度 L 我们在索引中插入了字符串 K (k (k+1)第 性格插入第(k+1)个字符意味着插入以索引(k+1)结尾的最长回文的节点。所以,最长的回文字符串是形式的( ‘s[k+1]’+“X”+’s[k+1]’ )X本身就是回文。现在的事实是,字符串X位于索引

所以,要插入角色 s[k+1] ,我们只需要在树中找到字符串X,并将插入边从权重为s[k+1]的X定向到一个新节点,该节点包含 s[k+1]+X+s[k+1] .现在的主要工作是在有效时间内找到字符串X。正如我们所知,我们正在存储所有节点的后缀链接。因此,要用字符串X跟踪节点,我们只需要向下移动当前节点的后缀链接,即包含插入s[k]的节点。请参阅下图以更好地理解。

下图中的当前节点表明,在处理了从0到k的所有索引后,它是以索引k结尾的最大回文。蓝色虚线路径是当前节点到树中其他处理节点的后缀边的链接。字符串X将存在于位于后缀链接链上的其中一个节点中。我们所需要的就是通过在链上迭代找到它。

为了找到包含字符串X的所需节点,我们将把第k+1个字符放在后缀链接链中每个节点的末尾,并检查相应后缀链接字符串的第一个字符是否等于第k+1个字符。

一旦我们找到X字符串,我们将引导一个权重为s[k+1]的插入边,并将其链接到包含以索引k+1结尾的最大回文的新节点。下图中括号之间的数组元素是存储在树中的节点。

17431829_1341904152560718_575533818_o

还有一件事要做。由于我们已经在这个s[k+1]插入处创建了一个新节点,因此我们还必须将其与其后缀链接子节点连接起来。再一次,为了这样做,我们将使用上面从节点X开始的后缀链接迭代来找到一些新字符串 Y 以至于 s[k+1]+Y+s[k+1] 是新创建节点的最大回文后缀。一旦找到它,我们将把新创建的节点的后缀链接与节点Y连接起来。

笔记 :当我们找到字符串X时,有两种可能性。第一种可能性是该字符串 s[k+1]Xs[k+1] 树中不存在,第二种可能性是它是否已经存在于树中。在第一种情况下,我们将以同样的方式进行,但在第二种情况下,我们不会单独创建一个新节点,而是将插入边从X链接到已经存在的节点 S[k+1]+X+S[k+1] 树中的节点。我们也不需要添加后缀链接,因为节点将已经包含其后缀链接。

考虑字符串s= 阿巴 “带着 长度=4 .

在初始状态下,我们将有两个伪根节点,一个长度为-1(一些虚构的字符串) )第二个 无效的 长度为0的字符串。此时,我们还没有在树中插入任何字符。Root1,即长度为-1的根节点将是插入发生的当前节点。

17467920_1341903835894083_903983119_n

第一阶段:我们将插入 s[0] i、 e’ A. ‘.我们将从当前节点即Root1开始检查。在长度为-1的字符串的开头和结尾插入“a”将产生长度为1的字符串,该字符串将是“a”。因此,我们创建一个新节点“a”,并将插入边从root1直接插入到这个新节点。现在,长度为1的字符串的最大回文后缀字符串将是空字符串,因此其后缀链接将指向根2,即空字符串。现在,当前节点将是这个新节点“a”。

17439827_1341903842560749_390303617_n

第二阶段:我们将插入 s[1] i、 e’ B ‘. 插入过程将从当前节点即“a”节点开始。我们将从当前节点开始遍历后缀链接链,直到找到合适的X字符串,所以在这里遍历后缀链接,我们再次发现root1是X字符串。再次在长度为-1的字符串中插入“b”将产生长度为1的字符串,即字符串“b”。此节点的后缀链接将转到空字符串,如上述插入所述。现在,当前节点将是这个新节点“b”。

17410351_1341903849227415_701414514_n

第三阶段:我们将插入 s[2] i、 e’ B ‘. 再次从当前节点开始,我们将遍历其后缀链接以找到所需的X字符串。在这种情况下,它被发现是root2,即空字符串,因为在空字符串的前面和结尾添加“b”会产生长度为2的回文“bb”。因此,我们将创建一个新节点“ bb “并将插入边从空字符串指向新创建的字符串。现在,当前节点的最大后缀回文将是节点“b”。因此,我们将把这个新创建的节点的后缀边链接到节点“b”。当前节点现在变成节点“bb”。

17474730_1341903845894082_542267262_n

第四阶段:我们将插入 s[3] i、 e’ A. ‘. 插入过程从当前节点开始,在本例中,当前节点本身是最大的X字符串,因此 s[0]+X+s[3] 是回文。因此,我们将创建一个新节点“ 阿巴 并将插入边从当前节点“bb”链接到这个新创建的边权重为“a”的节点。现在,这个新创建的节点的链接的后缀将链接到节点“a”,因为它是最大的回文后缀。

17393153_1341903855894081_1019464410_n

上述实现的C++实现如下:

// C++ program to demonstrate working of
// palindromic tree
#include "bits/stdc++.h"
using namespace std;
#define MAXN  1000
struct Node
{
// store start and end indexes of current
// Node inclusively
int start, end;
// stores length of substring
int length;
// stores insertion Node for all characters a-z
int insertEdg[26];
// stores the Maximum Palindromic Suffix Node for
// the current Node
int suffixEdg;
};
// two special dummy Nodes as explained above
Node root1, root2;
// stores Node information for constant time access
Node tree[MAXN];
// Keeps track the current Node while insertion
int currNode;
string s;
int ptr;
void insert( int idx)
{
//STEP 1//
/* search for Node X such that s[idx] X S[idx]
is maximum palindrome ending at position idx
iterate down the suffix link of currNode to
find X */
int tmp = currNode;
while ( true )
{
int curLength = tree[tmp].length;
if (idx - curLength >= 1 and s[idx] == s[idx-curLength-1])
break ;
tmp = tree[tmp].suffixEdg;
}
/* Now we have found X ....
* X = string at Node tmp
* Check : if s[idx] X s[idx] already exists or not*/
if (tree[tmp].insertEdg[s[idx]- 'a' ] != 0)
{
// s[idx] X s[idx] already exists in the tree
currNode = tree[tmp].insertEdg[s[idx]- 'a' ];
return ;
}
// creating new Node
ptr++;
// making new Node as child of X with
// weight as s[idx]
tree[tmp].insertEdg[s[idx]- 'a' ] = ptr;
// calculating length of new Node
tree[ptr].length = tree[tmp].length + 2;
// updating end point for new Node
tree[ptr].end = idx;
// updating the start for new Node
tree[ptr].start = idx - tree[ptr].length + 1;
//STEP 2//
/* Setting the suffix edge for the newly created
Node tree[ptr]. Finding some String Y such that
s[idx] + Y + s[idx] is longest possible
palindromic suffix for newly created Node.*/
tmp = tree[tmp].suffixEdg;
// making new Node as current Node
currNode = ptr;
if (tree[currNode].length == 1)
{
// if new palindrome's length is 1
// making its suffix link to be null string
tree[currNode].suffixEdg = 2;
return ;
}
while ( true )
{
int curLength = tree[tmp].length;
if (idx-curLength >= 1 and s[idx] == s[idx-curLength-1])
break ;
tmp = tree[tmp].suffixEdg;
}
// Now we have found string Y
// linking current Nodes suffix link with s[idx]+Y+s[idx]
tree[currNode].suffixEdg = tree[tmp].insertEdg[s[idx]- 'a' ];
}
// driver program
int main()
{
// initializing the tree
root1.length = -1;
root1.suffixEdg = 1;
root2.length = 0;
root2.suffixEdg = 1;
tree[1] = root1;
tree[2] = root2;
ptr = 2;
currNode = 1;
// given string
s = "abcbab" ;
int l = s.length();
for ( int i=0; i<l; i++)
insert(i);
// printing all of its distinct palindromic
// substring
cout << "All distinct palindromic substring for "
<< s << " : " ;
for ( int i=3; i<=ptr; i++)
{
cout << i-2 << ") " ;
for ( int j=tree[i].start; j<=tree[i].end; j++)
cout << s[j];
cout << endl;
}
return 0;
}


输出:

All distinct palindromic substring for abcbab : 
1)a
2)b
3)c
4)bcb
5)abcba
6)bab

时间复杂性 建造过程的时间复杂性将是 O(k*n) “这里” N “是字符串的长度,并且” K ‘是每次插入字符时在后缀链接中查找字符串X和字符串Y所需的额外迭代次数。让我们试着近似常数“k”。我们将考虑最坏的情况。 s=“aaaaaa bcccdeeeef” .在这种情况下,对于连续字符的类似条纹,每个索引需要额外2次迭代才能在后缀链接中找到字符串X和Y,但一旦它达到某个索引 这就是我=s[i-1]最大长度后缀的最左边指针将达到其最右边的限制。因此,尽管我 s[我]=s[i-1] ,它将花费总共n次迭代(每次迭代求和),对于剩余的i,当s[i]==s[i-1]时,需要2次迭代,对所有这样的i求和,需要2*n次迭代。因此,在这种情况下,我们的复杂性大约为O(3*n)~O(n)。所以,我们可以粗略地说,常数因子‘k’将非常小。因此,我们可以认为整体的复杂性是线性的。 O(线的长度) 。为了更好地理解,您可以参考参考链接。

参考资料:

本文由 尼蒂什·库马尔 .如果你喜欢GeekSforgek并想投稿,你也可以使用“投稿”撰写文章。极客。组织或邮寄你的文章到contribute@geeksforgeeks.org.看到你的文章出现在Geeksforgeks主页上,并帮助其他极客。

如果您发现任何不正确的地方,或者您想分享有关上述主题的更多信息,请写下评论。

© 版权声明
THE END
喜欢就支持一下吧
点赞15 分享