[串串记录] PAM

所谓回文自动机,就是一个能用来做回文问题的自动机。

-1. 待解决的问题

问一个串的本质不同回文串个数。

需要一个线形做法。

以下为 abbaabba 的回文自动机。图示在文末。

0. PAM 的构造

0.1. 性质

考虑建一个树形结构。也就是回文树。

考虑改改前缀树,每跑一个边 tr[x][c],那么串 \(r \to crc\)。(狗头)。所以串是从下往上再往下读。

  • 增量法构造,在 \(s[0\sim i-1]\) 的回文树增加一个 \(s[i]\),只会增加一个节点。

    证明:考虑新产生的最长回文子串,所有新产生的回文串,都是最长回文子串的回文后缀。会发现这些新产生的都能一一对应翻转以后的前面插入过的短串。于是只会增加一个最新回文子串的节点。

于是本质不同回文子串的数目是 \(O(n)\) 的。

你会发现答案就是自动机点数 - 2.

  • fail 指针

和 AC 自动机类似,它的 fail 指针对应最长可匹配的前后缀,PAM 的 fail 则定义为最长回文后缀的位置。

\(s[0\sim i-1]\) 的最长回文子串在 PAM 上的节点为 \(x\)

我们考虑 \(i - 1 - len[x]\)\(s[i]\) 是否匹配,如果匹配,我们就知道这个是最长的了。

考虑 \(s[i]\) 的最长回文子串是 \(s[i-len[i]+1\sim i]\),我们要求 \(s[i-len[i]+2 \sim i - 1]\) 是满足 \(s[i-1-len[x]] = s[i]\) 的最长回文子串,不然和新加的最长是他矛盾。

如果匹配了, 我们就让新节点的 fail 指向匹配的点 \(x\)

0.2. 细节

  1. fail[0] = 1,因为例如插入 \(aabbc\) 时,\(c\) 这个东西他是匹配不了啥的,但是能在奇数根连一条边,形如 \(1\to c\)
    如果是 \(aabbcc\),他是有机会形成 \(cc\) 这样的在 \(0\) 这个根的串,我们需要让他尝试一下。

upd: 我认为我上午对这个的理解有问题,应该是说,每个点跳 fail 找匹配时,到 \(1\) 点时一定满足条件。(一个长度为 1 的回文串)。然后 \(fail[1]\) 无所谓是啥。

  1. 要分类讨论新点是否存在,如果存在那么直接让当前节点转移到这个存在的点。(自动机嘛,维护的是当前 \(s[0\sim i]\) 的最长回文子串对应节点,直接在后面添加这个节点)。
    不存在就新建节点。注意先更新 fail,方法是一直跳 \(fail[x]\) 的 fail,然后更新新点的 \(fail\)。然后设 \(siz[i]\) 表示自动机上 \(i\) 结尾的回文子串数目。
    \(siz[x] = siz[fail[x]] + 1\),表示 \(x\) 比他最长回文后缀多一个回文子串。
    要注意先更新 \(fail[x]\),在更新 fail[x] 的回文树 fail[++idx] = tr[gfail(fail[y], i)][s[i] - 'a'], tr[y][s[i]-'a'] = idx;
    这个很牛逼,插入 abbbc。比如说我在插入 \(b\) 时这么写。
    插入 b 时,cur 在 2 号点,我们 fail[cur] 在 1 号店,\(y = gfail(fail[cur], i) = 1\),如果已经新建了 \(tr[y][s[i]]\),那么这时 fail[idx] 会指向 idx。(tr[y][s[i]] = idx)。
    还有一个问题,为啥是从 fail[cur] 开始跳来更新 fail[idx]。因为如果你从 cur 开始跳,会得到当前节点就是匹配了的,那么 \(fail[idx] = gfail(y, i) = y\)

upd:以上有点扯淡。我来捋一捋。

增量法新增一个节点1. 存在转移,则直接转移 2. 不存在转移,考虑什么情况能转移。设 \(x\) 表示 \(s[0\sim i - 1]\) 的最长回文子串对应节点,那么需要满足 \(s[i] = s[i - len[x] - 1]\),且中间得是个回文串。那么考虑设 \(fail[x]\) 表示 \(x\) 对应的最长回文后缀。每次跳 fail,check 刚刚这个条件。因为新增要求最长,所以第一次合法即停止。然后我们考虑新建节点 \(idx\)len[idx] = len[x] + 2 这个没啥问题。对于洛谷模板题,定义 \(siz[x]\) 表示 \(x\) 结尾的回文子串个数。其实就是 \(fail\) 树上的祖先个数。(每个祖先对应一个回文后缀)。考虑更新 \(fail[idx]\),我们需要的是去掉左右字符后,也就是先跳到 \(fail[x]\),再开始 getfail,然后找到某个可以匹配的,他的对应 tr[x][id] 即为 \(fail[idx]\)。因为这里是 fail[x] 的后缀,所以需要再走一步。然后我们钦定先更新 fail,再更新 tr。这个上面讲的很清楚。

#include <bits/stdc++.h>
#include <assert.h>
using namespace std;
typedef long long ll;
typedef double db;
#define ep emplace_back
#define pii pair<int,int>
#define fi first
#define se second
#define mp make_pair
#define fout freopen("out.out","w",stdout);
#define fin freopen("in.in","r",stdin);
#define DE {cerr << "QAQ" << endl;}
#define dd(x) cerr << #x" = " << x << endl;
inline int read() {
	int x=0, v=1,ch=getchar();
	while('0'>ch||ch>'9') {
		if(ch=='-') v=0;
		ch=getchar();
	}while('0'<=ch&&ch<='9') {
		x=(x*10)+(ch^'0');
		ch=getchar();
	}return v?x:-x;
}
const int MAX = 5e5 + 5;
char s[MAX];
int fail[MAX],idx=1,tr[MAX][26],len[MAX],siz[MAX];
int gfail(int x,int i){
	while(i-len[x]-1<0||s[i]!=s[i-len[x]-1]) x=fail[x];
	return x;
}
signed main() {
	fin;
	scanf("%s", s);
	int n=strlen(s);
	fail[0]=1; len[1]=-1;
	int x=0,lastans=0;
	for(int i=0;i<n;++i){
		if(i) s[i]=(s[i]-97+lastans)%26+97;
		int y=gfail(x,i);
		if(!tr[y][s[i]-'a']){
			fail[++idx] = tr[gfail(fail[y], i)][s[i] - 'a']; 
			tr[y][s[i]-'a'] = idx;
			len[idx]=len[y]+2;
			siz[idx]=siz[fail[idx]]+1; 
		}
		x=tr[y][s[i]-'a'];
//		printf("%d ", lastans=siz[x]);
	}
	return 0;
}

1. 应用

1.1 本质不同回文串数目

节点数 - 2.

1.2 维护 PAM 上不超过 \(x\) 一半长度的长度最长的点。

这个暴力跳 fail。

	for(int i=0;i<n;++i){
		int y=gfail(x,i);
		if(!tr[y][s[i]-'a']){	
			fail[++idx] = tr[gfail(fail[y], i)][s[i] - 'a']; 
			tr[y][s[i]-'a'] = idx;
			len[idx]=len[y]+2;
			siz[idx]=siz[fail[idx]]+1; 
			if(len[idx] == 1) nxt[idx] = 0; // 长度为 1,那么啥也没有。
			else {
				int pos = nxt[y];
				while(((len[pos] + 2) * 2 > len[idx] || s[i - len[pos] - 1] != s[i])) { // 这里 pos 少记录了两边的节点4】
					pos = fail[pos];
				}
				nxt[idx] = tr[pos][s[i] - 'a'];
			}
			// fail[x] : PAM 上节点 x  
		}
		x=tr[y][s[i]-'a'];
	}

1.3 在 PAM 上 dp。

  • [CERC2014]某个神秘题

开头末尾添加一个字符 / 添加一个当前字符串的反串到末尾。最小操作次数得到某个串

  • 答案是回文子串 + 若干加字符操作。
  • 可以归纳证明偶数串最后一步一定可以是操作 2.
  • 奇回文串没用,因为他肯定是某个偶回文转移来的,* 2 + 1 和 (+ 1) * 2 顺序无关竟要。
  • 可以证明转移时只要考虑长度不超过其一半的最大回文后缀拿去拼接。(同时也是回文前缀)。这个想法很好。基于事实 2。 证明:因为你如果通过更小的 * 2 再添加字符。加到这个"长度不超过一半的最大回文后缀",时肯定是操作了添加字符。但是根据 2. 你会发现这并不优。

综合以上。可以写出 dp。

还是他说的清楚。

1.4 在 PAM 上维护某个东西 / 广义后缀 PAM

后者听起来高大上,实际上每次插入重建 last = 0。 即可。。。

  • [The 1st Universal Cup. Stage 1: Shenyang] H

一堆串串,问你有多少个本质不同的回文串 \(P + Q\),满足 \(P + Q\) 也是回文串。定义两个 \(P + Q\) 本质不同,当且仅当 \(P1 \ne Q1\) or \(P2 \ne Q2\).

一个观察是,\(P + Q\) 形如 \(PPPPPPP\)

这里有个神奇图。

对于 PAM 上一个点 \(x\)。维护 \(f[x]\) 表示 PAM 上点 \(x\) 的循环节 \(A\) 的长度。

然后转移,考虑到最长回文后缀,也得是这个形式。(归纳归纳)

就有了这样的转移。

void ins(int i) {
	int id=t[i]-'a';
	x=getfail(x,i);
	if(!tr[x][id]) {
		fail[++idx]=tr[getfail(fail[x],i)][id];
		len[idx]=len[x]+2;
		if(len[idx] - len[fail[idx]] == sz[fail[idx]]) {
			dep[idx] = dep[fail[idx]] + 1; 
			sz[idx] = sz[fail[idx]];
		}else {	
			dep[idx] = 1;
			sz[idx] = len[idx];
		}
		if(t[i]<='z')ans+=dep[idx]*2-1;
		tr[x][id]=idx;
	}
	x=tr[x][id];
}

2.? 图

太大了,不好看文字,懒得让他们变小。




posted @ 2023-03-24 11:23  Lates  阅读(32)  评论(0编辑  收藏  举报