回文树与最小回文划分

I am in mood for kissing more

what I need is falling for you now,

On daisy you.

回文树的构造

简单讲一下,这个不是重点

考虑一个设一个奇根和一个偶根,分别存储奇回文串和偶回文串

类似 SAM ,我们依然可以考虑用以下几个信息来完成构建

  • \(fail\) 指针
  • \(*ch\) 数组

含义与 SAM 类似,至于如何构建,我们可以先把偶根的 \(fail\) 指向奇根, 然后因为字符串中本质不同的回文串是 \(O(n)\) 的,根据势能分析,暴力跳 \(fail\) 的时间也是 \(O(n)\) 的(每次深度最多加 1 ),所以直接暴力跳 \(fail\) 即可

模板:PAM模板

代码:

/*PAM(回文自动机)*/ 
#include<bits/stdc++.h>
using namespace std;
#define ll long long
int read(){
	char c = getchar();
	int x = 0;
	while(c < '0' || c > '9')		c = getchar();
	while(c >= '0' && c <= '9')		x = x * 10 + c - 48,c = getchar();
	return x;
}
const int N = 5e5 + 10;
struct PAM{
	int len,fail,id;
	int ch[26];
}pam[N<<1];
int ans[N],f[N];
char s[N];
int cnt = 1,now;
int getfail(int pos,int p){
	while(s[pos - pam[p].len - 1] != s[pos])	p = pam[p].fail;
	return p;
}
#define debug(x)	cout<<x<<endl;
void ins(int pos){
	now = getfail(pos,now);
	int c = s[pos] - 'a';
	if(!pam[now].ch[c]){
		int x = ++cnt;
		pam[x].len = pam[now].len + 2;
		pam[x].fail = pam[getfail(pos,pam[now].fail)].ch[c];	
		f[x] = f[pam[x].fail] + 1;	pam[now].ch[c] = x;now = x;
	}else	now = pam[now].ch[c];
	ans[pos] = f[now];
}
int main(){
	now = 1;
	pam[0].len = 0,pam[0].fail = 1;/*偶根*/ 
	pam[1].len = -1;/*奇根*/
	scanf("%s",s+1);
	int n = strlen(s + 1);
//	s[0] = '%';
	for(int i = 1; i <= n; ++i){
		s[i] = (s[i] - 97 + ans[i-1]) % 26 + 97;
		ins(i);
	}
	for(int i = 1; i <= n; ++i)
		printf("%d ",ans[i]);
	return 0;
}

最小回文划分

前置知识:

  • 上文
  • 基础的 border 理论

以下记 \(s\) ~ \(t\) 表示 \(reverse(s) = t\)

参考自:oi-wiki最小回文划分

以下介绍几个引理

  • \(t\)\(s\) 的 border,且 \(|s| \leq 2|t|\),那么 \(s\) 是 回文串,当且仅当 \(t\) 是回文串

\(proof:\)
\(|t| = m\), \(|s| = n\)

\(t\) 回文串,则 \(s[1,m]\) 回文, \(s[n - m + 1,n]\) 回文,设\(|s[n-m+1,m]| = x\), 注意到 \(s[n-m+1,m]\) ~ \(s[1,x]\),且 \(s[n-m+1,m] = s[1,x]\),
那么 \(s[n-m+1,m]\)必然是回文的

中间一部分是回文的,两边是回文的,且相当,
那么整个串必然是回文的

\(s\) 是回文串,类似的可以证明 \(t\) 是回文串

主要是懒得画图,不画图说明又很麻烦

  • \(s\) 的所有回文后缀按长度排序后,可以形成 \(\log|s|\) 段等差数列

证明还是比较简单的,我们考虑所有回文后缀都是最长回文后缀的 border ,然后根据 border 的理论,可以划分成 \(\log|s|\) 段等差数列

首先考虑朴素方程

\(dp_i = 1 + \min\{dp_j\}[s[j+1,i]\ is \ palindrome]\)

为了方便辨识等差数列,我们令 \(dif(x) = len(x) - len(fail(x))\), \(slink(x)\) 为该等差数列的首项(同时也是下一个等差数列的最后一项)所在的位置

我们用 \(g(x)\) 来保存 \(x\) 所在的等差数列所有的答案

考虑增量法,我们不妨思考 \(g(x)\)\(g(fail(x))\) 的关系

  • \(dif(x) = dif(fail(x))\),则这两个同处一个等差数列,根据引理 (1) , \(fail(x)\)\(x\)\(border\) , 即既是后缀也是前缀,(接下来不太好解释,可以参考 oi-wiki 的图),那么我们可以认为 \(g(fail(x))\) 的代表的所有回文串都向后移动了 \(dif(x)\),那么可以发现新增的 \(dp\) 值只有该等差数列首项上面的那个回文串,其长度为 \(dif(x) + len(slink(x))\),用其更新即可

  • 否则的话,那么 \(g(x)\) 仅需存储本身的长度,本身的长度即为 \(len(x)\) , 注意到此时 \(len(x) \equiv len(slink(x)) + dif(x)\)

暴力跳 \(slink\) 即可,复杂度 \(O(n\log n)\)

例题:CF932G

代码:AC code

posted @ 2021-06-21 10:16  y_dove  阅读(277)  评论(1编辑  收藏  举报