📜  回文树|简介与实施

📅  最后修改于: 2021-04-17 11:29:32             🧑  作者: Mango

我们会遇到各种问题,例如字符串的最大回文率,回文子串的数量以及回文子串上的更多有趣问题。大多数回文子串问题都有一些DP O(n 2 )解(n是给定字符串的长度),或者我们有一个复杂的算法,如Manacher算法,它可以在线性时间内解决回文问题。

在本文中,我们将研究一个有趣的数据结构,它将以更简单的方式解决上述所有类似问题。该数据结构是Mikhail Rubinchik发明的。

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

回文树的结构

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

插入边:
从节点uv的权重为x的插入边意味着节点v是通过在u的字符串的开头和结尾处插入x来形成的。由于u已经是回文,因此节点v处的结果字符串也将是回文。
x将是每个边缘的单个字符。因此,一个节点最多可以有26个插入边(考虑到小写字母字符串)。在我们的图形表示中,我们将为此边缘使用橙色

最大回文后缀边缘:
顾名思义,对于一个节点,该边将指向其“最大回文后缀字符串”节点。我们不会将完整的字符串本身视为最大回文后缀,因为这没有任何意义(自循环)。为了简单起见,我们将其称为后缀edge(这是指除完整字符串之外的最大后缀)。很明显,每个节点只有1个后缀边缘,因为我们不会在树中存储重复的字符串。我们将使用蓝色虚线边缘作为其图片表示形式。

17380071_1339816332769500_1143056574_o

根节点及其约定:
该树/图数据结构将包含2个根虚拟节点。更准确地说,将其视为链接在一起的两棵独立树的根。
根1将是一个虚拟节点,它将描述一个长度= -1的字符串(您可以从实现的角度轻松推断出为什么使用)。根2将是一个节点,它将描述一个长度为0的空字符串。

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

17439531_1339816346102832_1608791293_n

建造回文树

要构建回文树,我们将简单地将字符逐一插入字符串,直到到达结尾为止。插入完成后,我们将与回文树一起使用,其中将包含给定字符串的所有不同回文子字符串。我们需要确保的是,在每次插入新字符,我们的回文树都保留上述功能。让我们看看如何实现它。

假设我们给了一个长度为l的字符串s ,并将该字符串插入直到索引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链接到X中已经存在的S [k + 1] + X + S [k + 1]节点那个树。我们也不需要添加后缀链接,因为该节点将已经包含其后缀链接。

考虑一个长度为4的字符串s =“ abba ”。

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

17467920_1341903835894083_903983119_n

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

17439827_1341903842560749_390303617_n

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

17410351_1341903849227415_701414514_n

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

17474730_1341903845894082_542267262_n

阶段4:我们将插入s [3]即’ a ‘。插入过程从当前节点开始,在这种情况下,当前节点本身是最大的X字符串,因此s [0] + X + s [3]是回文。因此,我们将创建一个新节点“ abba ”,并将插入边从当前节点“ 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

输出:

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 =“ aaaaaabcccccccccdeeeeeeeeeeef” 。在这种情况下,对于连续字符的类似条纹,每个索引将花费额外的2次迭代才能在后缀链接中找到字符串X和Y,但是一旦到达某个索引i ,则s [i]!= s [i- 1]最大长度后缀的最左边的指针将达到其最右边的限制。因此,对于所有当s [i]!= s [i-1]的i ,它将花费总共n次迭代(每次迭代的总和),而对于其余的i,当s [i] == s [i-1]时,它将花费进行2次迭代,将所有此类i相加,并进行2 * n次迭代。因此,在这种情况下,我们的复杂度大约为O(3 * n)〜O(n)。因此,我们可以粗略地说出常数因子“ k”将非常小。因此,我们可以认为总体复杂度为线性O( 字符串的长度) 。您可以参考参考链接以更好地理解。

参考 :

  • http://codeforces.com/blog/entry/13959
  • http://adilet.org/blog/25-09-14/