6567. 【GDOI2020模擬】字符串

題目

給你一個字符串,問所有長度爲mm的字符串之中,對於子串ii,和它相似的子串分別是什麼。
“相似”的概念:兩個字符串至多有一個位置的字符不同。
n1e5n\leq 1e5


正解

由於比賽的時候基本上都在剛T1,所以這題沒有幹過。

各種暴力,大概都是從快速地判斷子串相等入手。
但是正解用到了一個新的性質:對於字符串SSTT,若lcp(S,T)+lcs(S,T)m1lcp(S,T)+lcs(S,T)\geq m-1,則他倆相似。
原因不解釋。

順着這個思路,很容易(可能)想到建後綴樹和前綴樹。
建樹直接跑SAM,SAM的failfail樹就是反串的後綴樹。
對於兩個字符串uuvv(這個是字符串的編號,並不是字符串的端點):
它們在後綴樹上的LCALCA的深度(SAM中的lenlen),便是它們lcplcp
那麼我們考慮在後綴樹的LCALCA處計算貢獻:以某個點爲根的時候,lcplcp定下來了。枚舉屬於兩個不同子樹中的點,通過前綴樹計算他們的lcslcs,如果lcsm1lcplcs\geq m-1-lcp,它們就是合法的一對。
思考lcplcp確定的時候,對於某個點uu,合法的vv滿足什麼。uuvv在前綴樹上的LCALCA的深度大於某個值,所以合法的vv在某棵子樹內。

接下來纔是具體做法:
考慮dsu on tree(可以上網搜,本質上和啓發式合併差不多),先處理完輕兒子,將輕兒子的信息清空;處理重兒子,然後將重兒子的信息繼承過來,暴力每個輕兒子,維護信息。
考慮AABB兩個樹連通塊合併,前者比後者大,按照啓發式合併的思想,暴力枚舉BB內的點。
用個數據結構來維護一下在前綴樹中的dfs序區間內,對應的後綴樹上的點出現在AA的點有多少個。支持單點加,區間查就好了。
對於點uu,倍增算出合法的vv在哪個節點的子樹內,然後在數據結構上查。
整個BB處理完之後,就合併入AA中,具體來說就是將他們每個點往數據結構里加。
最後,在清空信息的時候,不要忘了數據結構上的信息也要一同清空。

然而,我們只是做到了(u,v)(u,v)的貢獻掛在uu上(uBu \in B),並沒有掛在vv上。
於是我們再維護一個數據結構。這個支持區間加,單點查:對於點uu,求出合法的vv在哪個節點的子樹內,然後在數據結構上對應的區間中加。在最後輸出答案的時候單點查加上貢獻。
不過要注意一下:當uu從集合BB中進入集合AA中,它身上本來是不帶貢獻的(指uAu\in A時與某些BB產生的貢獻),但是它在某次修改中被連帶着“誤改”過。這怎麼辦呢?
如果用線段樹,可以用粗暴下傳標記的方式來解決這個問題。
其實沒有這個必要。既然是“誤改”的,那就在答案中剪掉嘛……(另外記得,清空信息的時候,單點查詢,將貢獻計入答案)
所以,只需要用樹狀數組就可以解決這個問題。
總時間複雜度O(nlg2n)O(n\lg^2 n)

另外,C_C講題的時候講了個在SA上分治的做法。具體就是在區間中找到heightheight最小的位置,將區間分成兩半。這個方法和我上面將的這個方法本質上並沒有多大區別,因爲衆所周知,後綴數組就是後綴樹的dfs序,heightheight就是相鄰兩個點的LCALCA深度。


代碼

using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#define N 100010
#define ll long long
int n,m;
char str[N];
struct Node{
	Node *c[26],*fail;
	int len;
} d[N*4],*S1,*S2,*T;
int cnt;
inline void insert(char ch,Node *S){
	Node *nw=&d[++cnt];
	nw->len=T->len+1;
	Node *p=T;
	for (;p && !p->c[ch-'a'];p=p->fail)
		p->c[ch-'a']=nw;
	if (!p)
		nw->fail=S;
	else{
		Node *q=p->c[ch-'a'];
		if (q->len==p->len+1)
			nw->fail=q;
		else{
			Node *clone=&d[++cnt];
			memcpy(clone,q,sizeof *q);
			clone->len=p->len+1;
			q->fail=nw->fail=clone;
			for (;p && p->c[ch-'a']==q;p=p->fail)
				p->c[ch-'a']=clone;
		}
	}
	T=nw;
}
int id1[N],id2[N],re[N*4];
struct EDGE{
	int to;
	EDGE *las;
} e[N*4];
int ne;
EDGE *last[N*4];
int rt1,rt2;
int fa[N*4][20];
int in[N*4],out[N*4],tot;
int dep[N*4],siz[N*4],hs[N*4];
void init(int x){
	for (int i=1;1<<i<=dep[x];++i)
		fa[x][i]=fa[fa[x][i-1]][i-1];
	in[x]=++tot;
	for (EDGE *ei=last[x];ei;ei=ei->las)
		init(ei->to);
	out[x]=tot;
}
void getsiz(int x){
	siz[x]=1;
	for (EDGE *ei=last[x];ei;ei=ei->las){
		getsiz(ei->to);
		siz[x]+=siz[ei->to];
		if (siz[ei->to]>siz[hs[x]])
			hs[x]=ei->to;
	}
}
int find(int x,int tar){
	if (tar>dep[x])
		return 0;
	tar=max(tar,0);
	for (int i=19;i>=0;--i)
		if (dep[fa[x][i]]>=tar)
			x=fa[x][i];
	return x;
}
int _t[N*2],s[N*2];
inline void add(int x,int c,int *t=_t){
	for (;x<=tot;x+=x&-x)
		t[x]+=c;
}
inline int query(int x,int *t=_t){
	int res=0;
	for (;x;x-=x&-x)
		res+=t[x];
	return res;
}
ll sum,ans[N];
void scan(int x,int lim){
	if (re[x]!=-1){
		int y=find(id2[re[x]+m-1],lim);
		if (y){
			ans[re[x]]+=query(out[y])-query(in[y]-1);
			add(in[y],1,s),add(out[y]+1,-1,s);
		}
	}
	for (EDGE *ei=last[x];ei;ei=ei->las)
		scan(ei->to,lim);
}
void insert(int x,int c){
	if (re[x]!=-1){
		add(in[id2[re[x]+m-1]],c);
		ans[re[x]]-=c*query(in[id2[re[x]+m-1]],s);
	}
	for (EDGE *ei=last[x];ei;ei=ei->las)
		insert(ei->to,c);
}
void dfs(int x){
	for (EDGE *ei=last[x];ei;ei=ei->las)
		if (ei->to!=hs[x]){
			dfs(ei->to);
			insert(ei->to,-1);
		}
	if (hs[x])
		dfs(hs[x]);
	if (re[x]!=-1){
		int y=find(id2[re[x]+m-1],m-1-dep[x]);
		if (y){
			ans[re[x]]+=query(out[y])-query(in[y]-1);
			add(in[y],1,s),add(out[y]+1,-1,s);
		}
		add(in[id2[re[x]+m-1]],1);
		ans[re[x]]-=query(in[id2[re[x]+m-1]],s);
	}
	for (EDGE *ei=last[x];ei;ei=ei->las)
		if (ei->to!=hs[x]){
			scan(ei->to,m-1-dep[x]);
			insert(ei->to,1);
		}
}
int main(){
	freopen("string.in","r",stdin);
	freopen("string.out","w",stdout);
	scanf("%d%d%s",&n,&m,str);
	T=S1=&d[++cnt];
	memset(re,255,sizeof re);
	for (int i=n-1;i>=0;--i){
		insert(str[i],S1),id1[i]=T-d;
		re[T-d]=(i+m-1<n?i:-1);
	}
	T=S2=&d[++cnt];
	for (int i=0;i<n;++i)
		insert(str[i],S2),id2[i]=T-d;
	dep[0]=-1;
	for (int i=1;i<=cnt;++i){
		dep[i]=d[i].len;
		if (d[i].fail==NULL)
			continue;
		fa[i][0]=d[i].fail-d;
		e[ne]={i,last[fa[i][0]]};
		last[fa[i][0]]=e+ne++;
	}
	rt1=S1-d,rt2=S2-d;
	init(rt2);
	getsiz(rt1);
	dfs(rt1);
	for (int i=0;i+m-1<n;++i)
		ans[i]+=query(in[id2[i+m-1]],s);
	for (int i=0;i+m-1<n;++i)
		printf("%d ",ans[i]);
	return 0;
}


總結

其實這題並不是很難吧,但是細節有點多,害我搞了一天(應該跟早上邊打程序邊學習政治有關係,所以得到教訓:調試的時候可以分心,打程序的時候儘量不要分心,不然細節很容易掛)
然後就是dsu on tree這個東西,說實話本質上是個用腳趾頭都能想到的啓發式合併。不過也應該要掌握這個概念,不要光想着儘管和它差不多的啓發式合併。
最後:我發現我現在真是越來越不能打SA了。SA雖然思想簡單,但細節遍地都是,極其難寫。半年還是一年來見到SA我都會用SAM代替。然而,
SA是我必須要度過的難關,因爲——SAM的空間不是那麼好承受啊(

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章