首页 > 其他分享 >回文自动机PAM从菜到菜

回文自动机PAM从菜到菜

时间:2022-11-08 21:47:01浏览次数:67  
标签:int dif 后缀 从菜 fail border PAM 回文

回文自动机

基础操作

两个初始状态一个长度为 \(0,-1\) 的偶回文根和奇回文根。

转移 \(\delta(x,c)\) 从 \(x\) 节点代表的回文串转移到两端加入字符 \(c\) 后到达的节点。

\(fail(x)\) 指针,指向自身的后缀最长回文串。特殊地,偶回文根指向奇回文根,奇回文根没有 \(fail\) 指针,因为奇回文根不会失配。

怎么构建呢,考虑增量构建法。

加一个字符,假设当前加入的是第 \(p\) 个字符。

我们从上一次到达的点,一直跳 \(fail\) ,找一个边上可以扩展的一个点停下,也就是第一个满足 \(s[p-len_x-1]=s[p]\) 的点 \(x\)。看它是否有 \(\delta(x,s[p])\) 的转移,有就不管,没有就新建节点。新建节点的 \(fail\) 指针通过 \(x\) 再跳一次 \(fail\) 找到第一个满足前面条件的点确定。

为什么从上一次插入时到达的点开始?因为它是以 \(p-1\) 为结尾的一个尽可能长的回文串。为什么只用改第一个满足条件的点?这是因为后面的满足条件的点必定在之前出现过,这个我们稍晚一点给出证明。

关于为什么暴力跳 \(fail\) 的时间复杂度有保证,这是因为我们每次插入时的起点是上一次跳到的点。每次我们跳一次 \(fail\) ,在 \(fail\) 树上的深度至少减一,而连接一个 \(fail\) 只会深度加一,可得复杂度线性。

关于插入时后面的满足条件的点必定出现过的证明。因为后面的点必定为前面的点的后缀,这些点又是回文,所以它前面肯定出现过。以及,状态数是线性的,这个可以通过刚才说的进行归纳证明。因为转移的节点唯一,然后转移数同样是线性的。

边界情况,偶根作为 \(0\) 的话可以解决一起。

【模板】回文自动机(PAM)

#include<bits/stdc++.h>
#define ll long long
#define db double
#define file(a) freopen(#a".in","r",stdin),freopen(#a".out","w",stdout)
#define sky fflush(stdout)
#define gc getchar
#define pc putchar
namespace IO{
	template<class T>
	inline void read(T &s){
		s=0;char ch=gc();bool f=0;
		while(ch<'0'||'9'<ch) {if(ch=='-') f=1;ch=gc();}
		while('0'<=ch&&ch<='9') {s=s*10+(ch^48);ch=gc();}
		if(ch=='.'){
			T p=0.1;ch=gc();
			while('0'<=ch&&ch<='9') {s=s+p*(ch^48);p/=10;ch=gc();}
		}
		s=f?-s:s;
	}
	template<class T,class ...A>
	inline void read(T &s,A &...a){
		read(s);read(a...);
	}
	template<class T>
	inline void print(T x){
		if(x<0) {x=-x;pc('-');}
		static char st[40];
		static int top;
		top=0;
		do{st[++top]=x-x/10*10+'0';}while(x/=10);
		while(top) {pc(st[top--]);}
	}
	template<class T,class ...A>
	inline void print(T s,A ...a){
		print(s);print(a...);
	}
};
using IO::read;
using IO::print;
const int N=5e5+3;
int n;
char la_ans;
struct Pali_auto{
	int len[N+2],fail[N+2];
	int tr[N+2][26];
	ll cnt[N+2];
	char s[N];
	int tot,la,tip;
	inline int newnode(int L){
		len[++tot]=L;
		memset(tr[tot],0,sizeof(tr[tot]) );
		fail[tot]=cnt[tot]=0;
		return tot;
	}
	inline void build(){
		tot=-1;
		la=tip=0;
		newnode(0);
		newnode(-1);
		fail[0]=1;
	}
	inline int extend(int x){
		while(s[tip-len[x]-1]!=s[tip]){
			x=fail[x];
		}
		return x;
	}
	inline ll ins(char ch){
		s[++tip]=ch;
		int c=ch-'a';
		int x=extend(la);
		if(!tr[x][c]){
			int y=newnode(len[x]+2);
			fail[y]=tr[extend(fail[x])][c];
			cnt[y]=cnt[fail[y] ]+1;
			tr[x][c]=y;
		}
		la=tr[x][c];
		return cnt[la];
	}
}t;
char s[N];
int main(){
	file(a);
	scanf("%s",(s+1) );
	n=strlen(s+1);
	t.build();
	for(int i=1;i<=n;++i){
		//pc(s[i]);
		ll k=t.ins(s[i]);
		printf("%lld ",k);
		s[i+1]=(s[i+1]-97+k)%26+97;
	}
	return 0;
}

一点性质

性质1

回文串的 \(border\) 是回文串。其回文后缀为其 \(border\)。

证明:

在 \(s\) 为回文的前提下,其 \(border\) 相等且互为反串,得证。

性质2

一个串 \(s\) 的半长(及以上)的 \(border\) 为回文串,则 \(s\) 为回文串。

证明:

达到半长可覆盖全串,易证。

性质3

回文串 \(x\) ,\(y\) 为 \(x\) 的最长回文真后缀,\(z\) 为 \(y\) 的最长回文真后缀, \(u,v\) 满足 \(x=uy,y=uz\),则:

1.\(|u|\ge |v|\)。

2.若 \(|u|>|v|\) ,则 \(|u|>|z|\) 。

3.若 \(|u|=|v|\),\(u=v\)。

证明:

1.显然 \(|u|\) 为 \(x\) 的最小周期,\(|v|\) 为 \(y\) 的最小周期。由 \(y\) 为 \(x\) 的 \(border\) 兼后缀,\(|u|\) 也为 \(y\) 的周期。若 \(|u|>|v|\) ,则 \(|v|\) 为 \(y\) 的最小周期不成立,矛盾。因此 \(|u|\ge |v|\)。

2.首先 \(v\) 为 \(x\) 前缀,设字符串 \(w\) 满足 \(x=vw\) ,则 \(z\) 为 \(w\) 的 \(border\) (由 \(y=vz\))。假设 \(|u|\le |z|\) ,则 \(|zu|=|w|\le 2|z|\),则 \(w\) 为回文串,\(w\) 为 \(x\) 的 \(border\)。再由 \(|u|>|v|\) ,得 \(|zu|=|w|>|vz|=|y|\),与 \(y\) 为最长回文后缀相矛盾。所以 \(|u|>|z|\)。

3.因为 \(u,v\) 皆为 \(x\) 的前缀,且长度相等,因此 \(u=v\)。

性质4

字符串 \(s\) 的所有回文后缀按长度排序后可划分为 \(O(\log |s|)\) 组等差数列。

证明:

对于 \(s\) 的最长回文后缀的 \(border\) 使用 border理论性质5 即可。

或者用性质3可知每次相邻的三个回文后缀长度,最小的长度必定小于最大的长度的一半。

应用

本质不同回文子串个数

除去初始状态后的状态数量。

回文子串出现次数

在 \(fail\) 树上做个 topo 排序累加一下子树中串出现次数即可。

最小回文划分

问最少将字符串 \(s\) 划分成多少个部分使得其各部分回文。

设 \(f[i]\) 表示前缀 \(s[1,i]\) 的最小回文划分,朴素的DP转移式是:(\(pali\) 为回文缩写)

\[f[i]=\min_{s[i,j+1] is~pali} f[j]+1 \]

回文子串个数是 \(O(n^2)\) 的,直接做会寄。

考虑回文后缀会划分成 \(O(\log |s|)\) 个等差数列。我们可以维护整个等差数列的 \(f\) 贡献和。记 \(g[x]\) 为从 \(x\) 开始的等差数列贡献和,\(dif[x]\) 表示 \(len[x]-len[fail[x] ]\),\(slink[x]\) 为 \(x\) 的 \(fail\) 父亲链上第一个满足 \(dif[p]\not=dif[fail[p] ]\) 的 \(p\)。我们的等差数列的贡献不包括 \(slink\) ,\(slink\) 的贡献放到下一个等差数列中计算。

首先知道一个结论,若 \(x\) 与 \(fail[x]\) 位于同一等差数列,那么 \(fail[x]\) 的上一次出现位置为 \(i-dif[x]\),这个结论我们等会给出证明。那么你发现 $g[fail[x]] $ 保存的贡献只比 \(g[x]\) 存的少一个。因为它原来保存的贡献位置只差为 \(dif[x]\) ,而现在 \(fail[x]\) 向后移动了 \(dif[x]\) 并加上了 \(x\) 的贡献,所以 \(x\) 把原来平移走的贡献补上,多出来的是 \(g[fail[x] ]\) 中最后一个贡献平移之后的结果,这个贡献是 \(f[i-dif[x]-len[slink[x] ]]\)。这么干讲可能比较抽象,我把 OI-wiki 那个图抽过来。

br

大概就是这样?

关于那个结论:“若 \(x\) 与 \(fail[x]\) 位于同一等差数列,那么 \(fail[x]\) 的上一次出现位置为 \(i-dif[x]\) ” 的证明。

啊,首先 \(fail[x]\) 肯定是在 \(i-dif[x]\) 出现过的。只要证明 \(fail[x]\) 在 \((i-dif[x],i)\) 中不曾出现即可。考虑反证,假设 \(fail[x]\) 在区间 \((i-dif[x],i)\) 出现过,由于他们属于同一等差数列(这里指 \(dif\) 相等)有 \(2|fail[x]|\ge |x|\) ,显然 \(fail[x]\) 与前面的 \(i-dif[x]\) 位置的 \(fail[x]\) 有交。记交集为 \(a\),\(b\) 满足 \(ab=fail[x]\) 。显然 \(aba\) 为回文串,这个比 \(fail[x]\) 长,与 \(fail[x]\) 为 \(x\) 的最长回文后缀矛盾,因此得证。

标签:int,dif,后缀,从菜,fail,border,PAM,回文
From: https://www.cnblogs.com/cbdsopa/p/16871305.html

相关文章

  • P4555 最长双回文串 解题报告
    看到回文串,于是就想到了马拉车。马拉车可以帮我们求出每个\(i\)的最大扩展距离,容易得出,双回文串就是两个回文串拼一起。当然,两个回文串必须要相交,不然形不成一个字符串......
  • LeetCode125. 验证回文串
    验证回文串Day:2022-11-7link:https://leetcode.cn/problems/valid-palindromequestion:如果在将所有大写字符转换为小写字符、并移除所有非字母数字字符之后,短语正......
  • C/C++算法从菜鸟到达人
    "IT有得聊”是机械工业出版社旗下IT专业资讯和服务平台,致力于帮助读者在广义的IT领域里,掌握更专业、实用的知识与技能,快速提升职场竞争力。今天为您推荐一本精品图书--C/C++......
  • 【模板】最长回文串长度 manacher
    \(pa_i\)表示以\(i\)为中心的(原串的)回文串长度#include<cstdio>#include<cstring>#include<algorithm>usingnamespacestd;typedeflonglongLL;intn,m,pa[......
  • 判断回文数
    //前提该数是5位数#include<stdio.h>intmain(){ intnumber; intwan,qian,shi,ge; scanf("%d",&number); wan=number/10000; printf("wan=%d\n",w......
  • 动态规划-回文串
    回文串是从左到右和从右到左读起来一样的字符串,变种还有回文子序列,区别就是字符可以不连续。求回文串个数、最长回文串、最长回文序列也是典型的二维动态规划问题。我们......
  • 力扣409(java&python)-最长回文串(简单)
    题目:给定一个包含大写字母和小写字母的字符串 s ,返回 通过这些字母构造成的最长的回文串 。在构造过程中,请注意区分大小写。比如 "Aa" 不能当做一个回文字符串......
  • 力扣HOT100算法题5:最长回文字串
    文章目录​​一、题目​​​​二、方法一:解题思路​​​​三、方法一:代码解析​​​​四、方法二:动态规划​​​​五、方法二:代码解析​​一、题目给你一个字符串s,找到s......
  • c 取一个数内的回文数
     #include<stdio.h>intis_gog(intn){intx=n,sum=0;while(x){sum=sum*10+x%10;x/=10;}returnsum==n;}intmain......
  • SUDO,PAM配置规范说明
    sudosudo简介说明sudo即superuserdo,允许系统管理员让普通用户执行一些或者全部的root命令的一个工具,如mount、reboot、shutdown、init、halt、user等等。这样不仅减少了ro......