回文树

概念

回文树可以用来处理一个字符串中所有的回文子串。一个串的本质不同回文子串个数最多为 \(n\) 个。

一个字符串的回文树由两棵树组成,一个维护所有长度为奇数的回文子串,一个维护所有长度为偶数的回文子串。树上除根节点外的每个节点都表示串中的一个回文子串。

\(len:\) 节点对应的回文子串长度。

\(fail:\) 指向该节点所对应的回文子串的最长回文后缀所对应的节点。

\(ch:\) 转移到该树的另一个节点,转移为向当前回文子串两端加上一个字符。

为方便处理,偶树的根的 \(len\) 设为 \(0\)\(fail\) 设为奇树的根,奇树的根的 \(len\) 设为 \(-1\)\(fail\) 设为其本身。

构造时用增量法即可。

void init()
{
    len[1]=-1,fail[0]=fail[1]=tot=1;
}
void insert(int i)
{
    int p=las,c=s[i]-'a';
    while(s[i-1-len[p]]!=s[i]) p=fail[p];
    if(ch[p][c])
    {
        las=ch[p][c];
        return;
    }
    int x=fail[p],y=++tot;
    while(s[i-1-len[x]]!=s[i]) x=fail[x];
    fail[y]=ch[x][c],len[y]=len[p]+2,ch[p][c]=las=y,cnt[y]=cnt[fail[y]]+1;
}

\(fail\) 树:

for(int i=0;i<=tot;++i)
    if(i!=1)
    	add(fail[i],i);

应用

一个字符串的本质不同回文子串个数即为其回文树除了两个根的节点个数。

字符串中一个位置的回文后缀个数即为该位置对应的节点的 \(fail\) 链长度。

在维护每个本质不同回文子串的出现次数时,还需在 \(fail\) 树上用儿子来更新父亲。

for(int i=tot;i;--i) cnt[fail[i]]+=cnt[i];

有时还需用到 \(trans\),指向长度小于等于其回文子串长度一半的最长回文后缀的节点,建树时维护即可。

void insert(int i)
{
    int p=las,c=s[i]-'a';
    while(s[i-1-len[p]]!=s[i]) p=fail[p];
    if(ch[p][c])
    {
        las=ch[p][c];
        return;
    }
    int x=fail[p],y=++tot;
    while(s[i-1-len[x]]!=s[i]) x=fail[x];
    fail[y]=ch[x][c],len[y]=len[p]+2,ch[p][c]=las=y;
    if(len[y]<=2) trans[y]=fail[y];
    else
    {
        int q=trans[p];
        while(s[i-1-len[q]]!=s[i]||(len[q]+2)*2>len[y]) q=fail[q];
        trans[y]=ch[q][c];
    }
}

最小回文划分

给定一个字符串 \(s\),求最小的 \(k\),使 \(s\) 划分为 \(k\) 段,且每段 \(s_i\) 都为回文串。

考虑 \(DP\),设 \(f_i\) 为考虑了 \([1,i]\) 这一段的最小划分,得:

\[\large f_i = \min_{j=1}^{i-1} f_j +1 \]

其中合法的转移为 \([j+1,i]\) 为回文串。

因为回文子串的个数是 \(O(n^2)\) 的,所以不能直接做,要考虑优化。

性质:\(s\) 的所有回文后缀按长度排序后,可以划分为 \(\log |s|\) 段等差数列。

因为若不等差时,回文串长度至少缩小为原先的一半。

\(diff:\) 节点 \(x\)\(fail_x\) 所对应的回文串的长度差。

\(slink:\) 节点 \(x\) 沿 \(fail\) 向上第一个节点 \(y\)\(fail\),其中 \(y\) 满足 \(diff_y \not = diff_x\),即 \(x\) 所在的等差数列中回文串长度最小的节点的 \(fail\)

\(g:\) \(x\) 所在的一段等差数列中 \(f\) 之和,且 \(x\) 为该等差数列中最长的回文串,\(i\) 为当前枚举到的位置,即:

\[\large g_x=\sum\limits_{slink_p =slink_x} f_{i-len_p} \]

如图,\(g_x\) 为橙色位置的 \(f\) 之和,\(g_{fail_x}\) 为蓝色位置的 \(f\) 之和,\(g_x\)\(g_{fail_x}\) 多的位置是 \(i-len_{slink_x}-diff_x\),更新完 \(g\) 后,用 \(g\) 更新 \(f\) 即可。

所示代码为求回文划分方案数。

void insert(int i)
{
    int p=las,c=s[i]-'a';
    while(s[i-1-len[p]]!=s[i]) p=fail[p];
    if(ch[p][c])
    {
        las=ch[p][c];
        return;
    }
    int x=fail[p],y=++tot;
    while(s[i-1-len[x]]!=s[i]) x=fail[x];
    fail[y]=ch[x][c],len[y]=len[p]+2,ch[p][c]=las=y,diff[y]=len[y]-len[fail[y]];
    if(diff[y]==diff[fail[y]]) slink[y]=slink[fail[y]];
    else slink[y]=fail[y];
}

......
    
for(int i=1;i<=n;++i)
{
    insert(i);
    for(int p=las;p;p=slink[p])
    {
        g[p]=f[i-len[slink[p]]-diff[p]];
        if(slink[p]!=fail[p]) g[p]=(g[p]+g[fail[p]])%mod;
        f[i]=(f[i]+g[p])%mod;
    }
}

复杂度为 \(O(n \log n)\)

Palindrome PartitionReverses

posted @ 2020-07-13 14:36  lhm_liu  阅读(944)  评论(0编辑  收藏  举报