OSDN Git Service

style: format markdown files with remark-lint
author24OI-bot <15963390+24OI-bot@users.noreply.github.com>
Mon, 30 Sep 2019 03:50:03 +0000 (23:50 -0400)
committer24OI-bot <15963390+24OI-bot@users.noreply.github.com>
Mon, 30 Sep 2019 03:50:03 +0000 (23:50 -0400)
docs/string/sa.md

index 94a1a78..51e34eb 100644 (file)
@@ -1,18 +1,18 @@
 ## 一些约定
 
-字符串相关的定义请参考 [字符串部分简介](./index.md)。
+字符串相关的定义请参考 [字符串部分简介](./index.md) 
 
 字符串下标从 $1$ 开始。
 
-“后缀 $i$” 代指以第 $i$ 个字符开头的后缀。
+“后缀 $i$ ”代指以第 $i$ 个字符开头的后缀。
 
 ## 后缀数组是什么?
 
-后缀数组(Suffix Array)主要是两个数组:$sa$ 和 $rk$
+后缀数组(Suffix Array)主要是两个数组: $sa$ 和 $rk$ 
 
-其中,$sa[i]$ 表示将所有后缀排序后第 $i$ 小的后缀的编号。$rk[i]$ 表示后缀 $i$ 的排名。
+其中, $sa[i]$ 表示将所有后缀排序后第 $i$ 小的后缀的编号。 $rk[i]$ 表示后缀 $i$ 的排名。
 
-这两个数组满足性质:$sa[rk[i]]=rk[sa[i]]=i$
+这两个数组满足性质: $sa[rk[i]]=rk[sa[i]]=i$ 
 
 后缀数组示例:
 
@@ -30,7 +30,7 @@
 
 先对每个长度为 $1$ 的子串(即每个字符)进行排序。
 
-假设我们已经知道了长度为 $w$ 的子串的排名 $rk_w[1..n]$(即,$rk_w[i]$ 表示 $s[i..\min(i+w-1,n)]$ 在 $\{s[x..\min(x+w-1,n)]\ |\ x\in[1,n]\}$ 中的排名),那么,以 $rk_w[i]$ 为第一关键字, $rk_w[i+w]$ 为第二关键字(若 $i+w>n$ 则令 $rk_w[i+w]$ 为无穷小)进行排序,就可以求出 $rk_{2w}[1..n]$
+假设我们已经知道了长度为 $w$ 的子串的排名 $rk_w[1..n]$ (即, $rk_w[i]$ 表示 $s[i..\min(i+w-1,n)]$ 在 $\{s[x..\min(x+w-1,n)]\ |\ x\in[1,n]\}$ 中的排名),那么,以 $rk_w[i]$ 为第一关键字, $rk_w[i+w]$ 为第二关键字(若 $i+w>n$ 则令 $rk_w[i+w]$ 为无穷小)进行排序,就可以求出 $rk_{2w}[1..n]$ 
 
 倍增排序示意图:
 
 
 ??? note "参考代码"
     ```cpp
-    #include <iostream>
+    #include <algorithm>
     #include <cstdio>
     #include <cstring>
-    #include <algorithm>
+    #include <iostream>
     
     using namespace std;
     
-    const int N=1000010;
+    const int N = 1000010;
     
     char s[N];
-    int n,w,sa[N],rk[N<<1],oldrk[N<<1];
+    int n, w, sa[N], rk[N << 1], oldrk[N << 1];
     // 为了防止访问 rk[i+w] 导致数组越界,开两倍数组。
     // 当然也可以在访问前判断是否越界,但直接开两倍数组方便一些。
     
-    int main()
-    {
-        int i,p;
+    int main() {
+      int i, p;
     
-        scanf("%s",s+1);
-        n=strlen(s+1);
-        for (i=1;i<=n;++i) rk[i]=s[i];
+      scanf("%s", s + 1);
+      n = strlen(s + 1);
+      for (i = 1; i <= n; ++i) rk[i] = s[i];
     
-        for (w=1;w<n;++w)
-        {
-            for (i=1;i<=n;++i) sa[i]=i;
-            sort(sa+1,sa+n+1,[](int x,int y){
-                return rk[x]==rk[y]?rk[x+w]<rk[y+w]:rk[x]<rk[y];
-            }); // 这里用到了 lambda
-            memcpy(oldrk,rk,sizeof(rk));
-            // 由于计算 rk 的时候原来的 rk 会被覆盖,要先复制一份
-            for (p=0,i=1;i<=n;++i)
-            {
-                rk[sa[i]]=oldrk[sa[i]]==oldrk[sa[i-1]]&&oldrk[sa[i]+w]==oldrk[sa[i-1]+w]?p:++p;
-                // 若两个子串相同,它们对应的 rk 也需要相同,所以要去重
-            }
+      for (w = 1; w < n; ++w) {
+        for (i = 1; i <= n; ++i) sa[i] = i;
+        sort(sa + 1, sa + n + 1, [](int x, int y) {
+          return rk[x] == rk[y] ? rk[x + w] < rk[y + w] : rk[x] < rk[y];
+        });  // 这里用到了 lambda
+        memcpy(oldrk, rk, sizeof(rk));
+        // 由于计算 rk 的时候原来的 rk 会被覆盖,要先复制一份
+        for (p = 0, i = 1; i <= n; ++i) {
+          rk[sa[i]] = oldrk[sa[i]] == oldrk[sa[i - 1]] &&
+                              oldrk[sa[i] + w] == oldrk[sa[i - 1] + w]
+                          ? p
+                          : ++p;
+          // 若两个子串相同,它们对应的 rk 也需要相同,所以要去重
         }
+      }
     
-        for (i=1;i<=n;++i) printf("%d ",sa[i]);
+      for (i = 1; i <= n; ++i) printf("%d ", sa[i]);
     
-        return 0;
+      return 0;
     }
     ```
 
 
 在刚刚的 $O(n\log^2n)$ 做法中,单次排序是 $O(n\log n)$ 的,如果能 $O(n)$ 排序,就能 $O(n\log n)$ 计算后缀数组了。
 
-前置知识:[计数排序](../basic/counting-sort.md),[基数排序](../basic/radix-sort.md)
+前置知识: [计数排序](../basic/counting-sort.md) , [基数排序](../basic/radix-sort.md) 
 
-由于计算后缀数组的过程中排序的关键字是排名,值域为 $O(n)$,并且是一个双关键字的排序,可以使用基数排序优化至 $O(n)$
+由于计算后缀数组的过程中排序的关键字是排名,值域为 $O(n)$ ,并且是一个双关键字的排序,可以使用基数排序优化至 $O(n)$ 
 
 ??? note "参考代码"
     ```cpp
-    #include <iostream>
+    #include <algorithm>
     #include <cstdio>
     #include <cstring>
-    #include <algorithm>
+    #include <iostream>
+    ```
 
     using namespace std;
 
 实际上,像这样就可以了:
 
 ```cpp
-for (p=0,i=n;i>n-w;--i) id[++p]=i;
-for (i=1;i<=n;++i) if (sa[i]>w) id[++p]=sa[i]-w;
+for (p = 0, i = n; i > n - w; --i) id[++p] = i;
+for (i = 1; i <= n; ++i)
+  if (sa[i] > w) id[++p] = sa[i] - w;
 ```
 
 意会一下,先把 $s[i+w..i+2w-1]$ 为空串(即第二关键字为无穷小)的位置放前面,再把剩下的按排好的顺序放进去。
 
 #### 优化计数排序的值域
 
-每次对 $rk$ 进行去重之后,我们都计算了一个 $p$,这个 $p$ 即是 $rk$ 的值域,将值域改成它即可。
+每次对 $rk$ 进行去重之后,我们都计算了一个 $p$ ,这个 $p$ 即是 $rk$ 的值域,将值域改成它即可。
 
-#### 将 rk[id[i]] 存下来,减少不连续内存访问
+#### 将 rk\[id[i]] 存下来,减少不连续内存访问
 
 这个优化在数据范围较大时效果非常明显。
 
@@ -169,14 +171,15 @@ for (i=1;i<=n;++i) if (sa[i]>w) id[++p]=sa[i]-w;
 
 同样是减少不连续内存访问,在数据范围较大时效果比较明显。
 
-把 `oldrk[sa[i]] == oldrk[sa[i - 1]] && oldrk[sa[i] + w] == oldrk[sa[i - 1] + w]` 替换成 `cmp(sa[i], sa[i - 1], w)`,`bool cmp(int x, int y, int w) { return oldrk[x] == oldrk[y] && oldrk[x + w] == oldrk[y + w]; }`
+把 `oldrk[sa[i]] == oldrk[sa[i - 1]] && oldrk[sa[i] + w] == oldrk[sa[i - 1] + w]` 替换成 `cmp(sa[i], sa[i - 1], w)` , `bool cmp(int x, int y, int w) { return oldrk[x] == oldrk[y] && oldrk[x + w] == oldrk[y + w]; }` 
 
 ??? note "参考代码"
     ```cpp
-    #include <iostream>
+    #include <algorithm>
     #include <cstdio>
     #include <cstring>
-    #include <algorithm>
+    #include <iostream>
+    ```
 
     using namespace std;
 
@@ -226,11 +229,11 @@ for (i=1;i<=n;++i) if (sa[i]>w) id[++p]=sa[i]-w;
 
 #### SA-IS
 
-可以参考 [诱导排序与 SA-IS 算法](https://riteme.site/blog/2016-6-19/sais.html)。
+可以参考 [诱导排序与 SA-IS 算法](https://riteme.site/blog/2016-6-19/sais.html) 
 
 #### DC3
 
-可以参考 [[2009]后缀数组——处理字符串的有力工具 by.罗穗骞][2]。
+可以参考[\[2009\] 后缀数组——处理字符串的有力工具 by. 罗穗骞][2]。
 
 ## 后缀数组的应用
 
@@ -238,15 +241,15 @@ for (i=1;i<=n;++i) if (sa[i]>w) id[++p]=sa[i]-w;
 
 将字符串 $S$ 复制一份变成 $SS$ 就转化成了后缀排序问题。
 
-例题:[「JSOI2007」字符加密](https://www.luogu.org/problem/P4051)
+例题: [「JSOI2007」字符加密](https://www.luogu.org/problem/P4051) 
 
 ### 在字符串中找子串
 
-任务是在线地在主串 $T$ 中寻找模式串 $S$ 。在线的意思是,我们已经预先知道知道主串 $T$ ,但是当且仅当询问时才知道模式串 $S$ 。我们可以先构造出 $T$ 的后缀数组,然后查找子串 $S$。若子串 $S$ 在 $T$ 中出现,它必定是 $T$ 的一些后缀的前缀。因为我们已经将所有后缀排序了,我们可以通过在 $p$ 数组中二分 $S$ 来实现。比较子串 $S$ 和当前后缀的时间复杂度为 $O(|S|)$ ,因此找子串的时间复杂度为 $O(|S|\log |T|)$ 。注意,如果该子串在 $T$ 中出现了多次,每次出现都是在 $p$ 数组中相邻的。因此出现次数可以通过再次二分找到,输出每次出现的位置也很轻松。
+任务是在线地在主串 $T$ 中寻找模式串 $S$ 。在线的意思是,我们已经预先知道知道主串 $T$ ,但是当且仅当询问时才知道模式串 $S$ 。我们可以先构造出 $T$ 的后缀数组,然后查找子串 $S$ 。若子串 $S$ 在 $T$ 中出现,它必定是 $T$ 的一些后缀的前缀。因为我们已经将所有后缀排序了,我们可以通过在 $p$ 数组中二分 $S$ 来实现。比较子串 $S$ 和当前后缀的时间复杂度为 $O(|S|)$ ,因此找子串的时间复杂度为 $O(|S|\log |T|)$ 。注意,如果该子串在 $T$ 中出现了多次,每次出现都是在 $p$ 数组中相邻的。因此出现次数可以通过再次二分找到,输出每次出现的位置也很轻松。
 
 ### 从字符串首尾取字符最小化字典序
 
-例题:[「USACO07DEC」Best Cow Line](https://www.luogu.org/problem/P2870)
+例题: [「USACO07DEC」Best Cow Line](https://www.luogu.org/problem/P2870) 
 
 题意:给你一个字符串,每次从首或尾取一个字符组成字符串,问所有能够组成的字符串中字典序最小的一个。
 
@@ -257,36 +260,37 @@ for (i=1;i<=n;++i) if (sa[i]>w) id[++p]=sa[i]-w;
 
 ??? note "参考代码"
     ```cpp
-    #include <iostream>
     #include <cctype>
     #include <cstdio>
     #include <cstring>
+    #include <iostream>
     
     using namespace std;
     
-    const int N=1000010;
+    const int N = 1000010;
     
     char s[N];
-    int n,sa[N],id[N],oldrk[N<<1],rk[N<<1],px[N],cnt[N];
+    int n, sa[N], id[N], oldrk[N << 1], rk[N << 1], px[N], cnt[N];
+    ```
 
     bool cmp(int x,int y,int w){ return oldrk[x]==oldrk[y]&&oldrk[x+w]==oldrk[y+w]; }
-    
+
     int main()
     {
         int i,w,m=200,p,l=1,r,tot=0;
-    
+
         cin>>n;
         r=n;
-    
+
         for (i=1;i<=n;++i) while (!isalpha(s[i]=getchar()));
         for (i=1;i<=n;++i) rk[i]=rk[2*n+2-i]=s[i];
-    
+
         n=2*n+1;
-    
+
         for (i=1;i<=n;++i) ++cnt[rk[i]];
         for (i=1;i<=m;++i) cnt[i]+=cnt[i-1];
         for (i=n;i>=1;--i) sa[cnt[rk[i]]--]=i;
-    
+
         for (w=1;w<n;w<<=1,m=p)
         {
             for (p=0,i=n;i>n-w;--i) id[++p]=i;
@@ -298,13 +302,13 @@ for (i=1;i<=n;++i) if (sa[i]>w) id[++p]=sa[i]-w;
             memcpy(oldrk,rk,sizeof(rk));
             for (p=0,i=1;i<=n;++i) rk[sa[i]]=cmp(sa[i],sa[i-1],w)?p:++p;
         }
-    
+
         while (l<=r)
         {
             printf("%c",rk[l]<rk[n+1-r]?s[l++]:s[r--]);
             if ((++tot)%80==0) puts("");
         }
-    
+
         return 0;
     }
     ```
@@ -313,19 +317,19 @@ for (i=1;i<=n;++i) if (sa[i]>w) id[++p]=sa[i]-w;
 
 ### LCP(最长公共前缀)
 
-两个字符串 $S$ 和 $T$ 的 LCP 就是最大的 $x$ ($x\le \min(|S|, |T|)$) 使得 $S_i=T_i\ (\forall\ 1\le i\le x)$ 。
+两个字符串 $S$ 和 $T$ 的 LCP 就是最大的 $x$ ( $x\le \min(|S|, |T|)$ ) 使得 $S_i=T_i\ (\forall\ 1\le i\le x)$ 。
 
 下文中以 $lcp(i,j)$ 表示后缀 $i$ 和后缀 $j$ 的最长公共前缀(的长度)。
 
 ### height 数组的定义
 
-$height[i]=lcp(sa[i],sa[i-1])$,即第 $i$ 名的后缀与它前一名的后缀的最长公共前缀。
+ $height[i]=lcp(sa[i],sa[i-1])$ ,即第 $i$ 名的后缀与它前一名的后缀的最长公共前缀。
 
-$height[1]$ 可以视作 $0$
+ $height[1]$ 可以视作 $0$ 
 
 ### O(n) 求 height 数组需要的一个引理
 
-$height[rk[i]]\ge height[rk[i-1]]-1$
+ $height[rk[i]]\ge height[rk[i-1]]-1$ 
 
 证明:
 
@@ -333,72 +337,71 @@ $height[rk[i]]\ge height[rk[i-1]]-1$
 
 当 $height[rk[i-1]]>1$ 时:
 
-设后缀 $i-1$ 为 $aAD$($A$ 是一个长度为 $height[rk[i-1]]-1$ 的字符串),那么后缀 $i$ 就是 $AD$。设后缀 $sa[rk[i-1]-1]$ 为 $aAB$ ,那么 $lcp(i-1,sa[rk[i-1]-1])=aA$。由于后缀 $sa[rk[i-1]-1]+1$ 是 $AB$,一定排在后缀 $i$ 的前面,所以后缀 $sa[rk[i]-1]$ 一定含有前缀 $A$,所以 $lcp(i,sa[rk[i]-1])$ 至少是 $height[rk[i-1]]-1$
+设后缀 $i-1$ 为 $aAD$ ( $A$ 是一个长度为 $height[rk[i-1]]-1$ 的字符串),那么后缀 $i$ 就是 $AD$ 。设后缀 $sa[rk[i-1]-1]$ 为 $aAB$ ,那么 $lcp(i-1,sa[rk[i-1]-1])=aA$ 。由于后缀 $sa[rk[i-1]-1]+1$ 是 $AB$ ,一定排在后缀 $i$ 的前面,所以后缀 $sa[rk[i]-1]$ 一定含有前缀 $A$ ,所以 $lcp(i,sa[rk[i]-1])$ 至少是 $height[rk[i-1]]-1$ 
 
 简单来说:
 
-$i-1$:$aAD$
+ $i-1$ : $aAD$ 
 
-$i$:$AD$
+ $i$ : $AD$ 
 
-$sa[rk[i-1]-1]$:$aAB$
+ $sa[rk[i-1]-1]$ : $aAB$ 
 
-$sa[rk[i-1]-1]+1$:$AB$
+ $sa[rk[i-1]-1]+1$ : $AB$ 
 
-$sa[rk[i]-1]$:$A[B/C]$
+ $sa[rk[i]-1]$ : $A[B/C]$ 
 
-$lcp(i,sa[rk[i]-1])$:$AX$($X$ 可能为空)
+ $lcp(i,sa[rk[i]-1])$ : $AX$ ( $X$ 可能为空)
 
 ### O(n) 求 height 数组的代码实现
 
 利用上面这个引理暴力求即可:
 
 ```cpp
-for (i=1,k=0;i<=n;++i)
-{
-    if (k) --k;
-    while (s[i+k]==s[sa[rk[i]-1]+k]) ++k;
-    ht[rk[i]]=k; //height太长了缩写为ht
+for (i = 1, k = 0; i <= n; ++i) {
+  if (k) --k;
+  while (s[i + k] == s[sa[rk[i] - 1] + k]) ++k;
+  ht[rk[i]] = k;  // height太长了缩写为ht
 }
 ```
 
-$k$ 不会超过 $n$,最多减 $n$ 次,所以最多加 $2n$ 次,总复杂度就是 $O(n)$
+ $k$ 不会超过 $n$ ,最多减 $n$ 次,所以最多加 $2n$ 次,总复杂度就是 $O(n)$ 
 
 ## height 数组的应用
 
 ### 两子串最长公共前缀
 
-$lcp(sa[i],sa[j])=\min\{height[i+1..j]\}$
+ $lcp(sa[i],sa[j])=\min\{height[i+1..j]\}$ 
 
 感性理解:如果 $height$ 一直大于某个数,前这么多位就一直没变过;反之,由于后缀已经排好序了,不可能变了之后变回来。
 
-严格证明可以参考[[2004]后缀数组 by.徐智磊][1]。
+严格证明可以参考[\[2004\] 后缀数组 by. 徐智磊][1]。
 
-有了这个定理,求两子串最长公共前缀就转化为了 [RMQ 问题](../topic/rmq.md)。
+有了这个定理,求两子串最长公共前缀就转化为了 [RMQ 问题](../topic/rmq.md) 
 
 ### 比较一个字符串的两个子串的大小关系
 
 假设需要比较的是 $A=S[a..b]$ 和 $B=S[c..d]$ 的大小关系。
 
-若 $lcp(a, c)\ge\min(|A|, |B|)$,$A<B\iff |A|<|B|$
+若 $lcp(a, c)\ge\min(|A|, |B|)$ , $A<B\iff |A|<|B|$ 
 
-否则,$A<B\iff rk[a]< rk[b]$
+否则, $A<B\iff rk[a]< rk[b]$ 
 
 ### 不同子串的数目
 
 子串就是后缀的前缀,所以可以枚举每个后缀,计算前缀总数,再减掉重复。
 
-“前缀总数”其实就是子串个数,为 $n(n+1)/2$。
+“前缀总数”其实就是子串个数,为 $n(n+1)/2$ 
 
 如果按后缀排序的顺序枚举后缀,每次新增的子串就是除了与上一个后缀的 LCP 剩下的前缀。这些前缀一定是新增的,否则会破坏 $lcp(sa[i],sa[j])=\min\{height[i+1..j]\}$ 的性质。只有这些前缀是新增的,因为 LCP 部分在枚举上一个前缀时计算过了。
 
 所以答案为:
 
-$$\frac{n(n+1)}{2}-\sum\limits_{i=2}^nheight[i]$$
+ $\frac{n(n+1)}{2}-\sum\limits_{i=2}^nheight[i]$ 
 
 ### 出现至少 k 次的子串的最大长度
 
-例题:[「USACO06DEC」Milk Patterns](https://www.luogu.org/problemnew/show/P2852)
+例题: [「USACO06DEC」Milk Patterns](https://www.luogu.org/problemnew/show/P2852) 
 
 ??? note "题解"
     出现至少 $k$ 次意味着后缀排序后有至少连续 $k$ 个后缀的 LCP 是这个子串。
@@ -409,10 +412,11 @@ $$\frac{n(n+1)}{2}-\sum\limits_{i=2}^nheight[i]$$
 
 ??? note "参考代码"
     ```cpp
-    #include <iostream>
     #include <cstdio>
-    #include <set>
     #include <cstring>
+    #include <iostream>
+    #include <set>
+    ```
 
     using namespace std;
 
@@ -473,7 +477,7 @@ $$\frac{n(n+1)}{2}-\sum\limits_{i=2}^nheight[i]$$
 
 ### 连续的若干个相同子串
 
-我们可以枚举连续串的长度 $|s|$ ,按照 $|s|$ 对整个串进行分块,对相邻两块的块首进行 LCP 与 LCS 查询,具体可见 [[2009]后缀数组——处理字符串的有力工具][2]。
+我们可以枚举连续串的长度 $|s|$ ,按照 $|s|$ 对整个串进行分块,对相邻两块的块首进行 LCP 与 LCS 查询,具体可见[\[2009\] 后缀数组——处理字符串的有力工具][2]。
 
 ### 结合并查集
 
@@ -487,10 +491,10 @@ $$\frac{n(n+1)}{2}-\sum\limits_{i=2}^nheight[i]$$
 
 ### 结合单调栈
 
-例题:[「AHOI2013」差异](https://loj.ac/problem/2377)
+例题: [「AHOI2013」差异](https://loj.ac/problem/2377) 
 
 ??? note "题解"
-    被加数的前两项很好处理,为 $n(n-1)(n+1)/2$(每个后缀都出现了 $n-1$ 次,后缀总长是 $n(n+1)/2$),关键是最后一项,即后缀的两两 LCP。
+    被加数的前两项很好处理,为 $n(n-1)(n+1)/2$ (每个后缀都出现了 $n-1$ 次,后缀总长是 $n(n+1)/2$ ),关键是最后一项,即后缀的两两 LCP。
 
     我们知道 $lcp(i,j)=k$ 等价于 $\min\{height[i+1..j]\}=k$。所以,可以把 $lcp(i,j)$ 记作 $\min\{x|i+1\le x\le j, height[x]=lcp(i,j)\}$ 对答案的贡献。
 
@@ -500,9 +504,10 @@ $$\frac{n(n+1)}{2}-\sum\limits_{i=2}^nheight[i]$$
 
 ??? note "参考代码"
     ```cpp
-    #include <iostream>
     #include <cstdio>
     #include <cstring>
+    #include <iostream>
+    ```
 
     using namespace std;
 
@@ -566,7 +571,7 @@ $$\frac{n(n+1)}{2}-\sum\limits_{i=2}^nheight[i]$$
     }
     ```
 
-类似的题目:[「HAOI2016」找相同字符](https://loj.ac/problem/2064) 。
+类似的题目: [「HAOI2016」找相同字符](https://loj.ac/problem/2064) 。
 
 ## 习题
 
@@ -611,10 +616,10 @@ $$\frac{n(n+1)}{2}-\sum\limits_{i=2}^nheight[i]$$
 
 论文:
 
-1. [[2004]后缀数组 by.徐智磊][1]
+1.  [\[2004\] 后缀数组 by. 徐智磊][1]
 
-2. [[2009]后缀数组——处理字符串的有力工具 by.罗穗骞][2]
+2.  [\[2009\] 后缀数组——处理字符串的有力工具 by. 罗穗骞][2]
 
-[1]: https://wenku.baidu.com/view/0dc03d2b1611cc7931b765ce0508763230127479.html "[2004]后缀数组 by.徐智磊"
+[1]: https://wenku.baidu.com/view/0dc03d2b1611cc7931b765ce0508763230127479.html "[2004] 后缀数组 by. 徐智磊"
 
-[2]: https://wenku.baidu.com/view/5b886b1ea76e58fafab00374.html "[2009]后缀数组——处理字符串的有力工具 by.罗穗骞"
\ No newline at end of file
+[2]: https://wenku.baidu.com/view/5b886b1ea76e58fafab00374.html "[2009] 后缀数组——处理字符串的有力工具 by. 罗穗骞"