最近突然捡起了好久不搞的字符串,研究了一下一直觉得很神奇的回文树。
做了十来道题,现在做个小结,至于回文树是什么,随便百度就可以找到精彩的解说,偶就不在这里献丑了。
相比于manacher,回文树要显得强大的多,同样是接近o(n)的复杂度,
回文树只需要多一点的空间,就可以实现许多用manacher实现起来非常复杂的功能。
并且就代码量而言,回文树也足够的简短,作为处理回文串的工具,实在是非常的美妙。
做了这么多回文树的题,可以发现几点回文树的妙用以及稍稍的优化策略。
大概有一下几点,
节点数减2是本质不同的回文子串数量。
统计全部回文子串可以考虑先记录每个点的cnt,到最后向拓扑排序一样往fail(后缀匹配失败指针)对应的节点累加。
如果在线统计的话,每个点的cnt可以代表该回文串对应的后缀回文子串的数量,然后动态加动态统计。
然后是动态在字符串末尾加入字母和删除的操作,因为回文树每次加入,改变的是last指针的位置,以及是否新加入一个节点,
可以另开一个last数组来保存之前的位置,以及一个flag数组来记录新加入的点和边,退回的时候还原last,并且删边删点即可。
还有另外一种前后都可以加字母的操作,这种的话就是为前后开两个last来记录位置,因为是回文串,所以左右是相同的,
那么fail指针是通用的,唯一的一个需要注意的地方是当整个串是回文的时候要把前后两个last合并在一起。
最后是另加新边的问题,目前只遇到过需要加一条half边的,也就是fail的一个变种,可以用类似方法构造。
有时候,回文树是没办法开出足够的数组来存边的,那么可以用链表来建边,以时间来换空间,其实相应的,该方法同样可以用于
ac自动机和后缀自动机,当然,既然可以用链表来存,那么同样的,平衡树其实要更好一些,不过这样问题就显得复杂一些了。
在hust上挂了一场test,上述的各种问题其中都可以见到,点我
稍微一提,关于回文,本人的一点想法,
上述的各种问题,其实相互之间都没有冲突,那么考虑一个这样的问题,
维护一个双端队列,每次可以加入和删除一个int范围的数字,询问当前状态下的回文子串数量(本质相同以及不同两种的)
这个问题相当于融合了上述的各种要点,如果操作次数达到1e6,那么问题就有些变态了。
不过如果能够掌握上述的内容,还是可以搞定的。估计代码要写个150行左右吧。
另一个问题是给出多个字符串,在给定一个匹配串,求匹配串和给出的多个字符串的最长公共回文子串。
这里,其实可以把回文树当做是ac自动机一样来用,同样有tire树和fail指针,那么稍作修改就可以了。
貌似目前还没有看到哪个oj有这两个问题,或者是我做题不够多吧。
struct linklist
{
int nt[maxn], ft[maxn], u[maxn], v[maxn], sz;
void clear() { sz = 0; }
void clear(int x) { ft[x] = -1; }
int get(int x, int y)
{
for (int i = ft[x]; i != -1; i = nt[i])
{
if (u[i] == y) return v[i];
}
return 0;
}//返回next[x][y]
void insert(int x, int y, int z)
{
u[sz] = y; v[sz] = z;
nt[sz] = ft[x]; ft[x] = sz++;
}//相当于next[x][y]=z;
};
struct PalindromicTree
{
const static int maxn = 2e5 + 10;//总大小
linklist next;
LL sz; //节点个数
int last[2], tot[2]; //前端和后端的指针,以及字符串的前后位置
int fail[maxn], len[maxn], cnt[maxn]; //失配指针,节点代表的回文串长度,统计答案用数组
char s[maxn]; //当前字符串数组
void clear()
{
len[1] = -1; len[2] = 0; //初始设定,两个根节点为1和2
fail[1] = fail[2] = 1; //失配指针指向1
cnt[1] = cnt[2] = 0; //初始为0
last[0] = last[1] = (sz = 3) - 2; //双端指针为-1起始为1节点
tot[0] = (tot[1] = T - 1) + 1; //两端设定长度为预计的长度
next.clear(); next.clear(1); next.clear(2); //清空next数组
}
int Node(int length)
{
len[sz] = length; //新建节点
cnt[sz] = 1;
next.clear(sz);
return sz++;
}
int getfail(int x, int k)
{
s[tot[0] - 1] = s[tot[1] + 1] = 0; //保证之前的数据不影响现在
while (s[tot[k]] != s[tot[k] + (k ? -1 : 1)*(len[x] + 1)]) x = fail[x];
return x;
}
int add(char pos, int k) //k表示类型,0为从前端插入,1为从后端插入
{
int x = (s[tot[k] += k ? 1 : -1] = pos) - 'a', y = getfail(last[k], k);
if (!(last[k] = next.get(y, x)))
{
next.insert(y, x, last[k] = Node(len[y] + 2));
fail[last[k]] = len[last[k]] == 1 ? 2 : next.get(getfail(fail[y], k), x);
cnt[last[k]] += cnt[fail[last[k]]];
if (len[last[k]] == tot[1] - tot[0] + 1) last[k ^ 1] = last[k];
}
return cnt[last[k]];
}
}solve;