计蒜客:后缀数组专题题解
这几天写后缀系列的东西简直写得石乐志,各种奇怪的问题。
不过也是涨姿势了,学到了点新东西。
首先,我不会后缀数组的倍增构造方法,这个东西现想太容易挂,背代码也不是很容易吧。所以接下来的题目我会尽可能用后缀自动机去求解。对于非得用后缀数组height做的题目,我会介绍一种新的,用后缀自动机构建后缀数组的方法。
好了,我们开始:
习题:密码安全度:
两个子串对应的差相等,显然差分。然后我们对这个差分序列建立SAM,对于每一个节点,拓扑排序跑出maxr,minr,然后用min(len,maxr-minr-1)更新答案(为什么-1?因为他让你源序列中不重复,如果不-1的话在原序列中会包含一个重复元素,例如2,5,8,10,13,更新的ans应该是2的说)。
最后答案+1输出即可。
代码:

1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<algorithm> 5 #include<map> 6 #include<queue> 7 #define debug cout 8 using namespace std; 9 const int maxn=4e5+1e2; 10 const int inf=0x3f3f3f3f; 11 12 struct Node { 13 map<int,Node*> ch; 14 Node* fa; 15 int len,deg,mir,mxr; 16 Node() { 17 mir = inf , mxr = -inf; 18 } 19 }ns[maxn],*root,*last; 20 21 int in[maxn>>1]; 22 int n,m,cnt,ans; 23 24 inline Node* NewNode(int ll) { 25 ns[++cnt].len = ll; 26 return ns+cnt; 27 } 28 29 inline void extend(int x,int rr) { 30 Node* p = last; 31 Node* np = NewNode(p->len+1); 32 np->mir = np->mxr = rr; 33 while( p && p->ch.find(x) == p->ch.end() ) 34 p->ch[x] = np, 35 p = p->fa; 36 if( !p ) 37 np->fa = root; 38 else { 39 Node* q = p->ch[x]; 40 if( q->len == p->len + 1 ) 41 np->fa = q; 42 else { 43 Node* nq = NewNode(p->len+1); 44 nq->fa = q->fa; 45 nq->ch = q->ch; 46 np->fa = q->fa = nq; 47 while( p && p->ch[x] == q ) 48 p->ch[x] = nq, 49 p = p->fa; 50 } 51 } 52 last = np; 53 } 54 55 inline void topo() { 56 ans = -1; 57 for(int i=1;i<=cnt;i++) 58 if( ns[i].fa ) 59 ++ns[i].fa->deg; 60 queue<Node*> q; 61 for(int i=1;i<=cnt;i++) 62 if( !ns[i].deg ) 63 q.push(ns+i); 64 while( q.size() ) { 65 const Node* pos = q.front(); q.pop(); 66 if( pos == root ) 67 break; 68 ans = max( ans , min( pos->len , pos->mxr - pos->mir - 1 ) ); 69 pos->fa->mxr = max( pos->fa->mxr , pos->mxr ); 70 pos->fa->mir = min( pos->fa->mir , pos->mir ); 71 if( !--pos->fa->deg ) 72 q.push(pos->fa); 73 } 74 } 75 76 int main() { 77 scanf("%d",&n); 78 for(int i=1;i<=n;i++) 79 scanf("%d",in+i); 80 81 last = root = NewNode(0); 82 for(int i=2;i<=n;i++) 83 extend(in[i]-in[i-1],i-1); 84 topo(); 85 86 printf("%d ",ans+1); 87 88 return 0; 89 }
习题:蒜头君传纸条:
出现至少k次的连续序列,我们构造后缀自动机,然后拓扑排序统计其right集合的大小(right集合大小就是相同子串数QAQ)。如果right集合比k大,则用当前节点的长度更新答案。
代码:

1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<algorithm> 5 #include<map> 6 #include<queue> 7 #define debug cout 8 using namespace std; 9 const int maxn=4e5+1e2; 10 11 struct Node { 12 map<int,Node*> ch; 13 Node* fa; 14 int len,deg,sum; 15 }ns[maxn],*root,*last; 16 17 int in[maxn>>1],n,m,cnt,ans; 18 19 inline Node* NewNode(int ll) { 20 ns[++cnt].len = ll; 21 return ns+cnt; 22 } 23 inline void extend(int x) { 24 Node* p = last; 25 Node* np = NewNode(p->len+1); 26 np->sum = 1; 27 while( p && p->ch.find(x) == p->ch.end() ) 28 p->ch[x] = np, 29 p = p->fa; 30 if( !p ) 31 np->fa = root; 32 else { 33 Node* q = p->ch[x]; 34 if( q->len == p->len + 1 ) 35 np->fa = q; 36 else { 37 Node* nq = NewNode(p->len+1); 38 nq->fa = q->fa; 39 nq->ch = q->ch; 40 q->fa = np->fa = nq; 41 while( p && p->ch[x] == q ) 42 p->ch[x] = nq, 43 p = p->fa; 44 } 45 } 46 last = np; 47 } 48 49 inline void topo() { 50 for(int i=1;i<=cnt;i++) 51 if( ns[i].fa ) 52 ++ns[i].fa->deg; 53 queue<Node*> q; 54 for(int i=1;i<=cnt;i++) 55 if( !ns[i].deg ) 56 q.push(ns+i); 57 while( q.size() ) { 58 const Node* pos = q.front(); q.pop(); 59 if( pos == root ) 60 break; 61 if( pos->sum >= m ) 62 ans = max( ans , pos->len ); 63 pos->fa->sum += pos->sum; 64 if( !--pos->fa->deg ) 65 q.push(pos->fa); 66 } 67 } 68 69 int main() { 70 scanf("%d%d",&n,&m); 71 for(int i=1;i<=n;i++) 72 scanf("%d",in+i); 73 74 last = root = NewNode(0); 75 76 for(int i=1;i<=n;i++) 77 extend(in[i]); 78 topo(); 79 80 81 printf("%d ",ans); 82 83 return 0; 84 }
习题:蒜头君传纸条进阶版:
这对于SAM我这种选手是一道坑题!
首先用SAM做,很容易想到正常求kth子串,然后右端点就是匹配到位置的right的最小值。然而SAM每次求kth子串是线性的,所以所以这样做是n方暴力!我交上去T了一次才知道。
正解非得用sa,及其height数组。
怎么做呢?对于对于从大到小排好序的每一个后缀,其贡献的不同的子串数为n-(sa[i]-1)-heightp[i-1]。(注意我的数组全都是从1开始用的),然后我们对于这个数组跑前缀和,二分一下,就能知道第k个子串出现的字典序最小的位置。
注意:字典序最小的位置不一定是最靠前的位置,例如对于aaaaa,我们查询第1个子串,找到的字典序最小的位置是5,然后就很愉悦地WA了。
正确的做法是先跑出当前答案串的长度len,在维护height[i]>=len的情况下向右边扫,同时用这一段用sa的min值更新区间左答案。
扫的过程可以二分+rmq,然后再rmq查询最小值。由于数据水,我直接暴力更新AC了。
我说过我拒接倍增构建后缀数组,那我怎么做的呢?先反向构建后缀自动机,这样跑出来的fail树就是后缀树了,然后再对fail树进行dfs构建出后缀数组。
为什么这样是正确的?我也不是很明白。自行脑补一下大概是对的吧。
代码:

1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<algorithm> 5 #define lli long long int 6 #define debug cout 7 using namespace std; 8 const int maxn=2e5+1e2; 9 10 char in[maxn]; 11 int fa[maxn],ch[maxn][26],tc[maxn][26],len[maxn],rig[maxn],root,last,cnt; 12 int stk[maxn],top; 13 int sa[maxn],rnk[maxn],height[maxn],sal; 14 lli sum[maxn],l,r,v; 15 int m,li; 16 17 inline int NewNode(int ll) { 18 len[++cnt] = ll; 19 return cnt; 20 } 21 inline void extend(int x,int pos) { 22 int p = last; 23 int np = NewNode(len[p]+1); 24 rig[np] = pos; 25 while( p && !ch[p][x] ) 26 ch[p][x] = np, 27 p = fa[p]; 28 if( !p ) 29 fa[np] = root; 30 else { 31 int q = ch[p][x]; 32 if( len[q] == len[p] + 1 ) 33 fa[np] = q; 34 else { 35 int nq = NewNode(len[p]+1); 36 memcpy(ch[nq],ch[q],sizeof(ch[q])); 37 fa[nq] = fa[q]; 38 fa[q] = fa[np] = nq; 39 while( p && ch[p][x] == q ) 40 ch[p][x] = nq, 41 p = fa[p]; 42 } 43 } 44 last = np; 45 } 46 47 inline void pre(int pos) { 48 for(int i=0;i<26;i++) 49 if( ch[pos][i] && len[ch[pos][i]] == len[pos] + 1 ) { 50 const int &x = ch[pos][i]; 51 stk[++top] = i; 52 tc[fa[x]][stk[len[x]-len[fa[x]]]] = x; 53 pre(x); 54 stk[top--] = '