回文树

1|0概念

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

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

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

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

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

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

构造时用增量法即可。

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);

2|0应用

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

字符串中一个位置的回文后缀个数即为该位置对应的节点的 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]; } }

3|0最小回文划分

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

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

fi=minj=1i1fj+1

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

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

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

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

diff: 节点 xfailx 所对应的回文串的长度差。

slink: 节点 x 沿 fail 向上第一个节点 yfail,其中 y 满足 diffydiffx,即 x 所在的等差数列中回文串长度最小的节点的 fail

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

gx=slinkp=slinkxfilenp

如图,gx 为橙色位置的 f 之和,gfailx 为蓝色位置的 f 之和,gxgfailx 多的位置是 ilenslinkxdiffx,更新完 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(nlogn)

Palindrome PartitionReverses


__EOF__

本文作者lhm_
本文链接https://www.cnblogs.com/lhm-/p/13293090.html
关于博主:sjzez 的一名 OI 学生
版权声明:转载标明出处
声援博主:希望得到宝贵的建议
posted @   lhm_liu  阅读(957)  评论(0编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示