利用后缀数组构造后缀树

来源:互联网 发布:网络 在线客服 编辑:程序博客网 时间:2024/06/05 17:44

由于蒟蒻azui前段时间忙着准备省选,并在省选中闷声滚大粗,博客停更了好久。。

省选过后整个人各种颓,整天玩玩泥巴什么的。。。

前段时间学后缀数组的时候上网查相关资料,看到说后缀数组和后缀树是可以相互转化的,并且uoj上有大量通过后缀自动机建出后缀树然后dfs遍历获得后缀数组的模板,但是通过后缀数组来建后缀树的资料确实稀缺。

也许大牛们都觉得这xjbYY一下就可以写了,所以网上没找到对应的代码,那么我来补个坑吧。大牛勿喷。。

先谈谈我的理解吧。。

讲道理后缀数组和后缀树应该是完全等价的,但前两者和后缀自动机不等价,各有千秋。

后缀树的优点就在于它就是后缀trie缩边而来,在数据规模小的时候可以手动构造来观察。同时现有大量树相关的算法和数据结构,所以后缀树可以很容易地和树链剖分,倍增,虚树结合,效率很高。同时由于树的层次感强,DP的时候统计点对方便。缺点是构造比较晦涩,我至今不会Ukkonen算法,之前做后缀树都是离线sam构造的。

后缀数组的优点就是短小精悍,构造简单,空间不需要乘字符集。缺点就是缺乏层次感,做DP的时候需要结合并查集,单调栈之类的东西。

所以说,利用后缀数组来构造后缀树还是有一定互补性的。特别是在有些场合字符串特别长,用ukk或者sam的方式来构造的话内存乘上字符集大小会比较大,然后在字符集很大的时候sam的转移指针必须用map保存,时间上多个log。后缀数组构造后缀树的一个优势就是时空复杂度不受字符集影响(当然字符集超过n需要离散化那就另当别论)。

之前没有实现过真正的后缀树,有些表示可能和Ukk算法里的经典表示不太一样,意会即可。

具体算法流程比较简单。先构造出后缀数组,令sa[i]表示排名i的后缀的位置,height[i]表示lcp(sa[i],sa[i-1])。遍历sa数组的过程也正是后缀树中的叶子节点的dfs序,而height数组就是相邻两个叶子的lcp的高度,然后每次加入一个叶子后上溯至深度恰好为height处,如果不存在这样的节点,分开恰好涵盖住height这个深度的边,新增一个点使其高度恰好为height。然后新增一条边连向当前后缀的终点节点,考虑到后缀树中一条边会贡献这条边的边长那么多本质不同的子串,令这条边的边长为当前后缀的n-sa[i]-height[i]即可。

这棵树是用链式前向星存储的,所以上溯遇到的需要分开的边一定存在于边表的表首,这一点非常完美,为效率提供了保障。但是要注意因为前向星的特性,边是反着存的,建好后缀树后的dfs序是sa数组的反序。但这一般不影响解题。如果确实需要,可以改成vector存图,或者手动翻转链表中的元素,不会影响时间复杂度。

不考虑后缀数组部分,建后缀树的函数是O(n)的。虽然有两层循环,但是显然后缀树中一条边只会在新建的时候被访问一次,且只会被上溯一次,上溯到这条边上面之后这条边永远也不会再被访问到了,而根据后缀树的正确性,只有不超过2n-2条边,所以是线性的。

要注意一般后缀树要在最后添一个字符防止一个后缀成为另一个后缀的前缀。这样也许会在某些题上对答案造成影响,实际实现时可以不那么做,只需打标记看一个节点是否是一个后缀的终点即可。

代码实现如下。因为是用倍增实现的后缀数组,所以比较慢。如果追求速度的话可以换成最新的O(n)的后缀诱导排序算法(sais),sais+build实际运行非常快。

#include<iostream>#include<algorithm>#include<cstdio>#include<cstring>#include<assert.h>#define rep(i,a,b) for(int i=a;i<=b;++i)#define erp(i,a,b) for(int i=a;i>=b;--i)#define LL long longusing namespace std;const int MAXN = 100005;const int MAXS = MAXN * 2;char s[MAXN];int n;int r[MAXN], rnk[MAXN], height[MAXN], sa[MAXN];namespace SA{int wa[MAXN], wb[MAXN], ws[MAXN], wv[MAXN];bool cmp(int*r, int a, int b, int l){return r[a]==r[b] && r[a+l]==r[b+l];}void da(int*r, int n, int m){int i, j, p, *x=wa, *y=wb;for(i=0;i<m;++i) ws[i]=0;for(i=0;i<n;++i) ws[x[i]=r[i]]++;for(i=1;i<m;++i) ws[i] += ws[i-1];for(i=n-1;i>=0;--i) sa[--ws[x[i]]] = i;for (j=1; j<n; j*=2, m=p){for(p=0,i=n-j;i<n;++i) y[p++]=i;for(i=0;i<n;++i) if(sa[i]>=j) y[p++]=sa[i]-j;for(i=0;i<m;++i) ws[i]=0;for(i=0;i<n;++i) wv[i]=x[y[i]];for(i=0;i<n;++i) ws[wv[i]]++;for(i=1;i<m;++i) ws[i]+=ws[i-1];for(i=n-1;i>=0;--i) sa[--ws[wv[i]]] = y[i];for(swap(x,y),i=p=1,x[sa[0]]=0; i<n;++i)x[sa[i]] = cmp(y,sa[i],sa[i-1],j)?p-1:p++;}}void getHeight(int*r, int n){rep(i, 1, n) rnk[sa[i]]=i;for(int i=0,j=0,k=0; i<n; height[rnk[i++]]=k)for(k?k--:0,j=sa[rnk[i]-1]; r[i+k]==r[j+k]; ++k);}} //默认1为根int last = 1, lastlen, ncnt = 1;int fa[MAXN]; //后缀树中的父亲int dis[MAXN]; //节点深度bool en[MAXN]; //标注一个节点是否是一个后缀的终点struct Ed {int to, len;Ed *nxt;} Edges[MAXS], *ecnt = Edges, *adj[MAXS];void adde(int a, int b, int c) //由父亲向儿子连单向边{fa[b] = a; // noticedis[b] = dis[a] + c;(++ecnt)->to = b;ecnt->len = c;ecnt->nxt = adj[a];adj[a] = ecnt;}void buildSuffixTree(){for (int i = 0; i<n; ++i) r[i] = s[i]+1;SA::da(r, n+1, 256);SA::getHeight(r, n);adde(1, ++ncnt, n-sa[1]);last = ncnt;en[last] = 1;for (int i = 2; i<=n; ++i){int h = height[i];int p = last, nowlen = n-sa[i]-height[i];int np = ++ncnt;while (dis[p] > h) p = fa[p];int br = h - dis[p];if (br) //split the edge{int q = ++ncnt, t = adj[p]->to;int len = adj[p]->len; //保持原来的边长和指向fa[q] = p; //新建一个点放在原来的边中间,使其深度恰好为height[i]adj[p]->len = br;adj[p]->to = q;dis[q] = dis[p] + br;adde(q, t, len - br);p = q;}adde(p, np, nowlen);last = np;en[np] = 1;}}int main(){scanf("%s", s);n = strlen(s);buildSuffixTree();return 0;}

实战演练:bzoj4199, noi2015品酒大会

解析:最大乘积一定是由最大值和次大值或最小值和次小值相乘得到,树DP的时候记录一下即可。

效果:没有加fread和fwrite,没有进行常数优化,速度勉强能进大视野第一页,足以说明这个算法的可靠性了。

用的sais算法实现的后缀数组。由于我不是很懂这个算法,直接粘的sais算法的模板囧。。


#include<iostream>#include<algorithm>#include<cstdio>#include<cstring>#include<assert.h>#define rep(i,a,b) for(int i=a;i<=b;++i)#define erp(i,a,b) for(int i=a;i>=b;--i)#define LL long longusing namespace std;const int MAXN = 300005;const int MAXS = MAXN * 2;const int inf = 0x3f3f3f3f;const LL INF = 1ll<<61;template<typename T>inline void get(T&r){register char c,f=0; r=0;do {c=getchar();if(c=='-')f=1;} while(c<'0'||c>'9');do r=r*10+c-'0',c=getchar(); while(c>='0'&&c<='9');if (f) r=-r;} char s[MAXN];int N;int rnk[MAXN], height[MAXN], sa[MAXN]; namespace SA {int s[MAXS], t[MAXS];int p[MAXN], cnt[MAXN], cur[MAXN];#define pushS(x) sa[cur[s[x]]--] = x#define pushL(x) sa[cur[s[x]]++] = x#define inducedSort(v) fill_n(sa, n, -1); fill_n(cnt, m, 0);  \for (int i = 0; i < n; i++) cnt[s[i]]++;  \for (int i = 1; i < m; i++) cnt[i] += cnt[i-1];   \for (int i = 0; i < m; i++) cur[i] = cnt[i]-1;\for (int i = n1-1; ~i; i--) pushS(v[i]);  \for (int i = 1; i < m; i++) cur[i] = cnt[i-1];\for (int i = 0; i < n; i++) if (sa[i] > 0 &&  t[sa[i]-1]) pushL(sa[i]-1); \for (int i = 0; i < m; i++) cur[i] = cnt[i]-1;\for (int i = n-1;  ~i; i--) if (sa[i] > 0 && !t[sa[i]-1]) pushS(sa[i]-1)void sais(int n, int m, int *s, int *t, int *p) {int n1 = t[n-1] = 0, ch = rnk[0] = -1, *s1 = s+n;for (int i = n-2; ~i; i--) t[i] = s[i] == s[i+1] ? t[i+1] : s[i] > s[i+1];for (int i = 1; i < n; i++) rnk[i] = t[i-1] && !t[i] ? (p[n1] = i, n1++) : -1;inducedSort(p);for (int i = 0, x, y; i < n; i++) if (~(x = rnk[sa[i]])) {if (ch < 1 || p[x+1] - p[x] != p[y+1] - p[y]) ch++;else for (int j = p[x], k = p[y]; j <= p[x+1]; j++, k++)if ((s[j]<<1|t[j]) != (s[k]<<1|t[k])) {ch++; break;}s1[y = x] = ch;}if (ch+1 < n1) sais(n1, ch+1, s1, t+n, p+n1);else for (int i = 0; i < n1; i++) sa[s1[i]] = i;for (int i = 0; i < n1; i++) s1[i] = p[sa[i]];inducedSort(s1);}template<typename T>int mapCharToInt(int n, const T *str) {int m = *max_element(str, str+n);fill_n(rnk, m+1, 0);for (int i = 0; i < n; i++) rnk[str[i]] = 1;for (int i = 0; i < m; i++) rnk[i+1] += rnk[i];for (int i = 0; i < n; i++) s[i] = rnk[str[i]] - 1;return rnk[m];}template<typename T>void suffixArray(int n, const T *str) {int m = mapCharToInt(++n, str);sais(n, m, s, t, p);for (int i = 0; i < n; i++) rnk[sa[i]] = i;for (int i = 0, h = height[0] = 0; i < n-1; i++) {int j = sa[rnk[i]-1];while (i+h < n && j+h < n && s[i+h] == s[j+h]) h++;if ((height[rnk[i]] = h)) h--;}}};int last = 1, lastlen, ncnt = 1;int fa[MAXS], dis[MAXS], en[MAXS];LL a[MAXS];LL bg1[MAXS], bg2[MAXS], sm1[MAXS], sm2[MAXS];LL cnt[MAXS], mx[MAXS], f[MAXS], g[MAXS];struct Ed {int to, len;Ed *nxt;} Edges[MAXS], *ecnt = Edges, *adj[MAXS];void adde(int a, int b, int c){fa[b] = a;dis[b] = dis[a] + c;(++ecnt)->to = b;ecnt->len = c;ecnt->nxt = adj[a];adj[a] = ecnt;} void buildSuffixTree(){int n = N;SA::suffixArray(n, s);adde(1, ++ncnt, n-sa[1]);last = ncnt;en[last] = 1;bg1[last] = sm1[last] = a[sa[1]];for (int i = 2; i<=n; ++i){int h = height[i];int p = last, nowlen = n-sa[i]-height[i];int np = ++ncnt;while (dis[p] > h) p = fa[p];int br = h - dis[p];if (br){int q = ++ncnt, t = adj[p]->to;int len = adj[p]->len;fa[q] = p;adj[p]->len = br;adj[p]->to = q;dis[q] = dis[p] + br;adde(q, t, len - br);p = q;}adde(p, np, nowlen);last = np;bg1[np] = sm1[np] = a[sa[i]];en[np] = 1;}}int dfs(int u){int siz = en[u], t;for (Ed*p = adj[u]; p; p=p->nxt){t = dfs(p->to);f[u] += 1ll * t * siz;siz += t;if (bg1[p->to]>bg1[u]) bg2[u]=bg1[u], bg1[u]=bg1[p->to];else if (bg1[p->to]>bg2[u]) bg2[u]=bg1[p->to];if (bg2[p->to]>bg2[u]) bg2[u] = bg2[p->to];if (sm1[p->to]<sm1[u]) sm2[u]=sm1[u], sm1[u]=sm1[p->to];else if (sm1[p->to]<sm2[u]) sm2[u]=sm1[p->to];if (sm2[p->to]<sm2[u]) sm2[u] = sm2[p->to];}cnt[dis[u]] += f[u];if (siz > 1){g[u] = max(sm1[u]*sm2[u], bg1[u]*bg2[u]);mx[dis[u]] = max(mx[dis[u]], g[u]);}return siz;}int main(){scanf("%d", &N);scanf("%s", s);rep(i, 0, N-1) get(a[i]);rep(i, 0, N) mx[i] = -INF;rep(i, 1, N*2) bg1[i]=bg2[i]=-INF, sm1[i]=sm2[i]=INF, g[i]=-INF;buildSuffixTree();dfs(1);erp(i, N-2, 0) cnt[i] += cnt[i+1], mx[i] = max(mx[i], mx[i+1]);rep(i, 0, N-1) printf("%lld %lld\n", cnt[i], (cnt[i]?mx[i]:0));return 0;}

1 0
原创粉丝点击