題目
給你一個字符串,問所有長度爲的字符串之中,對於子串,和它相似的子串分別是什麼。
“相似”的概念:兩個字符串至多有一個位置的字符不同。
正解
由於比賽的時候基本上都在剛T1,所以這題沒有幹過。
各種暴力,大概都是從快速地判斷子串相等入手。
但是正解用到了一個新的性質:對於字符串和,若,則他倆相似。
原因不解釋。
順着這個思路,很容易(可能)想到建後綴樹和前綴樹。
建樹直接跑SAM,SAM的樹就是反串的後綴樹。
對於兩個字符串和(這個是字符串的編號,並不是字符串的端點):
它們在後綴樹上的的深度(SAM中的),便是它們。
那麼我們考慮在後綴樹的處計算貢獻:以某個點爲根的時候,定下來了。枚舉屬於兩個不同子樹中的點,通過前綴樹計算他們的,如果,它們就是合法的一對。
思考確定的時候,對於某個點,合法的滿足什麼。和在前綴樹上的的深度大於某個值,所以合法的在某棵子樹內。
接下來纔是具體做法:
考慮dsu on tree(可以上網搜,本質上和啓發式合併差不多),先處理完輕兒子,將輕兒子的信息清空;處理重兒子,然後將重兒子的信息繼承過來,暴力每個輕兒子,維護信息。
考慮和兩個樹連通塊合併,前者比後者大,按照啓發式合併的思想,暴力枚舉內的點。
用個數據結構來維護一下在前綴樹中的dfs序區間內,對應的後綴樹上的點出現在的點有多少個。支持單點加,區間查就好了。
對於點,倍增算出合法的在哪個節點的子樹內,然後在數據結構上查。
整個處理完之後,就合併入中,具體來說就是將他們每個點往數據結構里加。
最後,在清空信息的時候,不要忘了數據結構上的信息也要一同清空。
然而,我們只是做到了的貢獻掛在上(),並沒有掛在上。
於是我們再維護一個數據結構。這個支持區間加,單點查:對於點,求出合法的在哪個節點的子樹內,然後在數據結構上對應的區間中加。在最後輸出答案的時候單點查加上貢獻。
不過要注意一下:當從集合中進入集合中,它身上本來是不帶貢獻的(指時與某些產生的貢獻),但是它在某次修改中被連帶着“誤改”過。這怎麼辦呢?
如果用線段樹,可以用粗暴下傳標記的方式來解決這個問題。
其實沒有這個必要。既然是“誤改”的,那就在答案中剪掉嘛……(另外記得,清空信息的時候,單點查詢,將貢獻計入答案)
所以,只需要用樹狀數組就可以解決這個問題。
總時間複雜度
另外,C_C講題的時候講了個在SA上分治的做法。具體就是在區間中找到最小的位置,將區間分成兩半。這個方法和我上面將的這個方法本質上並沒有多大區別,因爲衆所周知,後綴數組就是後綴樹的dfs序,就是相鄰兩個點的深度。
代碼
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的空間不是那麼好承受啊(