后綴樹的線性在線構建-Ukkonen算法


Ukkonen算法是一個非常直觀的算法,其思想精妙之處在於不斷加字符的過程中,用字符串上的一段區間來表示一條邊,並且自動擴展,在需要的時候把邊分裂。使用這個算法的好處在於它非常好寫,代碼很短,並且它是在線的,時間復雜度為\(O(n)\) ,是后綴樹構建算法的佳選。

算法

我們保存當前節點now的位置,以及剩下還沒有實際上插入的后綴數量remain。設當前字符串中已插入的字符數量為\(n\)

最開始remain+1,n+1,代表當前字符串中多了一個字符,多了一個需要插入的后綴。很明顯,當前我們要插入后綴的長度為remain,因為后綴是連續的。所以這個后綴的開頭位置為n-remain+1 。如果當前要插入后綴的長度大於當前出邊的長度,那么不斷往后跳直到符合要求。

這時有三種情況:

  • 不存在需要的出邊,那么我們直接加邊即可。
  • 存在需要的出邊,並且所需字符與邊上的字符相同,即要插入的后綴被隱含在這條邊中了,那么我們退出
  • 存在需要的邊,但所需字符與邊上字符不同。這時候我們就需要分裂這條邊。
  • 如果插入了邊,並且當前點為root,那么remain-1

這時候有一個很明顯的問題,如果我們每一次都退回根節點重新查找,那么時間復雜度可以達到\(O(n^2)\)。但我們可以發現一個性質,當前插入的這個后綴的下一個后綴就是我們要插入的下一個后綴。比如說,我們當前插入了abc這個后綴,那么下一個插入后綴必定是bc。這樣,我們每次可以把這一次add中的上一個點通過一種特殊的后綴連接連到這個點,那么我們就可以快速跳link來找下一個插入位置了。

如果我們處理的是子串,那么這樣就夠了,但是如果我們處理的是后綴,那么還需要在最后加入一個沒有出現過的字符來把所有的隱含點拿出來。

時間復雜度

每次跳link的時候需要插入的長度其實都是在減的,而需要插入的長度一共最多為\(O(n)\),所以跳來跳去的部分復雜度為\(O(n)\)。而我們注意到每次插入的復雜度都是\(O(1)\)的,並且后綴樹的節點個數最多為\(2n-1\),所以插入也是\(O(n)\)的,因此總的復雜度為\(O(n)\)

資料

在學這個算法的過程中找到了很多資料,選幾個比較好的出來分享一下:

Visualization of Ukkonen's Algorithm: 一個非常棒的算法可視化的動畫

Ukkonen算法模擬與教程: 良心作者,大家給他點贊!!(后面那個Github上的代碼是錯的不要學)

代碼

這是bzoj3238的代碼。不用管graph那一塊啦,后綴樹就是ST。

#include<cstdio>
#include<cstring>
#include<cctype>
#include<cstdlib>
#include<algorithm>
using namespace std;
typedef long long giant;
const int maxc=28;
const int maxn=1e6+10;
giant ans;
char s[maxn];
struct graph {
	struct edge {
		int v,w,nxt;
	} e[maxn];
	int h[maxn],tot,dep[maxn],size[maxn];
	graph ():tot(0) {}
	void add(int u,int v,int w) {
		e[++tot]=(edge){v,w,h[u]};
		h[u]=tot;
	}
	void dfs(int x,int fa) {
		size[x]=0;
		bool flag=false;
		for (int i=h[x],v=e[i].v;i;i=e[i].nxt,v=e[i].v) {
			flag=true;
			dep[v]=dep[x]+e[i].w;
			dfs(v,x);
			ans-=(giant)dep[x]*size[x]*size[v]*2ll;
			size[x]+=size[v];
		}
		if (!flag) {
			--dep[x];
			size[x]=(dep[x]>dep[fa]);
		}
	}
} G;
struct ST {
	const static int inf=1e8;
	int t[maxn][maxc],len[maxn],start[maxn],link[maxn],s[maxn],tot,n,rem,now;
	ST ():tot(1),n(0),rem(0),now(1) {len[0]=inf;}
	int node(int sta,int l) {
		start[++tot]=sta,len[tot]=l,link[tot]=1;
		return tot;
	}
	void add(int x) {
		s[++n]=x,++rem;
		for (int last=1;rem;) {
			while (rem>len[t[now][s[n-rem+1]]]) rem-=len[now=t[now][s[n-rem+1]]];
			int ed=s[n-rem+1];
			int &v=t[now][ed];
			int c=s[start[v]+rem-1];
			if (!v) {
				v=node(n-rem+1,inf);
				link[last]=now;
				last=now;
			} else if (x==c) {
				link[last]=now;
				last=now;
				break;
			} else {
				int u=node(start[v],rem-1);
				t[u][x]=node(n,inf);
				t[u][c]=v,start[v]+=rem-1,len[v]-=rem-1;
				link[last]=v=u,last=v;
			}
			if (now==1) --rem; else now=link[now];
		}
	}
	void run() {
		for (int i=1;i<=tot;++i) for (int j=1;j<maxc;++j) if (t[i][j]) G.add(i,t[i][j],len[t[i][j]]); 
	}
} st;
int main() {
#ifndef ONLINE_JUDGE
	freopen("test.in","r",stdin);
	freopen("my.out","w",stdout);
#endif
	scanf("%s",s+1);
	int n=strlen(s+1);
	for (int i=1;i<=n;++i) st.add(s[i]-'a'+1);
	st.add(27);
	st.run();
	ans=(giant)(n-1)*n*(n+1)>>1;
	G.dfs(1,0);
	printf("%lld\n",ans);
	return 0;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM