後綴自動機學習筆記

做用

後綴自動機\((SAM)\)是一個能解決許多字符串相關問題的有力的數據結構ios

它能夠把一個字符串的全部子串都表示出來c++

並且從根出發的任意一條合法路徑都是該串中的一個子串數組

構造

要表示一個字符串全部的子串,最簡單的方法是對於全部的子串建一棵字典樹數據結構

可是這樣作時間複雜度和空間複雜度都是 \(O(n^2)\)ui

所以考慮把一些沒有用的節點和邊去掉spa

定義 \(endpos(p)\) 爲一個子串 \(p\) 出現的全部位置的右端點標號組成的集合code

關於 \(endpos\) 有以下的性質排序

\(1\)、若是兩個子串的 \(endpos\) 相同,則其中子串一個必然爲另外一個的後綴繼承

\(2\)、對於任意兩個子串 \(t\)\(p\)\(( len_t\le len_p)\),要麼\(endpos(t)\in endpos(p)\),要麼 \(endpos(t) \bigcap endpos(p)=\emptyset\)隊列

\(3\)、對於 \(endpos\) 相同的子串,咱們將它們歸爲一個 \(endpos\) 等價類

對於任意一個 \(endpos\) 等價類,將包含在其中的全部子串依長度從大到小排序,則每個子串的長度均爲上一個子串的長度減 \(1\),且爲上一個子串的後綴

簡單來講,一個 \(endpos\) 等價類內的串的長度連續

\(4\)\(endpos\) 等價類個數的級別爲 \(O(n)\)

若是咱們在一個字符串的前面添加兩個不一樣的字符

能夠把當前的集合分紅兩個沒有交集的集合

相似於線段樹的分割方法能夠達到最大的劃分數 \(2n\)

因此後綴自動機的空間要開 \(2\)

若是把母集做爲分割出來的小集合的父親,就會造成一棵樹,這就是 \(parent\ tree\)

\(parent\ tree\) 上的節點還有一個性質 \(minlen[now]=maxlen[fa]+1\)

這樣對於每個 \(endpos\) 等價類,只須要記錄屬於它的字符串的最大長度便可,最小長度能夠由父親節點推出來

後綴自動機是在 \(parent\) 樹的基礎上構建而來的,\(parent\) 樹的節點就是後綴自動機的節點

固然,後綴自動機不只有 \(parent\ tree\) 的父子關係,也有 \(trie\) 樹那樣的轉移邊

沿着一個節點經過一條轉移邊到達另外一個節點表明着在當前字符串後面添加字符

\(parent\ tree\) 上的父親節點達到兒子節點則表明着在當前字符串前面添加字符

具體構造的時候採用增量法構造

rg int p=lst;
rg int np=lst=++cnt;
len[np]=len[p]+1;

設當前已經構造到了字符串的第 \(n\) 個字符

首先記錄一下添加第 \(n-1\) 個字符時新建的節點 \(p\)

對於當前的位置新開一個節點 \(np\),表明着只含有位置 \(n\)\(endpos\) 集合

顯然字符串 \(s[1 \cdots n]\) 在以前必定沒有出現過,因此它必定屬於 \(np\) 這個點

因此這個集合中最長的字符串是 \(s[1 \cdots n]\),也就是 \(s[1 \cdots n-1]+1\)

for(;p && !ch[p][c];p=fa[p]) ch[p][c]=np;
if(!p) fa[np]=1;

如今咱們要考慮的就是 \(s[2 \cdots n],s[3 \cdots n] \cdots s[n \cdots n]\) 屬於哪個 \(endpos\) 集合

由於咱們不知道它們在以前有沒有出現過

因此去跳 \(np\)\(parent\ tree\) 上的父親

含義就是字符串 \(s[1 \cdots n-1]\) 在前面不斷去掉字符

\(s[1 \cdots n-1],s[2 \cdots n-1] \cdots s[n-1 \cdots n-1]\) 能不能在後面添加一個字符 \(s[n]\)到達一個已有的狀態

若是不能,就表明着當前長度以 \(n\) 結尾的字符串尚未出現過,那麼它顯然也屬於 \(np\) 這個集合

同時由 \(p\)\(np\) 建一條邊,表明着能夠在當前字符串的後面添加字符

若是都到根節點了尚未找到一個以前已經出現過的字符串

只能說明 \(s[n]\) 這個種類的字符以前沒有出現

直接把它連向根節點就好了

rg int q=ch[p][c];
if(len[q]==len[p]+1) fa[np]=q;

不然說明以 \(n\) 爲結尾的子串以前出現過一部分

這些已經出現過的子串的 \(endpos\) 確定不是單獨的一個 \(n\)

因此就不能把它歸到 \(np\) 這個集合中了

並且咱們並不知道這些以 \(s[n]\) 結尾的子符串是否是 \(s[1 \cdots n]\) 的子串

有可能所有是,也有可能只有一部分是

若是所有是的話,咱們就不須要新開一個 \(endpos\) 集合了

只要在以前的 \(endpos\) 集合的基礎上添一個 \(n\) 就好了

不然咱們就要把這兩部分分開,一部分含有 \(n\),一部分不含有 \(n\)

判斷的標準就是 \(len[q]=len[p]+1\)

由於咱們由 \(p\)\(q\) 的含義就是在一個以 \(s[n-1]\) 結尾的字符串的後面加一個字符變成以 \(s[n]\) 結尾的字符串

若是出現了 \(q\) 中最長字符串不是僅僅添加了一個字符獲得的狀況

說明這個最長字符串必定不是以 \(s[n]\) 結尾的

反之亦然

若是知足條件就很好辦了,直接把 \(np\) 的父親設爲 \(q\)

並且此時咱們不用去跳 \(p\) 的父親了

由於 \(p\) 的父親節點的出邊指向的節點必定全是以 \(s[n]\) 結尾的

並且這些字符串必定是 \(q\) 中字符串的後綴

它們的 \(endpos\) 集合都總體加了 \(n\)

rg int nq=++cnt;
len[nq]=len[p]+1;
memcpy(ch[nq],ch[q],sizeof(ch[q]));
fa[nq]=fa[q];
fa[q]=fa[np]=nq;
for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq;

若是不知足呢

確定要把兩個集合分開

新建一個點 \(nq\) 存儲 \(endpos\) 多了 \(n\) 的字符串

顯然這些字符串中長度最長的就是 \(len[p]+1\)

因此 \(len[nq]=len[p]+1\)

由於咱們只是把原來的集合一分爲二,因此原來集合的出邊直接讓兩個兒子繼承就好了

可是父子關係要變一下

由於 \(nq\)\(q\) 是由一個集合分出來的,它們確定知足後綴關係

由於 \(nq\) 的長度更短而且 \(endpos\) 集合中的元素更多

因此要把 \(q\) 的父親設爲 \(nq\)

同理 \(np\) 的父親也是 \(nq\)

最後再把本應該指向 \(nq\) 的邊改過來就好了

void insert(rg int c){
	rg int p=lst;
	rg int np=lst=++cnt;
	len[np]=len[p]+1;
	for(;p && !ch[p][c];p=fa[p]) ch[p][c]=np;
	if(!p) fa[np]=1;
	else {
		rg int q=ch[p][c];
		if(len[q]==len[p]+1) fa[np]=q;
		else {
			rg int nq=++cnt;
			len[nq]=len[p]+1;
			memcpy(ch[nq],ch[q],sizeof(ch[q]));
			fa[nq]=fa[q];
			fa[q]=fa[np]=nq;
			for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
		}
	}
	siz[np]=1;
}

這樣建造出來的後綴自動機是一個\(DAG\)\(AC\)自動機則是一個 \(Trie\) 圖)

和迴文自動機不一樣,長度長的不必定編號大,也就是說 \(1 \sim n\) 不必定是一個拓撲序

因此還要按照長度進行桶排,獲得的纔是最終的拓撲序

廣義後綴自動機

和普通的後綴自動機同理

加入一個新的字符串以前,要把 \(lst\) 重置成 \(1\)

對於以前已經出現過的節點不要重複去建就好了

要注意的是廣義後綴自動機不能按照桶排來肯定拓撲序

要用正常的隊列的寫法

struct SAM{
	int ch[maxn][28],fa[maxn],len[maxn],cnt,siz[maxn];
	int insert(rg int lst,rg int c){
		rg int p=lst;
		if(ch[p][c]){
			rg int q=ch[p][c];
			if(len[q]==len[p]+1) return q;
			else {
				rg int nq=++cnt;
				len[nq]=len[p]+1;
				memcpy(ch[nq],ch[q],sizeof(ch[q]));
				fa[nq]=fa[q];
				fa[q]=nq;
				for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq; 
				return nq;
			}
		}
		rg int np=++cnt;
		len[np]=len[p]+1;
		for(;p && !ch[p][c];p=fa[p]) ch[p][c]=np;
		if(!p) fa[np]=1;
		else {
			rg int q=ch[p][c];
			if(len[q]==len[p]+1) fa[np]=q;
			else {
				rg int nq=++cnt;
				len[nq]=len[p]+1;
				memcpy(ch[nq],ch[q],sizeof(ch[q]));
				fa[nq]=fa[q];
				fa[q]=fa[np]=nq;
				for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq; 
			}
		}
		return np;
	}
	void build(){
		cnt=1;
		n=read();
		for(rg int i=1;i<=n;i++){
			scanf("%s",s+1);
			rg int nlen=strlen(s+1);
			for(rg int lst=1,j=1;j<=nlen;j++){
				lst=insert(lst,s[j]-'a'+1);
				siz[lst]++;
			}
		}
	}
	void calc(){
		rg long long ans=0;
		for(rg int i=1;i<=cnt;i++){
			ans+=len[i]-len[fa[i]];
		}
		printf("%lld\n",ans);
	}
}sam;

例題

P3181 [HAOI2016]找相同字符

題目傳送門

分析

廣義後綴自動機的模板題

對於節點 \(i\),它表明的 \(endpos\) 集合中本質不一樣的字符串一共有 \(len[i]-len[fa[i]]\)

對於兩個字符串,分別記錄一下它們在某個 \(endpos\) 集合中出現的次數

最終的答案就是 \((len[i]-len[fa[i]]) \times siz[i][0] \times siz[i][1]\)

代碼

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#define rg register
inline int read(){
	rg int x=0,fh=1;
	rg char ch=getchar();
	while(ch<'0' || ch>'9'){
		if(ch=='-') fh=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9'){
		x=(x<<1)+(x<<3)+(ch^48);
		ch=getchar();
	}
	return x*fh;
}
const int maxn=2e6+5;
char s[maxn];
struct SAM{
	int ch[maxn][28],fa[maxn],len[maxn],cnt,siz[maxn][2],rd[maxn];
	std::queue<int> q;
	int insert(rg int lst,rg int c){
		rg int p=lst;
		if(ch[p][c]){
			rg int q=ch[p][c];
			if(len[q]==len[p]+1) return q;
			else {
				rg int nq=++cnt;
				len[nq]=len[p]+1;
				memcpy(ch[nq],ch[q],sizeof(ch[q]));
				fa[nq]=fa[q];
				fa[q]=nq;
				for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq; 
				return nq;
			}
		}
		rg int np=++cnt;
		len[np]=len[p]+1;
		for(;p && !ch[p][c];p=fa[p]) ch[p][c]=np;
		if(!p) fa[np]=1;
		else {
			rg int q=ch[p][c];
			if(len[q]==len[p]+1) fa[np]=q;
			else {
				rg int nq=++cnt;
				len[nq]=len[p]+1;
				memcpy(ch[nq],ch[q],sizeof(ch[q]));
				fa[nq]=fa[q];
				fa[q]=fa[np]=nq;
				for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq; 
			}
		}
		return np;
	}
	void build(){
		cnt=1;
		for(rg int i=0;i<=1;i++){
			scanf("%s",s+1);
			rg int nlen=strlen(s+1);
			for(rg int lst=1,j=1;j<=nlen;j++){
				lst=insert(lst,s[j]-'a'+1);
				siz[lst][i]++;
			}
		}
		for(rg int i=1;i<=cnt;i++) rd[fa[i]]++;
		for(rg int i=1;i<=cnt;i++) if(rd[i]==0) q.push(i);
		while(!q.empty()){
			rg int now=q.front();
			q.pop();
			siz[fa[now]][0]+=siz[now][0],siz[fa[now]][1]+=siz[now][1];
			--rd[fa[now]];
			if(rd[fa[now]]==0) q.push(fa[now]);
		}
	}
	void calc(){
		rg long long ans=0;
		for(rg int i=1;i<=cnt;i++){
			ans+=1LL*(len[i]-len[fa[i]])*siz[i][0]*siz[i][1];
		}
		printf("%lld\n",ans);
	}
}sam;
int main(){
	sam.build();
	sam.calc();
	return 0;
}

P4081 [USACO17DEC]Standing Out from the Herd P

題目傳送門

分析

用線段樹合併維護每個 \(emdpos\) 集合中有多少個字符串出現過

若是隻有一種字符串出現過就累加答案

注意有些狀況下後綴自動機上的線段樹合併和普通的線段樹合併有所不一樣

普通的線段樹合併會破壞原來兩顆線段樹的形態,想要查詢以前的信息就不許確了

因此合併的時候要新開一個節點

這道題由於是邊查詢邊統計答案,因此用正常的寫法就行

代碼

#include<cstdio>
#include<cstring>
#include<iostream>
#define rg register
inline int read(){
	rg int x=0,fh=1;
	rg char ch=getchar();
	while(ch<'0' || ch>'9'){
		if(ch=='-') fh=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9'){
		x=(x<<1)+(x<<3)+(ch^48);
		ch=getchar();
	}
	return x*fh;
}
const int maxn=1e6+5;
struct trr{
	int lch,rch,siz;
}tr[maxn*20];
int rt[maxn],cnt,n;
void push_up(rg int da){
	tr[da].siz=tr[tr[da].lch].siz+tr[tr[da].rch].siz;
}
int ad(rg int da,rg int l,rg int r,rg int wz){
	if(!da) da=++cnt;
	if(l==r){
		tr[da].siz=1;
		return da;
	}
	rg int mids=(l+r)>>1;
	if(wz<=mids) tr[da].lch=ad(tr[da].lch,l,mids,wz);
	else tr[da].rch=ad(tr[da].rch,mids+1,r,wz);
	push_up(da);
	return da;
}
int bing(rg int aa,rg int bb,rg int l,rg int r){
	if(!aa || !bb) return aa+bb;
	if(l==r){
		tr[aa].siz+=tr[bb].siz;
		tr[aa].siz=1;
		return aa;
	}
	rg int mids=(l+r)>>1;
	tr[aa].lch=bing(tr[aa].lch,tr[bb].lch,l,mids);
	tr[aa].rch=bing(tr[aa].rch,tr[bb].rch,mids+1,r);
	push_up(aa);
	return aa;
}
int cx(rg int da,rg int l,rg int r){
	if(l==r) return l;
	rg int mids=(l+r)>>1;
	if(tr[tr[da].lch].siz) return cx(tr[da].lch,l,mids);
	else return cx(tr[da].rch,mids+1,r);
}
char s[maxn];
struct SAM{
	int ch[maxn][28],fa[maxn],cnt,len[maxn],tax[maxn],a[maxn],ans[maxn];
	int insert(rg int lst,rg int c){
		rg int p=lst;
		if(ch[p][c]){
			rg int q=ch[p][c];
			if(len[q]==len[p]+1) return q;
			else {
				rg int nq=++cnt;
				len[nq]=len[p]+1;
				memcpy(ch[nq],ch[q],sizeof(ch[q]));
				fa[nq]=fa[q];
				fa[q]=nq;
				for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
				return nq;
			}
		}
		rg int np=++cnt;
		len[np]=len[p]+1;
		for(;p && !ch[p][c];p=fa[p]) ch[p][c]=np;
		if(!p){
			fa[np]=1;
		} else {
			rg int q=ch[p][c];
			if(len[q]==len[p]+1) fa[np]=q;
			else {
				rg int nq=++cnt;
				len[nq]=len[p]+1;
				memcpy(ch[nq],ch[q],sizeof(ch[q]));
				fa[nq]=fa[q];
				fa[q]=fa[np]=nq;
				for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
			}
		}
		return np;
	}
	void build(){
		cnt=1;
		n=read();
		for(rg int i=1;i<=n;i++){
			scanf("%s",s+1);
			rg int nlen=strlen(s+1);
			for(rg int j=1,lst=1;j<=nlen;j++){
				lst=insert(lst,s[j]-'a'+1);
				rt[lst]=ad(rt[lst],1,n,i);
			}
		}
	}
	void calc(){
		for(rg int i=1;i<=cnt;i++) tax[len[i]]++;
		for(rg int i=1;i<=cnt;i++) tax[i]+=tax[i-1];
		for(rg int i=1;i<=cnt;i++) a[tax[len[i]]--]=i;
		for(rg int i=cnt;i>=1;i--){
			rg int p=a[i];
			if(tr[rt[p]].siz==1){
				ans[cx(rt[p],1,n)]+=len[p]-len[fa[p]];
			} 
			rt[fa[p]]=bing(rt[fa[p]],rt[p],1,n);
		}
		for(rg int i=1;i<=n;i++){
			printf("%d\n",ans[i]);
		}
	}
}sam;
int main(){
	sam.build();
	sam.calc();
	return 0;
}

P4770 [NOI2018] 你的名字

題目傳送門

分析

給你一個字符串 \(S\), 有不少組詢問, 每次給定一個 \(T\), 求 \(T\) 中不在 \(S[l:r]\) 中出現的本質不一樣的子串個數

\(T\) 的本質不一樣的子串減去 \(T\)\(S[l:r]\) 中出現的本質不一樣的子串個數

前者很好求,對於 \(T\) 建出後綴自動機,答案就是 \(\sum len[i]-len[fa[i]]\)

關鍵在於如何求出後面的部分

先考慮最簡單的匹配問題

即對於一個字符串 \(T\),求出它與另外一個字符串 \(S\) 的最長公共子串

對於 \(S\) 創建後綴自動機,把初始的位置置爲根節點

對於 \(T\) 從第一個字符開始枚舉

若是後綴自動機的節點上有當前字符的出邊就一直走下去,同時把匹配長度加一

不然就一直跳 \(parent\ tree\) 直到匹配上爲止,把匹配長度置爲當前節點的長度

若是到了根節點還匹配不上,就把匹配長度置爲 \(0\)

now=1,cs=0;
for(rg int i=1;i<=n;i++){
	rg int p=s[i]-'a'+1;
	while(now && !ch[now][p]) now=fa[now],cs=len[now];
	if(!now){
		now=1;
		cs=0;
	} else {
		cs++;
		now=ch[now][p];
	}
	ans=std::max(ans,cs);
}

如今無非是在普通匹配的基礎上加上了 \([l,r]\) 的限制

只要用線段樹合併維護一下 \(endpos\) 集合便可,這裏要寫新開節點的那一種

在線段樹上查詢當前的節點在 \([l,r]\) 中能匹配的最靠右的端點

若是一直不存在就一直向上跳

跳到第一個合法的位置以後還要繼續向上跳

由於越往上 \(endpos\) 集合中含有的元素越多

查詢右端點時獲得的答案也就越靠右

一直跳到當前節點的長度限制答案爲止

還有一個問題就是如何去重

由於不一樣位置本質相同的字符串只算一次

這時候咱們就須要對於 \(T\) 建一個後綴自動機

這樣就能夠求出以每個節點爲結尾的第一次出現的字符串的個數

每一位匹配的長度和這個值取一個 \(min\) 便可

代碼

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#define rg register
const int maxn=1e6+5;
char s[maxn];
int n,t,rt[maxn],trcnt,l,r;
struct trr{
	int lch,rch,mmax;
}tr[maxn*20];
void push_up(rg int da){
	tr[da].mmax=std::max(tr[tr[da].lch].mmax,tr[tr[da].rch].mmax);
}
int ad(rg int da,rg int l,rg int r,rg int wz){
	da=++trcnt;
	if(l==r){
		tr[da].mmax=wz;
		return da;
	}
	rg int mids=(l+r)>>1;
	if(wz<=mids) tr[da].lch=ad(tr[da].lch,l,mids,wz);
	else tr[da].rch=ad(tr[da].rch,mids+1,r,wz);
	push_up(da);
	return da;
}
int bing(rg int aa,rg int bb,rg int l,rg int r){
	if(!aa || !bb) return aa+bb;
	rg int cc=++trcnt,mids=(l+r)>>1;
	if(l==r){
		tr[cc].mmax=std::max(tr[aa].mmax,tr[bb].mmax);
		return cc;
	}
	tr[cc].lch=bing(tr[aa].lch,tr[bb].lch,l,mids);
	tr[cc].rch=bing(tr[aa].rch,tr[bb].rch,mids+1,r);
	push_up(cc);
	return cc;
}
int cx(rg int da,rg int l,rg int r,rg int L,rg int R){
	if(!da) return -1;
	if(l>=L && r<=R) return tr[da].mmax;
	rg int nans=-1,mids=(l+r)>>1;
	if(L<=mids) nans=std::max(nans,cx(tr[da].lch,l,mids,L,R));
	if(R>mids) nans=std::max(nans,cx(tr[da].rch,mids+1,r,L,R));
	return nans;
}
struct SAM{
	int len[maxn],ch[maxn][28],fa[maxn],lst,cnt,id[maxn],tax[maxn],nlen,jl[maxn];
	void insert(rg int c){
		rg int p=lst;
		rg int np=lst=++cnt;
		len[np]=len[p]+1;
		for(;p && !ch[p][c];p=fa[p]) ch[p][c]=np;
		if(!p) fa[np]=1;
		else {
			rg int q=ch[p][c];
			if(len[q]==len[p]+1) fa[np]=q;
			else {
				rg int nq=++cnt;
				len[nq]=len[p]+1;
				memcpy(ch[nq],ch[q],sizeof(ch[q]));
				fa[nq]=fa[q];
				fa[q]=fa[np]=nq;
				for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
			}
		}
	}
	void build(rg int op){
		for(rg int i=1;i<=cnt;i++) fa[i]=0,memset(ch[i],0,sizeof(ch[i]));
		lst=cnt=1;
		nlen=strlen(s+1);
		for(rg int i=1;i<=nlen;i++){
			insert(s[i]-'a'+1);
			jl[i]=len[fa[lst]];
			if(!op) rt[lst]=ad(rt[lst],1,n,i);
		}
		if(!op){
			for(rg int i=1;i<=cnt;i++) tax[len[i]]++;
			for(rg int i=1;i<=nlen;i++) tax[i]+=tax[i-1];
			for(rg int i=1;i<=cnt;i++) id[tax[len[i]]--]=i;
			for(rg int i=cnt;i>=1;i--){
				rg int tmp=id[i];
				if(fa[tmp]) rt[fa[tmp]]=bing(rt[fa[tmp]],rt[tmp],1,n);
			}
		}
	}
}sam1,sam2;
long long solve(rg int l,rg int r){
	rg long long ans=0;
	for(rg int i=1;i<=sam2.cnt;i++) ans+=sam2.len[i]-sam2.len[sam2.fa[i]];
	rg int now=1,cs=0,tmp,nrt,nans=0;
	for(rg int i=1;i<=sam2.nlen;i++){
		rg int p=s[i]-'a'+1;
		tmp=cx(rt[sam1.ch[now][p]],1,n,l,r);
		while(now && tmp==-1){
			now=sam1.fa[now],cs=sam1.len[now];
			tmp=cx(rt[sam1.ch[now][p]],1,n,l,r);
		}
		if(!now) now=1,cs=0;
		else {
			now=sam1.ch[now][p],cs++,nrt=sam1.fa[now],nans=std::min(sam1.len[now],tmp-l+1);
			while(nrt){
				tmp=cx(rt[nrt],1,n,l,r);
				nans=std::max(nans,std::min(tmp-l+1,sam1.len[nrt]));
				if(tmp-l+1>=sam1.len[nrt]) break;
				nrt=sam1.fa[nrt];
			}
			nans=std::min(nans,cs);
			if(nans>sam2.jl[i]) ans-=(nans-sam2.jl[i]);
		}
	}
	return ans;
}
int main(){
	scanf("%s",s+1);
	n=strlen(s+1);
	sam1.build(0);
	scanf("%d",&t);
	rg int l,r;
	for(rg int i=1;i<=t;i++){
		scanf("%s%d%d",s+1,&l,&r);
		sam2.build(1);
		printf("%lld\n",solve(l,r));
	}
	return 0;
}

CF666E Forensic Examination

題目傳送門

分析

對於字符串數組創建廣義後綴自動機

查詢以前先對於串 \(S\) 在後綴自動上跑一遍匹配

對於每個位置記錄以該位置結尾的後綴在後綴自動機上可以匹配的最長的位置以及對應的節點

對於每個 \(endpos\) 集合用線段樹記錄一下它在哪些子串中出現過以及出現的次數

若是要查詢 \(s[l,r]\) 在字符串組中的那一個字符串中出現的次數最多

從預處理出來的以 \(r\) 結尾的後綴可以匹配的最長的位置對應的節點進行樹上倍增

找到第一個長度大於等於 \(r-l+1\) 的位置

此時這個位置所含有的字符串的種類必定是最多的

直接在對應的線段樹上查詢便可

代碼

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<iostream>
#include<queue>
#define rg register
inline int read(){
	rg int x=0,fh=1;
	rg char ch=getchar();
	while(ch<'0' || ch>'9'){
		if(ch=='-') fh=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9'){
		x=(x<<1)+(x<<3)+(ch^48);
		ch=getchar();
	}
	return x*fh;
}
const int maxn=1e6+5;
struct trr{
	int lch,rch,val,jl;
}tr[maxn*20];
int trcnt,rt[maxn];
void push_up(rg int da){
	if(tr[tr[da].lch].val<tr[tr[da].rch].val) tr[da].jl=tr[tr[da].rch].jl;
	else if(tr[tr[da].rch].val<tr[tr[da].lch].val) tr[da].jl=tr[tr[da].lch].jl;
	else tr[da].jl=std::min(tr[tr[da].lch].jl,tr[tr[da].rch].jl);
	tr[da].val=std::max(tr[tr[da].lch].val,tr[tr[da].rch].val);
}
int ad(rg int da,rg int l,rg int r,rg int wz){
	if(!da) da=++trcnt;
	if(l==r){
		tr[da].val++;
		tr[da].jl=wz;
		return da;
	}
	rg int mids=(l+r)>>1;
	if(wz<=mids) tr[da].lch=ad(tr[da].lch,l,mids,wz);
	else tr[da].rch=ad(tr[da].rch,mids+1,r,wz);
	push_up(da);
	return da;
}
int bing(rg int aa,rg int bb,rg int l,rg int r){
	if(!aa || !bb) return aa+bb;
	rg int cc=++trcnt,mids=(l+r)>>1;
	if(l==r){
		tr[cc].val=tr[aa].val+tr[bb].val;
		tr[cc].jl=l;
		return cc;
	}
	tr[cc].lch=bing(tr[aa].lch,tr[bb].lch,l,mids);
	tr[cc].rch=bing(tr[aa].rch,tr[bb].rch,mids+1,r);
	push_up(cc);
	return cc;
}
int cx(rg int da,rg int l,rg int r,rg int L,rg int R){
	if(!da) return 0;
	if(l>=L && r<=R) return da;
	rg int mids=(l+r)>>1,nans=0,tmp;
	if(L<=mids){
		tmp=cx(tr[da].lch,l,mids,L,R);
		if(tr[tmp].val>tr[nans].val) nans=tmp;
		else if(tr[tmp].val==tr[nans].val){
			if(tr[tmp].jl<tr[nans].jl) nans=tmp;
		}
	}
	if(R>mids){
		tmp=cx(tr[da].rch,mids+1,r,L,R);
		if(tr[tmp].val>tr[nans].val) nans=tmp;
		else if(tr[tmp].val==tr[nans].val){
			if(tr[tmp].jl<tr[nans].jl) nans=tmp;
		}
	}
	return nans;
}
char s1[maxn],s2[maxn];
int n,m,q,mat[maxn];
struct SAM{
	int fa[maxn],ch[maxn][28],len[maxn],cnt,nlen,rd[maxn],zx[maxn][22],sta[maxn],tp,mmax[maxn];
	std::queue<int> q;
	int insert(rg int lst,rg int c){
		rg int p=lst;
		if(ch[p][c]){
			rg int q=ch[p][c];
			if(len[q]==len[p]+1) return q;
			else {
				rg int nq=++cnt;
				len[nq]=len[p]+1;
				memcpy(ch[nq],ch[q],sizeof(ch[q]));
				fa[nq]=fa[q];
				fa[q]=nq;
				for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
				return nq;
			}
		}
		rg int np=++cnt;
		len[np]=len[p]+1;
		for(;p && !ch[p][c];p=fa[p]) ch[p][c]=np;
		if(!p) fa[np]=1;
		else {
			rg int q=ch[p][c];
			if(len[q]==len[p]+1) fa[np]=q;
			else {
				rg int nq=++cnt;
				len[nq]=len[p]+1;
				memcpy(ch[nq],ch[q],sizeof(ch[q]));
				fa[nq]=fa[q];
				fa[q]=fa[np]=nq;
				for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
			}
		}
		return np;
	}
	void build(){
		cnt=1;
		for(rg int i=1;i<=m;i++){
			scanf("%s",s2+1);
			nlen=strlen(s2+1);
			for(rg int j=1,lst=1;j<=nlen;j++){
				lst=insert(lst,s2[j]-'a'+1);
				rt[lst]=ad(rt[lst],1,m,i);
			}
		}
		for(rg int i=1;i<=cnt;i++) rd[fa[i]]++,zx[i][0]=fa[i];
		for(rg int i=1;i<=cnt;i++) if(rd[i]==0) q.push(i);
		while(!q.empty()){
			rg int now=q.front();
			q.pop();
			sta[++tp]=now;
			rt[fa[now]]=bing(rt[fa[now]],rt[now],1,m);
			rd[fa[now]]--;
			if(rd[fa[now]]==0) q.push(fa[now]);
		}
		for(rg int i=tp;i>=1;i--){
			rg int now=sta[i];
			for(rg int j=1;j<=20;j++){
				zx[now][j]=zx[zx[now][j-1]][j-1];
			}
		}
	}
	void pre(){
		rg int cs=0,now=1;
		for(rg int i=1;i<=n;i++){
			rg int p=s1[i]-'a'+1;
			while(now && !ch[now][p]) now=fa[now],cs=len[now];
			if(!now) now=1,cs=0;
			else now=ch[now][p],cs++;
			mat[i]=now,mmax[i]=cs;
		}
	}
	void solve(rg int l1,rg int r1,rg int l2,rg int r2){
		if(mmax[r2]<r2-l2+1){
			printf("%d 0\n",l1);
			return;
		}
		rg int now=mat[r2];
		for(rg int i=20;i>=0;i--){
			if(len[zx[now][i]]>=r2-l2+1) now=zx[now][i];
		}
		if(len[now]<r2-l2+1){
			printf("%d 0\n",l1);
			return;
		}
		rg int tmp=cx(rt[now],1,m,l1,r1);
		if(!tmp) printf("%d 0\n",l1);
		else printf("%d %d\n",tr[tmp].jl,tr[tmp].val);
	}
}sam;
int main(){
	scanf("%s",s1+1);
	n=strlen(s1+1);
	m=read();
	sam.build(),sam.pre();
	q=read();
	rg int l1,r1,l2,r2;
	for(rg int i=1;i<=q;i++){
		l1=read(),r1=read(),l2=read(),r2=read();
		sam.solve(l1,r1,l2,r2);
	}
	return 0;
}

P5212 SubString

題目傳送門

分析

一個字符串出現的次數就是它子樹內的權值和

強制在線要用 \(lct\) 維護

由於子樹和很差維護

因此能夠在每一次修改後把當前節點到根的路徑都加上對應的權值

查詢的時候只要單點查詢就好了

代碼

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define rg register
inline int read(){
	rg int x=0,fh=1;
	rg char ch=getchar();
	while(ch<'0' || ch>'9'){
		if(ch=='-') fh=-1;
		ch=getchar();
	}
	while(ch>='0' && ch<='9'){
		x=(x<<1)+(x<<3)+(ch^48);
		ch=getchar();
	}
	return x*fh;
}
const int maxn=3e6+5;
char s[maxn];
std::string chars;
int mask;
void getit(int mask){
	scanf("%s",s);
	chars=s;
	for(int j=0;j<chars.length();j++){
		mask=(mask*131+j)%chars.length();
		char t=chars[j];
		chars[j]=chars[mask];
		chars[mask]=t;
	}
}
struct LCT{
	int ch[maxn][2],fa[maxn],sum[maxn],tag[maxn],rev[maxn],sta[maxn],tp;
	void push_down(rg int da){
		rg int lc=ch[da][0],rc=ch[da][1];
		if(rev[da]){
			rev[lc]^=1,rev[rc]^=1,rev[da]^=1;
			std::swap(ch[da][0],ch[da][1]);
		}
		if(tag[da]){
			if(lc){
				tag[lc]+=tag[da];
				sum[lc]+=tag[da];
			}
			if(rc){
				tag[rc]+=tag[da];
				sum[rc]+=tag[da];
			}
			tag[da]=0;
		}
	}
	bool isroot(rg int da){
		return (ch[fa[da]][0]!=da)&&(ch[fa[da]][1]!=da);
	}
	void xuanzh(rg int x){
		rg int y=fa[x];
		rg int z=fa[y];
		rg int k=(ch[y][1]==x);
		if(!isroot(y)){
			ch[z][ch[z][1]==y]=x;
		}
		fa[x]=z;
		ch[y][k]=ch[x][k^1];
		fa[ch[x][k^1]]=y;
		ch[x][k^1]=y;
		fa[y]=x;
	}
	void splay(rg int x){
		sta[tp=1]=x;
		for(rg int i=x;!isroot(i);i=fa[i]) sta[++tp]=fa[i];
		for(rg int i=tp;i>=1;i--) push_down(sta[i]);
		while(!isroot(x)){
			rg int y=fa[x];
			rg int z=fa[y];
			if(!isroot(y)){
				(ch[z][1]==y)^(ch[y][1]==x)?xuanzh(x):xuanzh(y);
			}
			xuanzh(x);
		}
	}
	void access(rg int x){
		for(rg int y=0;x;y=x,x=fa[x]){
			splay(x);
			ch[x][1]=y;
		}
	}
	void makeroot(rg int x){
		access(x);
		splay(x);
		rev[x]^=1;
		push_down(x);
	}
	int findroot(rg int x){
		access(x);
		splay(x);
		push_down(x);
		while(ch[x][0]){
			x=ch[x][0];
			push_down(x);
		}
		splay(x);
		return x;
	}
	void split(rg int x,rg int y){
		makeroot(x);
		access(y);
		splay(y);
	}
	void link(rg int x,rg int y){
		makeroot(x);
		if(findroot(y)!=x) fa[x]=y;
	}
	void cut(rg int x,rg int y){
		makeroot(x);
		if(findroot(y)==x && fa[y]==x && ch[y][0]==0){
			ch[x][1]=fa[y]=0;
		}
	}
}lct;
struct SAM{
	int ch[maxn][28],fa[maxn],lst,len[maxn],cnt;
	void insert(rg int c){
		rg int p=lst;
		rg int np=lst=++cnt;
		len[np]=len[p]+1;
		for(;p && !ch[p][c];p=fa[p]) ch[p][c]=np;
		if(!p){
			fa[np]=1;
			lct.link(np,fa[np]);
		} else {
			rg int q=ch[p][c];
			if(len[q]==len[p]+1){
				fa[np]=q;
				lct.link(np,fa[np]);
			} else {
				rg int nq=++cnt;
				len[nq]=len[p]+1;
				memcpy(ch[nq],ch[q],sizeof(ch[q]));
				lct.cut(q,fa[q]);
				lct.link(nq,fa[q]);
				lct.link(np,nq);
				lct.link(q,nq);
				fa[nq]=fa[q];
				fa[q]=fa[np]=nq;
				lct.splay(q);
				lct.sum[nq]=lct.sum[q];
				for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
			}
		}
		lct.split(np,1);
		lct.sum[1]++;
		lct.tag[1]++;
	}
	void build(){
		lst=cnt=1;
		scanf("%s",s+1);
		rg int nlen=strlen(s+1);
		for(int i=1;i<=nlen;i++){
			insert(s[i]-'A');
		}
	}
	void ad(){
		rg int nlen=chars.length();
		for(rg int i=0;i<nlen;i++) insert(chars[i]-'A');
	}
	int cx(){
		rg int now=1,nlen=chars.length();
		for(rg int i=0;i<nlen;i++){
			rg int p=chars[i]-'A';
			if(!ch[now][p]) return 0;
			now=ch[now][p];
		}
		lct.splay(now);
		return lct.sum[now];
	}
}sam;
int q;
int main(){
	q=read();
	sam.build();
	for(rg int i=1;i<=q;i++){
		scanf("%s",s+1);
		if(s[1]=='A'){
			getit(mask);
			sam.ad();
		} else {
			getit(mask);
			rg int nans=sam.cx();
			printf("%d\n",nans);
			mask^=nans;
		}
	}
	return 0;
}

P4248 [AHOI2013]差別

題目傳送門

分析

在後綴自動機中兩個字符串中的 \(lcp\) 就是它們在 \(fail\) 樹上的最近公共祖先

題目給出的式子其實就是兩兩之間的路徑長度

枚舉每一條邊的貢獻加起來便可

代碼

#include<cstdio>
#include<cstring>
#include<algorithm>
#define rg register
const int maxn=4e6+5;
char s[maxn];
long long ans;
struct SAM{
	int ch[maxn][28],fa[maxn],lst,cnt,len[maxn],a[maxn],tax[maxn],siz[maxn],n;
	void insert(rg int c){
		rg int p=lst;
		rg int np=lst=++cnt;
		len[np]=len[p]+1;
		for(;p && !ch[p][c];p=fa[p]) ch[p][c]=np;
		if(!p) fa[np]=1;
		else {
			rg int q=ch[p][c];
			if(len[q]==len[p]+1) fa[np]=q;
			else {
				rg int nq=++cnt;
				len[nq]=len[p]+1;
				memcpy(ch[nq],ch[q],sizeof(ch[q]));
				fa[nq]=fa[q];
				fa[q]=fa[np]=nq;
				for(;p && ch[p][c]==q;p=fa[p]) ch[p][c]=nq;
			}
		}
		siz[np]=1;
	}
	void build(){
		scanf("%s",s+1);
		n=strlen(s+1);
		std::reverse(s+1,s+1+n);
		lst=cnt=1;
		for(rg int i=1;i<=n;i++) insert(s[i]-'a'+1);
	}
	void calc(){
		for(rg int i=1;i<=cnt;i++) tax[len[i]]++;
		for(rg int i=1;i<=cnt;i++) tax[i]+=tax[i-1];
		for(rg int i=1;i<=cnt;i++) a[tax[len[i]]--]=i;
		for(rg int i=cnt;i>=1;i--){
			rg int p=a[i];
			siz[fa[p]]+=siz[p];
			ans+=1LL*(len[p]-len[fa[p]])*siz[p]*(n-siz[p]);
		}
		printf("%lld\n",ans);
	}
}sam;
int main(){
	sam.build();
	sam.calc();
	return 0;
}
相關文章
相關標籤/搜索