【BZOJ4566】[HAOI2016] 找相同字符(重拾后缀自动机)

点此看题面

大致题意: 问两个字符串有多少个相同子串,此处认为位置不同、本质相同的子串是不同的方案。

前言

为什么要说重拾后缀自动机啊,明明就从未真正学会过好不好。。。

简单的重拾后缀自动机

感觉以前的博客真是在瞎写,全都写得云里雾里的,根本就没什么干货。。。

今天花了一个多小时重新研究了一遍,才有些明白它究竟在搞什么。

为防止以后又忘记,这里记些要点:

  • 从根沿边到达任一节点,把走过的字符连起来是原串的一个前缀。
  • \(endpos\)表示一个子串在原串中出现位置的集合,每个节点维护的是若干\(endpos\)相同的子串,且这些串的长度必然依次加\(1\)。而每个点的\(len\)维护的是其中最长串的长度。
  • 每个节点表示的串可以视作父节点表示的串在前面加上若干字符得到的,其中最短串的长度是父节点最长串的长度加\(1\)。因此,父节点表示的串必然是该节点表示的串的后缀,该节点的\(endpos\)集合必然被父节点\(endpos\)集合包含,且父节点的每个子节点\(endpos\)集合必然无交。

大概就先这些吧,还有什么可能会写在其他题解里emmm

此题解法

考虑我们建出第一个串的后缀自动机,然后第二个串在第一个串的后缀自动机上跑匹配。

我们枚举第二个串的每一个前缀,并假设当前匹配到了节点\(p\),对应第二个串中的长度为\(k\)

则对于\(p\)的父节点及其所有祖先(列祖列宗,注意不包括\(p\)自身),都一定是第二个串的后缀。统计答案时应加上每一个节点本质不同的串的种数(\(len-len_{fa}\))乘上包含这个后缀的子串个数(\(Sz\)),这可以事先预处理。

然后考虑\(p\)节点,由于\(p\)节点表示的是一个串的集合,而\(len(p)\)可能比\(k\)大(即集合中部分串比\(k\)长),所以\(p\)节点上本质不同的串的种数应该是\(k-len(fa_p)\),同样乘上包含这个后缀的子串个数(\(Sz(p)\))。

具体实现详见代码。

代码

#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 200000
#define LL long long
using namespace std;
int n;char s1[N+5],s2[N+5];
class SuffixAutomation
{
	private:
		int Nt,lst,p[N<<1],t[N<<1];struct node {LL V;int Sz,F,L,S[30];}O[N<<1];
	public:
		I SuffixAutomation() {Nt=lst=1;}
		I void Ins(CI x)//插入字符
		{
			RI p=lst,now=lst=++Nt;O[now].L=O[p].L+1,O[now].Sz=1;
			W(p&&!O[p].S[x]) O[p].S[x]=now,p=O[p].F;if(!p) return (void)(O[now].F=1);
			RI q=O[p].S[x];if(O[p].L+1==O[q].L) return (void)(O[now].F=q);
			RI k=++Nt;(O[k]=O[q]).L=O[p].L+1,O[O[now].F=O[q].F=k].Sz=0;
			W(p&&O[p].S[x]==q) O[p].S[x]=k,p=O[p].F;
		}
		I void Work()//预处理
		{
			RI i,x;for(i=1;i<=Nt;++i) ++t[O[i].L];for(i=1;i<=n;++i) t[i]+=t[i-1];
			for(i=1;i<=Nt;++i) p[t[O[i].L]--]=i;for(i=Nt;i;--i) x=p[i],O[O[x].F].Sz+=O[x].Sz;//统计子树内串的个数
			for(i=1;i<=Nt;++i) x=p[i],O[x].V=O[O[x].F].V+1LL*O[x].Sz*(O[x].L-O[O[x].F].L);//统计这个串及其祖先的答案
		}
		I void Solve(char *s)//第二个串跑匹配
		{
			LL res=0;for(RI i=1,l=strlen(s+1),p=1,k=0,x;i<=l;++i)//枚举字符
			{
				x=s[i]&31;W(p&&!O[p].S[x]) p=O[p].F;if(!p) {p=1,k=0;continue;}//往上跳找到一个有该儿子的节点
				k=min(k,O[p].L)+1,p=O[p].S[x],res+=O[O[p].F].V+1LL*O[p].Sz*(k-O[O[p].F].L);//计算当前前缀的答案
			}printf("%lld\n",res);
		}
}S;
int main()
{
	RI i;for(scanf("%s",s1+1),n=strlen(s1+1),i=1;i<=n;++i) S.Ins(s1[i]&31);
	return S.Work(),scanf("%s",s2+1),S.Solve(s2),0;
}

猜你喜欢

转载自www.cnblogs.com/chenxiaoran666/p/BZOJ4566.html