OSDN Git Service

:sparkles:(docs/string) update suffix array
authorouuan <y___o___u@126.com>
Mon, 30 Sep 2019 03:41:06 +0000 (11:41 +0800)
committerouuan <y___o___u@126.com>
Mon, 30 Sep 2019 03:41:06 +0000 (11:41 +0800)
docs/string/images/sa1.png
docs/string/images/sa2.png
docs/string/images/sa3.png
docs/string/sa.md

index ea4172a..1383bf0 100644 (file)
Binary files a/docs/string/images/sa1.png and b/docs/string/images/sa1.png differ
index 854711f..b3c9e11 100644 (file)
Binary files a/docs/string/images/sa2.png and b/docs/string/images/sa2.png differ
index 7fc431a..3c7479b 100644 (file)
Binary files a/docs/string/images/sa3.png and b/docs/string/images/sa3.png differ
index ad16fbb..94a1a78 100644 (file)
-## 定义
+## 一些约定
 
®\9aä¹\89 $S$ æ\98¯ä¸\80个é\95¿åº¦ä¸º $n$ ç\9a\84å­\97符串ã\80\82å­\97符串ç\9a\84 $S$ ç¬¬ $i$ ä¸ªå\90\8eç¼\80æ\98¯å­\90串 $S[i \dots n-1]$ 
­\97符串ç\9b¸å\85³ç\9a\84å®\9aä¹\89请å\8f\82è\80\83 [å­\97符串é\83¨å\88\86ç®\80ä»\8b](./index.md)
 
-一个 **后缀数组** 将包含一些整数,表示一个给定字符串所有排序后的后缀的 **起点位置** 
+字符串下标从 $1$ 开始
 
-例如字符串 $S=abaab$ ,它的所有后缀有:
+“后缀 $i$” 代指以第 $i$ 个字符开头的后缀。
 
-$$
-\begin{array}{ll}
-1. & abaab \\
-2. & baab \\
-3. & aab \\
-4. & ab \\
-5. & b
-\end{array}
-$$
+## 后缀数组是什么?
 
\9c¨æ\8e\92åº\8fè¿\99äº\9bå\90\8eç¼\80å\90\8eï¼\9a
\90\8eç¼\80æ\95°ç»\84ï¼\88Suffix Arrayï¼\89主è¦\81æ\98¯ä¸¤ä¸ªæ\95°ç»\84ï¼\9a$sa$ å\92\8c $rk$ã\80\82
 
-$$
-\begin{array}{ll}
-2. & aab \\
-3. & ab \\
-0. & abaab \\
-4. & b \\
-1. & baab
-\end{array}
-$$
+其中,$sa[i]$ 表示将所有后缀排序后第 $i$ 小的后缀的编号。$rk[i]$ 表示后缀 $i$ 的排名。
 
-因此字符串 $S​$ 的后缀数组为 $(2,~ 3,~ 0,~ 4,~ 1)​$ 
+这两个数组满足性质:$sa[rk[i]]=rk[sa[i]]=i$
 
-这是一个广泛用于数据压缩,生物信息学的领域的数据结构以及宽泛的说,用在任何处理字符串和字符串匹配的题目。
+后缀数组示例:
 
-子串:就是子串。
+[![](./images/sa1.png)][2]
 
-后缀:就是从 $i$ 这个位置开始到该字符串的末尾的一个子串。
+## 后缀数组怎么求?
 
-字符串大小比较:把 $a$ 和 $b$ 这两个串按照字典序进行比较。
+### O(n^2logn) 做法
 
-字典序:从左到右比较,遇到第一个不相同的字符时,字符在字母表中靠前的字典序较小;若某个串已扫描完,而另一个串未扫描完,且前面的字符都相同,则已扫描完的字典序靠前(即设长度较短的为串 $S_1$ ,较长的为 $S_2$ , $S_2$ 前 $|S_1|$ 个字符为 $S_2'$ ,且 $S_2'=S_1$ ,则 $S_1$ 字典序较小);若两串长度相等且未找到不同的字符,则称两串相同)
+我相信这个做法大家还是能自己想到的,用 `string` + `sort` 就可以了。由于比较两个字符串是 $O(n)$ 的,所以排序是 $O(n^2\log n)$ 的。
 
-后缀数组: $sa[i]$ 代表该字符串的 $len$ 个后缀中,从 $sa[i]$ 开始的后缀排在为 $i$ 个。 $sa$ 数组记录的是“排第几的是哪个后缀”。
+### O(nlog^2n) 做法
 
-名次数组: $rank[i]$ 代表从 $i$ 开始的后缀排名为 $rank[i]$ 。 $rank$ 数组记录的是“某个后缀排在第几个”
+这个做法要用到倍增的思想
 
-## 构造
+先对每个长度为 $1$ 的子串(即每个字符)进行排序。
 
-###  $O(n^2 \log 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]$。
 
-这是最朴素的算法。找出所有后缀并用快速排序或者归并排序,同时维护下标。排序时比较 $O(n\log n)$ 次,直接比较长度为 $n$ 的字符串的时间复杂度为 $O(n)$ ,因此时间复杂度为 $O(n^2 \log n)$ 。
+倍增排序示意图:
 
-```cpp
-int rank[123], sa[123];
-
-struct Str {
-  string s;
-  int wei;
-  friend bool operator<(Str a1, Str a2) { return a1.s < a2.s; }
-} k[123];
-
-int main() {
-  string s;
-  cin >> s;
-  int len = s.size() - 1;
-
-  for (int i = 0; i <= len; i++) {
-    k[i].wei = i;
-    for (int j = i; j <= len; j++) k[i].s = k[i].s + s[j];
-  }
-
-  sort(k, k + len + 1);
-  for (int i = 0; i <= len; i++) {
-    rank[k[i].wei] = i;
-    sa[i] = k[i].wei;
-  }
-
-  exit(0);
-}
-```
+[![](./images/sa2.png)][2]
 
-###  $O(n \log n)$ 算法
+如果用 `sort` 进行排序,复杂度就是 $O(n\log^2n)$ 的。
 
-严格来说,接下来的算法将不会排序后缀,而是循环的移动一个字符串。我们可以~ 轻易地~ 得到一个算法来将它变成后缀排序:假设有一个字符串 $S'$ ,那么添加任一小于 $S'$ 中所有字符到 $S'$ 末尾的时间复杂度是可以接受的。通常使用符号 $\$$ 来表示一个小于当前字符集中所有字符的字符。在以上前提下,排序后的循环移动的排列和排序后后缀的排列相同。例如字符串 $dabbb$ :
+??? note "参考代码"
+    ```cpp
+    #include <iostream>
+    #include <cstdio>
+    #include <cstring>
+    #include <algorithm>
+    
+    using namespace std;
+    
+    const int N=1000010;
+    
+    char s[N];
+    int n,w,sa[N],rk[N<<1],oldrk[N<<1];
+    // 为了防止访问 rk[i+w] 导致数组越界,开两倍数组。
+    // 当然也可以在访问前判断是否越界,但直接开两倍数组方便一些。
+    
+    int main()
+    {
+        int i,p;
+    
+        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 (i=1;i<=n;++i) printf("%d ",sa[i]);
+    
+        return 0;
+    }
+    ```
 
-$$
-\begin{array}{lll}
-1. & abbb$d & abbb \\
-4. & b$dabb & b \\
-3. & bb$dab & bb \\
-2. & bbb$da & bbb \\
-0. & dabbb$ & dabbb
-\end{array}
-$$
+### O(nlogn) 做法
 
\9b ä¸ºæ\88\91们è¦\81æ\8e\92åº\8f循ç\8e¯ç§»å\8a¨ï¼\8cæ\88\91们è¦\81è\80\83è\99\91 **循ç\8e¯å­\90串** ã\80\82æ\88\91们å°\86使ç\94¨è®°å\8f· $S[i\dots j]$ è¡¨ç¤º $S$ ç\9a\84ä¸\80个å­\90串ã\80\82å\8d³ä½¿ $i>j$ ä¹\9fä¸\8dä¾\8bå¤\96ï¼\8cå\9b ä¸ºå\9c¨è¿\99ç§\8dæ\83\85å\86µä¸\8bæ\88\91们表示ç\9a\84å­\97符串æ\98¯ $S[i \dots n-1]+S[0\dots j]$ ï¼\8cå\8f¦å¤\96æ\88\91们å°\86æ\8a\8aæ\89\80æ\9c\89ä¸\8bæ \87å\8f\96模 $n$ ï¼\8cä½\86å\9c¨ä¸\8bæ\96\87中为æ\8f\8fè¿°ç®\80便å°\86忽ç\95¥è¯¥æ\93\8dä½\9c
\9c¨å\88\9aå\88\9aç\9a\84 $O(n\log^2n)$ å\81\9aæ³\95中ï¼\8cå\8d\95次æ\8e\92åº\8fæ\98¯ $O(n\log n)$ ç\9a\84ï¼\8cå¦\82æ\9e\9cè\83½ $O(n)$ æ\8e\92åº\8fï¼\8cå°±è\83½ $O(n\log n)$ è®¡ç®\97å\90\8eç¼\80æ\95°ç»\84äº\86
 
\9c¨æ\88\91们å°\86è¦\81讨论ç\9a\84ç®\97æ³\95中ï¼\8cå°\86ä¼\9aæ\9c\89 $\lceil \log n \rceil + 1$ æ¬¡è¿­ä»£ã\80\82å\9c¨ç¬¬ $k(k=0\dots \lceil \log n \rceil)$ æ¬¡è¿­ä»£ä¸­ï¼\8cæ\88\91们å°\86ä¼\9aæ\8e\92åº\8f $n$ ä¸ªé\95¿åº¦ä¸º $2^k$ ç\9a\84循ç\8e¯å­\90串 $s$ ã\80\82å\9c¨ç¬¬ $\lceil \log n \rceil$ æ¬¡è¿­ä»£å\90\8eï¼\8cé\95¿åº¦ä¸º $2^{\lceil \log n \rceil}\geq n$ ç\9a\84å­\90串å°\86被æ\8e\92åº\8fï¼\8cå\9b æ­¤è¿\99ä¸\8eå\90\8cæ\97¶æ\8e\92åº\8f循ç\8e¯ç§»å\8a¨ç­\89ä»·
\89\8dç½®ç\9f¥è¯\86ï¼\9a[计æ\95°æ\8e\92åº\8f](../basic/counting-sort.md)ï¼\8c\9fºæ\95°æ\8e\92åº\8f](../basic/radix-sort.md)
 
-在算法的每次迭代中,令 $p[i]$ 表示第 $i$ 个子串(起点为 $i$ ,长度为 $2^k$ )的子串的下标,在排列 $p[0\dots n-1]$ 的基础之上,我们还会维护数组 $c[0\dots n-1]$ ,其中 $c[i]$ 表示子串隶属的 **等价类** 。因为某些子串可能是相同的,因此算法需要将他们同等处理。为了方便,等价类将被以 $0$ 起的计数标记。另外, $c[i]$ 中的值将被用以保留顺序信息:如果一个子串比另外一个子串的字典序小,它的等价类标记也应该小于另一个子串。等价类的数量将被储存在一个可变的类当中
+由于计算后缀数组的过程中排序的关键字是排名,值域为 $O(n)$,并且是一个双关键字的排序,可以使用基数排序优化至 $O(n)$
 
-现在来看一个例子,例如字符串 $S=aaba$ 。每次迭代时它的循环子串和对应的数组 $p[]$ 和 $c[]$ 如下所示:
+??? note "参考代码"
+    ```cpp
+    #include <iostream>
+    #include <cstdio>
+    #include <cstring>
+    #include <algorithm>
 
-$$
-\begin{array}{cccc}
-0: & (a,~ a,~ b,~ a) & p = (0,~ 1,~ 3,~ 2) & c = (0,~ 0,~ 1,~ 0)\\
-1: & (aa,~ ab,~ ba,~ aa) & p = (0,~ 3,~ 1,~ 2) & c = (0,~ 1,~ 2,~ 0)\\
-2: & (aaba,~ abaa,~ baaa,~ aaab) & p = (3,~ 0,~ 1,~ 2) & c = (1,~ 2,~ 3,~ 0)\\
-\end{array}
-$$
+    using namespace std;
 
-值得注意的是, $p$ 数组的值可以不同。例如,在第一次迭代中,该数组可以是 $p = (3,~ 1,~ 0,~ 2)$ 或者 $p = (3,~ 0,~ 1,~ 2)$ 。同时, $c[]$ 数组的值是固定的,没有其他可能性。
+    const int N=1000010;
 
-现在让我们来关注算法的实现。我们要写一个函数,使得输入字符串 $s​$ 后返回它排序后的循环移动。
+    char s[N];
+    int n,sa[N],rk[N<<1],oldrk[N<<1],id[N],cnt[N];
 
-```cpp
-vector<int> sort_cyclic_shifts(string const& s) {
-  int n = s.size();
-  const int alphabet = 256;
-```
-
-一开始,我们要排序长度为 $1$ 的循环子串,即我们要排序字符串中所有的字符,然后将它们分入等价类,即相同字符分入相同类。这个操作可以使用朴素算法思想,例如 **计数排序** 。对于每个字符,我们计算它在字符串中的出现次数,然后用这个信息形成 $p$ 数组。在这之后我们遍历 $p$ 数组,然后通过比较相邻字符构造 $c$ 数组。
+    int main()
+    {
+        int i,m,p,w;
 
-```cpp
-vector<int> p(n), c(n), cnt(max(alphabet, n), 0);
-for (int i = 0; i < n; i++) cnt[s[i]]++;
-for (int i = 1; i < alphabet; i++) cnt[i] += cnt[i - 1];
-for (int i = 0; i < n; i++) p[--cnt[s[i]]] = i;
-c[p[0]] = 0;
-int classes = 1;
-for (int i = 1; i < n; i++) {
-  if (s[p[i]] != s[p[i - 1]]) classes++;
-  c[p[i]] = classes - 1;
-}
-```
+        scanf("%s",s+1);
+        n=strlen(s+1);
+        m=max(n,300);
+        for (i=1;i<=n;++i) ++cnt[rk[i]=s[i]];
+        for (i=1;i<=m;++i) cnt[i]+=cnt[i-1];
+        for (i=n;i>=1;--i) sa[cnt[rk[i]]--]=i;
 
-接下来我们说说如何进行迭代。假设我们已经完成了前 $k-1$ 步,并且计算了 $p$ 数组和 $c$ 数组。我们接下来想在 $O(n)$ 时间复杂度内计算这两个数组在第 $k$ 步的值。因为我们总共执行这个步骤 $O(\log n)$ 次,所以算法的时间复杂度是 $O(n\log n)$ 。
+        for (w=1;w<n;w<<=1)
+        {
+            memset(cnt,0,sizeof(cnt));
+            for (i=1;i<=n;++i) id[i]=sa[i];
+            for (i=1;i<=n;++i) ++cnt[rk[id[i]+w]];
+            for (i=1;i<=m;++i) cnt[i]+=cnt[i-1];
+            for (i=n;i>=1;--i) sa[cnt[rk[id[i]+w]]--]=id[i];
+            memset(cnt,0,sizeof(cnt));
+            for (i=1;i<=n;++i) id[i]=sa[i];
+            for (i=1;i<=n;++i) ++cnt[rk[id[i]]];
+            for (i=1;i<=m;++i) cnt[i]+=cnt[i-1];
+            for (i=n;i>=1;--i) sa[cnt[rk[id[i]]]--]=id[i];
+            memcpy(oldrk,rk,sizeof(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;
+        }
 
-为了完成这一目标,我们注意到长度为 $2^k$ 的循环子串包括两个长度为 $2^{k-1}$ 的子串,我们可以使用上一步 $c$ 数组的计算结果在 $O(1)$ 的时间复杂度内通过比较得出。因此,对于两个长度为 $2^k$ ,起点分别为 $i$ 和 $j$ 的子串,所需的用来比较信息在二元组 $(c[i],c[i+2^{k-1}])$ 和 $(c[j],c[j+2^{k-1}])$ 中已经全部包含。
+        for (i=1;i<=n;++i) printf("%d ",sa[i]);
 
-$$
-\dots
-\overbrace{
-\underbrace{s_i \dots s_{i+2^{k-1}-1}}_{\text{length} = 2^{k-1},~ \text{class} = c[i]}
-\quad
-\underbrace{s_{i+2^{k-1}} \dots s_{i+2^k-1}}_{\text{length} = 2^{k-1},~ \text{class} = c[i + 2^{k-1}]}
-}^{\text{length} = 2^k}
-\dots
-\overbrace{
-\underbrace{s_j \dots s_{j+2^{k-1}-1}}_{\text{length} = 2^{k-1},~ \text{class} = c[j]}
-\quad
-\underbrace{s_{j+2^{k-1}} \dots s_{j+2^k-1}}_{\text{length} = 2^{k-1},~ \text{class} = c[j + 2^{k-1}]}
-}^{\text{length} = 2^k}
-\dots
-$$
+        return 0;
+    }
+    ```
 
-这就给我们带来了一个十分简单的解法:以 **这些二元组** 为关键字, **排序** 所有长度为 $2^k$ 的子串。这给我们带来了所需要的顺序数组 $p$ 。然而一般的排序算法的时间复杂度为 $O(n\log n)$ ,这样的时间复杂度看起来不是很令人满意,因为构造后缀数组的整体时间复杂度将变成 $O(n \log^2 n)$ 。
+### 一些常数优化
 
-有什么办法可以使得排序更快呢?因为这些元素值不超过 $n$ ,我们可以再次使用计数排序。然而直接这样做并不是最优的,因为时间复杂度后面隐藏着一个常数。为了优化常数,我们要用另外一个小技巧。
+如果你把上面那份代码交到 LOJ 上:
 
-我们这里使用了一个用在 **基数排序** 上的方法:为了排序这些元素,我们先以第二关键字排序,然后以第一关键字排序。另外,我们使用的排序是稳定排序,即不破坏值相同元素之间的相对位置关系。然而第二关键字已经在上一次的迭代中排序好了,因此,为了排序第二关键字,我们只需要从 $p[]$ 中的下标中减去 $2^{k-1}$ 即可。例如,长度为 $2^{k-1}$ 的最小子串在位置 $i$ 开始,那么第二关键字最小的且长度为 $2^k$ 的以 $i-2^{k-1}$ 为起点。
+![](./images/sa3.png)
 
-因此,只有通过做简单的减法,我们才能排序 $p$ 数组里的第二关键字。现在我们要对第一关键字进行一个稳定的排序。同上,这个操作能被计数排序完成
+这是因为,上面那份代码的常数的确很大
 
-现在就只剩计算等价类数组 $c[]$ 了,但是和之前一样,这个操作可以通过简单地迭代排序后的 $p$ 数组并比较相邻元素。
+#### 第二关键字无需计数排序
 
-以下是剩下的代码。我们使用临时数组 $pn[]$ 和 $cn[]$ 来储存第二关键字的排序和新的等价类下标。
+实际上,像这样就可以了:
 
 ```cpp
-vector<int> pn(n), cn(n);
-for (int h = 0; (1 << h) < n; ++h) {
-  for (int i = 0; i < n; i++) {
-    pn[i] = p[i] - (1 << h);
-    if (pn[i] < 0) pn[i] += n;
-  }
-  fill(cnt.begin(), cnt.begin() + classes, 0);
-  for (int i = 0; i < n; i++) cnt[c[pn[i]]]++;
-  for (int i = 1; i < classes; i++) cnt[i] += cnt[i - 1];
-  for (int i = n - 1; i >= 0; i--) p[--cnt[c[pn[i]]]] = pn[i];
-  cn[p[0]] = 0;
-  classes = 1;
-  for (int i = 1; i < n; i++) {
-    pair<int, int> cur = {c[p[i]], c[(p[i] + (1 << h)) % n]};
-    pair<int, int> prev = {c[p[i - 1]], c[(p[i - 1] + (1 << h)) % n]};
-    if (cur != prev) ++classes;
-    cn[p[i]] = classes - 1;
-  }
-  c.swap(cn);
-}
-return p;
-}
+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;
 ```
 
-算法整体时间复杂度为 $O(n\log n)$ ,空间复杂度为 $O(n)$ 。如果考虑字符集大小 $k$ 的影响,整体时间复杂度为 $O((n+k)\log n)$ ,空间复杂度为 $O(n+k)$ 
+意会一下,先把 $s[i+w..i+2w-1]$ 为空串(即第二关键字为无穷小)的位置放前面,再把剩下的按排好的顺序放进去
 
-为了简单,我们用了整个 ASCII 表范围作为了字母表,如果我们知道字符集,例如小写字母,那么这个做法能够得到优化。然而,这个优化并不大,因为字符集的复杂度影响仅仅只是 $O(\log k)$ 。
+#### 优化计数排序的值域
 
-另外一点要注意的是,这个算法只排序循环的移动。在文章的一开头已经说过了,我们可以通过在后缀末尾加一个小于字符串中任意字符的字符来生成已排序的后缀顺序,同时这样的排序通过循环的移动形成了字符串,例如排序 $s+\$$ 的循环移动。这显然会给出 $s$ 的后缀数组,但是前面会多一个等于 $|S|$ 的值
+每次对 $rk$ 进行去重之后,我们都计算了一个 $p$,这个 $p$ 即是 $rk$ 的值域,将值域改成它即可
 
-```cpp
-vector<int> suffix_array_construction(string s) {
-  s += "$";
-  vector<int> sorted_shifts = sort_cyclic_shifts(s);
-  sorted_shifts.erase(sorted_shifts.begin());
-  return sorted_shifts;
-}
-```
+#### 将 rk[id[i]] 存下来,减少不连续内存访问
 
-### 模板
+这个优化在数据范围较大时效果非常明显。
 
-```cpp
-#include <bits/stdc++.h>
-using namespace std;
-
-int n;
-int sa[150], x[150], c[150], y[150];
-char a[150];
-
-inline void SA() {
-  int m = 128;
-  for (int i = 0; i <= m; i++) c[i] = 0;
-  for (int i = 1; i <= n; i++) c[x[i]]++;
-  for (int i = 1; i <= m; i++) c[i] += c[i - 1];
-  for (int i = n; i; i--) sa[c[x[i]]--] = i;
-
-  for (int k = 1, p; k <= n; k <<= 1) {
-    p = 0;
-    for (int i = n; i > n - k; i--) y[++p] = i;
-    for (int i = 1; i <= n; i++)
-      if (sa[i] > k) y[++p] = sa[i] - k;
-
-    for (int i = 0; i <= m; i++) c[i] = 0;
-    for (int i = 1; i <= n; i++) c[x[i]]++;
-    for (int i = 1; i <= m; i++) c[i] += c[i - 1];
-    for (int i = n; i; i--) sa[c[x[y[i]]]--] = y[i];
-
-    p = y[sa[1]] = 1;
-    for (int i = 2, a, b; i <= n; i++) {
-      a = sa[i] + k > n ? -1 : x[sa[i] + k];
-      b = sa[i - 1] + k > n ? -1 : x[sa[i - 1] + k];
-      y[sa[i]] = (x[sa[i]] == x[sa[i - 1]]) && (a == b) ? p : ++p;
+#### 用函数 cmp 来计算是否重复
+
+同样是减少不连续内存访问,在数据范围较大时效果比较明显。
+
+把 `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 <cstdio>
+    #include <cstring>
+    #include <algorithm>
+
+    using namespace std;
+
+    const int N=1000010;
+
+    char s[N];
+    int n,sa[N],rk[N],oldrk[N<<1],id[N],px[N],cnt[N];
+    //px[i] = rk[id[i]](用于排序的数组所以叫 px)
+
+    bool cmp(int x,int y,int w) {
+        return oldrk[x]==oldrk[y]&&oldrk[x+w]==oldrk[y+w];
     }
-    swap(x, y);
-    m = p;
-  }
-}
 
-int main() {
-  scanf("%s", a + 1);
+    int main()
+    {
+        int i,m=300,p,w;
+
+        scanf("%s",s+1);
+        n=strlen(s+1);
+        for (i=1;i<=n;++i) ++cnt[rk[i]=s[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) // m=p 就是优化计数排序值域
+        {
+            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;
+            memset(cnt,0,sizeof(cnt));
+            for (i=1;i<=n;++i) ++cnt[px[i]=rk[id[i]]];
+            for (i=1;i<=m;++i) cnt[i]+=cnt[i-1];
+            for (i=n;i>=1;--i) sa[cnt[px[i]]--]=id[i];
+            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;
+        }
+
+        for (i=1;i<=n;++i) printf("%d ",sa[i]);
+
+        return 0;
+    }
+    ```
 
-  n = strlen(a + 1);
-  for (int i = 1; i <= n; i++) x[i] = a[i];
-  SA();
+### O(n) 做法
 
-  for (int i = 1; i <= n; i++) printf("%d", sa[i]);
-  exit(0);
-}
-```
+在一般的题目中,常数较小的倍增求后缀数组是完全够用的,求后缀数组以外的部分也经常有 $O(n\log n)$ 的复杂度,倍增求解后缀数组不会成为瓶颈。
+
+但如果遇到特殊题目、时限较紧的题目,或者是你想追求更短的用时,就需要学习 $O(n)$ 求后缀数组的方法。
+
+#### SA-IS
 
-代码里 $x[i]$ 就是 $rank[i]$ 
+可以参考 [诱导排序与 SA-IS 算法](https://riteme.site/blog/2016-6-19/sais.html)
 
- $y[i]$ :假设 $y[i]=a\ ,\  y[i+1]=b$ 那么在原串中从 $a+2^k$ 开始的 $2^k$ 个字符组成的子串 **小于等于** 从 $b+2^k$ 开始的 $2^k$ 个字符组成的子串。
+#### DC3
 
-最好理解这个代码时,每一步都结合着基数排序来考虑
+可以参考 [[2009]后缀数组——处理字符串的有力工具 by.罗穗骞][2]
 
-## 应用
+## å\90\8eç¼\80æ\95°ç»\84ç\9a\84åº\94ç\94¨
 
 ### 寻找最小的循环移动位置
 
-在不往字符串末尾加字符时,上面的算法已经排序了所有的循环移动,因此 $p[0]$ 就是循环移动的最小位置。
+将字符串 $S$ 复制一份变成 $SS$ 就转化成了后缀排序问题。
+
+例题:[「JSOI2007」字符加密](https://www.luogu.org/problem/P4051)。
 
 ### 在字符串中找子串
 
-任务是在线地在主串 $T$ 中寻找模式串 $S$ 。在线的意思是,我们已经预先知道知道主串 $T$ ,但是当且仅当询问时才知道模式串 $S$ 。我们可以为字符串 $T$ 在 $O(|T|\log |T|)$ 时间复杂度内构造它的后缀数组。现在我们可以开始查找子串 $S$ 了。子串 $S$ 的出现必须是 $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)。
+
+题意:给你一个字符串,每次从首或尾取一个字符组成字符串,问所有能够组成的字符串中字典序最小的一个。
+
+??? note "题解"
+    暴力做法就是每次最坏 $O(n)$ 地判断当前应该取首还是尾(即比较取首得到的字符串与取尾得到的反串的大小),只需优化这一判断过程即可。
+
+    由于需要在原串后缀与反串后缀构成的集合内比较大小,可以将反串拼接在原串后,并在中间加上一个没出现过的字符(如 `#`,代码中可以直接使用空字符),求后缀数组,即可 $O(1)$ 完成这一判断。
+
+??? note "参考代码"
+    ```cpp
+    #include <iostream>
+    #include <cctype>
+    #include <cstdio>
+    #include <cstring>
+    
+    using namespace std;
+    
+    const int N=1000010;
+    
+    char s[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;
+            for (i=1;i<=n;++i) if (sa[i]>w) id[++p]=sa[i]-w;
+            memset(cnt,0,sizeof(cnt));
+            for (i=1;i<=n;++i) ++cnt[px[i]=rk[id[i]]];
+            for (i=1;i<=m;++i) cnt[i]+=cnt[i-1];
+            for (i=n;i>=1;--i) sa[cnt[px[i]]--]=id[i];
+            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;
+    }
+    ```
 
-### 比较一个字符串的两个子串的大小关系
+## height 数组
 
-这题中,我们想在 $O(1)$ 时间内比较 $S$ 的两个等长子串的大小关系。
+### LCP(最长公共前缀)
 
-我们先花费 $O(|S|\log |S|)$ 的时间复杂度构造 $S$ 的后缀数组并储存所有等价类数组 $c[]$ 的中间值
+两个字符串 $S$ 和 $T$ 的 LCP 就是最大的 $x$ ($x\le \min(|S|, |T|)$) 使得 $S_i=T_i\ (\forall\ 1\le i\le x)$ 
 
-通过这个信息,我们就可以在 $O(1)$ 时间复杂度内通过比较两个字符串的等价类比较任意两个长度为 $2$ 的正整数幂次方的子串了。现在,我们想要拓展这个算法,使得其可用于任意长度
+下文中以 $lcp(i,j)$ 表示后缀 $i$ 和后缀 $j$ 的最长公共前缀(的长度)
 
-假设有两个长度为 $l$ ,起点下标为 $i$ 和 $j$ 的子串。我们找到子串中长度最大的子段,即使得 $2^k\leq l$ 且 $k$ 最大。然后,比较两个子串就可以等价为比较两个重叠的,长度为 $2^k$ 的子段:一开始两个子段起点是 $i$ 和 $j$ ,如果这两个子段相同,就去比较两个结束点为 $i+l-1$ 和 $j+l-1$ 的子段。
+### height 数组的定义
 
-$$
-\dots
-\overbrace{\underbrace{s_i \dots s_{i+l-2^k} \dots s_{i+2^k-1}}_{2^k} \dots s_{i+l-1}}^{\text{first}}
-\dots
-\overbrace{\underbrace{s_j \dots s_{j+l-2^k} \dots s_{j+2^k-1}}_{2^k} \dots s_{j+l-1}}^{\text{second}}
-\dots
-$$
+$height[i]=lcp(sa[i],sa[i-1])$,即第 $i$ 名的后缀与它前一名的后缀的最长公共前缀。
 
-$$
-\dots
-\overbrace{s_i \dots \underbrace{s_{i+l-2^k} \dots s_{i+2^k-1} \dots s_{i+l-1}}_{2^k}}^{\text{first}}
-\dots
-\overbrace{s_j \dots \underbrace{s_{j+l-2^k} \dots s_{j+2^k-1} \dots s_{j+l-1}}_{2^k}}^{\text{second}}
-\dots
-$$
+$height[1]$ 可以视作 $0$。
 
-以下为比较的实现。请注意,这个代码假设你已经算好了 $k$ , $k$ 可以在 $\lfloor \log l \rfloor$ 的时间复杂度中算出,但是更有效的算法是预处理每个 $l$ 的 $k$ 值,你可以通过 ST 表算法算出所有的 $k$ 值,该算法与其思路相似。
+### O(n) 求 height 数组需要的一个引理
 
-```cpp
-int compare(int i, int j, int l, int k) {
-  pair<int, int> a = {c[k][i], c[k][(i + l - (1 << k)) % n]};
-  pair<int, int> b = {c[k][j], c[k][(j + l - (1 << k)) % n]};
-  return a == b ? 0 : a < b ? -1 : 1;
-}
-```
+$height[rk[i]]\ge height[rk[i-1]]-1$
 
-### 消耗额外空间复杂度的两子串最长公共前缀求解
+证明:
 
¯¹äº\8eä¸\80个ç»\99å®\9aç\9a\84å­\97符串 $S$ ï¼\8cæ\88\91们å\85\88è¦\81计ç®\97å®\83ç\9a\84ä»»æ\84\8f两个å\90\8eç¼\80ç\9a\84æ\9c\80é\95¿å\85¬å\85±å\89\8dç¼\80 **LCP** ï¼\8cå\81\87设è¿\99两个å\90\8eç¼\80ç\9a\84èµ·ç\82¹æ\98¯ $i$ å\92\8c $j$ 
½\93 $height[rk[i-1]]\le1$ æ\97¶ï¼\8cä¸\8aå¼\8fæ\98¾ç\84¶æ\88\90ç«\8bï¼\88å\8f³è¾¹å°\8fäº\8eç­\89äº\8e $0$ ï¼\89
 
-此处的代码将额外花费 $O(|S|\log |S|)$ 的空间。下一节,我们将讨论另外一个完全不一样的做法,该做法将只花费线性的内存。
+当 $height[rk[i-1]]>1$ 时:
 
-我们先花费 $O(|S|\log |S|)$ 的时间复杂度构造 $S$ 的后缀数组并储存所有等价类数组 $c[]$ 的中间值
+设后缀 $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$ 和 $j$ 的后缀。我们可以在 $O(1)$ 时间复杂度内比较两个长度为 $2$ 的正整数幂次方的字符串。做法是,我们按 $2$ 的从高至低次幂比较字符串,如果两个这样一个长度的字符串相同,我们在答案上加上这个长度,然后到相等部分右端继续检查,即 $i$ 和 $j$ 加上当前的长度。具体实现如下,注意,其中 `log_n` 表示 $\lfloor log_2 n\rfloor$ 。
+简单来说:
+
+$i-1$:$aAD$
+
+$i$:$AD$
+
+$sa[rk[i-1]-1]$:$aAB$
+
+$sa[rk[i-1]-1]+1$:$AB$
+
+$sa[rk[i]-1]$:$A[B/C]$
+
+$lcp(i,sa[rk[i]-1])$:$AX$($X$ 可能为空)
+
+### O(n) 求 height 数组的代码实现
+
+利用上面这个引理暴力求即可:
 
 ```cpp
-int lcp(int i, int j) {
-  int ans = 0;
-  for (int k = log_n; k >= 0; k--) {
-    if (c[k][i] == c[k][j]) {
-      ans += 1 << k;
-      i += 1 << k;
-      j += 1 << k;
-    }
-  }
-  return ans;
+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)$。
 
-我们的目标和前一节一样,即我们要计算一个给定字符串 $S$ 的两个后缀的最长公共前缀 LCP。
+## height 数组的应用
 
-与前一节不一样的是,我们在这里只消耗 $O(|S|)$ 的时间复杂度,处理结果将会是一个数组,该数组中储存了字符串的重要信息,因此也会被用于解决其他问题。LCP 的询问可以通过在数组中执行 RMQ(区间最小值询问)的询问完成,所以不同的实现可以达到 $\log$ 级别甚至常数级别的时间复杂度。(译者注:此处的 LCP 数组表示通常意义上的 height 数组)
+### 两子串最长公共前缀
 
-这个算法的基础是,我们要计算相邻的排序后的后缀的 LCP。换言之,我们将会构造一个数组 $\text{LCP}[0\dots n-2]​$ ,其中 $\text{LCP}[0\dots n-2]​$ 等于 $p[i]​$ 和 $p[i+1]​$ 的 LCP 长度。这个数组将使得我们能够计算出字符串中任意两个相邻后缀的 LCP。拓展到任意两个后缀,可以从这个数组中得到。其实,查询后缀 $p[i]​$ 和 $p[j]​$ 的 LCP 答案就是 $min\{ lcp[i],~lcp[i+1],~\dots,~lcp[j-1]\}​$ 。
+$lcp(sa[i],sa[j])=\min\{height[i+1..j]\}$
 
-因此当我们算出 $\text{LCP}$ 后问题就简化成了计算 $\text{RMQ}$ ,不同的算法有不同的时间复杂度
+感性理解:如果 $height$ 一直大于某个数,前这么多位就一直没变过;反之,由于后缀已经排好序了,不可能变了之后变回来
 
-所以我们的主要任务就是构造 $\text{LCP}$ 数组。我们将使用 Kasai 算法,该算法能在 $O(|S|)$ 时间复杂度内构造这个数组
+严格证明可以参考[[2004]后缀数组 by.徐智磊][1]
 
-现在来观察两个排序后的相邻后缀,假设他们的起点位置是 $i​$ 和 $j​$ 且他们的 LCP 等于 $k​$ ,其中 $k>0​$ 。如果我们同时移除这两个后缀的第一个字符,即起点变成 $i+1​$ 和 $j+1​$ ,此时这两个后缀的 LCP 是 $k-1​$ 。然而这个值不能直接拿来用然后写进 $\text{LCP}​$ 数组,因为这两个后缀在排序后不一定相邻。后缀 $i+1$ 显然小于后缀 $j+1$ ,但是他们之中可能有些后缀。然而,因为我们知道了两个后缀间的 LCP 是这两个下标间的最小值,同时,任意两对间的距离至少要是 $k-1$ ,特别是 $i+1$ 和下一个后缀。同时这个值可能会更大
+有了这个定理,求两子串最长公共前缀就转化为了 [RMQ 问题](../topic/rmq.md)
 
-现在我们可以开始实现这个算法了。我们将按照后缀长度进行循环,通过这种方法,我们可以重用最后一次的 $k$ 值,因为从后缀 $i$ 到后缀 $i+1$ 就是去掉第一个字符。我们要开一个 $\text{rank}​$ 数组来记录一个后缀在排序后的数组中的位置。
+### 比较一个字符串的两个子串的大小关系
 
-代码如下,我们最多减去 $n$ 次 $k$ (每次循环最多一次,除了 $\text{rank}[i]==n-1$ ,在这个情况下 $k$ 将直接被置零),并且两个子串的 LCP 最多是 $n-1$ 。我们增加 $k$ 的操作也是最多 $n$ 次,因此时间复杂度为 $O(n)$ 
+假设需要比较的是 $A=S[a..b]$ 和 $B=S[c..d]$ 的大小关系
 
-```cpp
-vector<int> lcp_construction(string const& s, vector<int> const& p) {
-  int n = s.size();
-  vector<int> rank(n, 0);
-  for (int i = 0; i < n; i++) rank[p[i]] = i;
-
-  int k = 0;
-  vector<int> lcp(n - 1, 0);
-  for (int i = 0; i < n; i++) {
-    if (rank[i] == n - 1) {
-      k = 0;
-      continue;
-    }
-    int j = p[rank[i] + 1];
-    while (i + k < n && j + k < n && s[i + k] == s[j + k]) k++;
-    lcp[rank[i]] = k;
-    if (k) k--;
-  }
-  return lcp;
-}
-```
+若 $lcp(a, c)\ge\min(|A|, |B|)$,$A<B\iff |A|<|B|$。
+
+否则,$A<B\iff rk[a]< rk[b]$。
 
 ### 不同子串的数目
 
-我们先预处理字符串 $S$ ,即计算它的后缀数组和 LCA 数组。使用这些信息,我们就可以算出字符串中有多少不同子串了。
+子串就是后缀的前缀,所以可以枚举每个后缀,计算前缀总数,再减掉重复。
+
+“前缀总数”其实就是子串个数,为 $n(n+1)/2$。
+
+如果按后缀排序的顺序枚举后缀,每次新增的子串就是除了与上一个后缀的 LCP 剩下的前缀。这些前缀一定是新增的,否则会破坏 $lcp(sa[i],sa[j])=\min\{height[i+1..j]\}$ 的性质。只有这些前缀是新增的,因为 LCP 部分在枚举上一个前缀时计算过了。
+
+所以答案为:
 
-为了达到目的,我们要想一下哪个新的子串在 $p[0]$ 位置开始,然后想想哪个在 $p[1]$ 开始。其实,我们使用排序后的后缀,然后看看给新的子串什么前缀,因此我们将不会错过任何一个。
+$$\frac{n(n+1)}{2}-\sum\limits_{i=2}^nheight[i]$$
 
-因为后缀已经排序了,明显当前后缀 $p[i]​$ 将为他的所有前缀贡献一个新的子串,除了由 $p[i-1]​$ 贡献的子串。即它除了前 $\text{LCP}[i-1]​$ 个前缀的所有前缀。因为当前后缀的长度是 $n-p[i]​$ ,因此 $n-p[i]-\text{LCP}[i-1]​$ 个新后缀将在 $p[i]​$ 开始。对所有后缀求和即得答案,即
+### 出现至少 k 次的子串的最大长度
 
-$$
-\sum_{i=0}^{n-1} (n - p[i]) - \sum_{i=0}^{n-2} \text{lcp}[i] = \frac{n^2 + n}{2} - \sum_{i=0}^{n-2} \text{lcp}[i]​
-$$
+例题:[「USACO06DEC」Milk Patterns](https://www.luogu.org/problemnew/show/P2852)。
 
-## 实用技巧及数据结构
+??? note "题解"
+    出现至少 $k$ 次意味着后缀排序后有至少连续 $k$ 个后缀的 LCP 是这个子串。
 
-这一部分的 $height$ 数组(或简写为 $h$ 数组),即为上述的 $LCP$ 数组,其定义为 $h_i$ 为后缀 $p_i$ 和 $p_{i-1}$ 的 LCP 长度。特别地,我们定义 $h_1=0$ 。两个数组的转换关系为 $h_i=LCP_{i-2}$ 
+    所以,求出每相邻 $k-1$ 个 $height$ 的最小值,再求这些最小值的最大值就是答案
 
-### RMQ
+    可以使用单调队列 $O(n)$ 解决,但使用其它方式也足以 AC。
 
-利用 $LCP_{i,j}=\min\{h_{i+1},h_{i+2},...,h_j\}$ 的性质,维护 $h$ 数组的区间最小值,用来求任意两起始点的 LCP 值。若要求一个出现次数至少为 $k$ 的字符串的长度,则每次取连续 $k-1$ 个 $h$ 值的 $\min$ ,这些 $\min$ 值中的最大值即为所求答案。这样就把这个问题转化为了一个经典的 RMQ 问题。
+??? note "参考代码"
+    ```cpp
+    #include <iostream>
+    #include <cstdio>
+    #include <set>
+    #include <cstring>
 
-如果要求是否有某字符串在文本串中至少不重叠地出现了两次,可以二分目标串的长度 $|s|$ ,将 $h$ 数组划分成若干个连续 LCP 大于等于 $|s|$ 的段,利用 RMQ 对每个段求其中出现的数中最大和最小的下标,若这两个下标的距离满足条件,则一定有长度为 $|s|$ 的字符串不重叠地出现了两次。
+    using namespace std;
 
-### 本质不同的子串
+    const int N=40010;
 
-本质不同的子串即为结束位置在当前后缀与后缀排序中前一个后缀的 LCP 长度(即 $h$ 数组的值)之后的子串。
+    int n,k,a[N],sa[N],rk[N],oldrk[N],id[N],px[N],cnt[1000010],ht[N],ans;
+    multiset<int> t; // multiset 是最好写的实现方式
 
-### 反串
+    bool cmp(int x,int y,int w){ return oldrk[x]==oldrk[y]&&oldrk[x+w]==oldrk[y+w]; }
 
-需要处理满足某种条件的最长回文串时,我们可以将该串翻转后接到原串的后面,中间用一个特殊字符空开,再对新串进行后缀排序。
+    int main()
+    {
+        int i,j,w,p,m=1000000;
 
-利用这个技巧,还可以做从两段取点使字典序最小的决策。
+        scanf("%d%d",&n,&k);
+        --k;
+
+        for (i=1;i<=n;++i) scanf("%d",a+i);
+        for (i=1;i<=n;++i) ++cnt[rk[i]=a[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;
+            for (i=1;i<=n;++i) if (sa[i]>w) id[++p]=sa[i]-w;
+            memset(cnt,0,sizeof(cnt));
+            for (i=1;i<=n;++i) ++cnt[px[i]=rk[id[i]]];
+            for (i=1;i<=m;++i) cnt[i]+=cnt[i-1];
+            for (i=n;i>=1;--i) sa[cnt[px[i]]--]=id[i];
+            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;
+        }
+
+        for (i=1,j=0;i<=n;++i)
+        {
+            if (j) --j;
+            while (a[i+j]==a[sa[rk[i]-1]+j]) ++j;
+            ht[rk[i]]=j;
+        }
+
+        for (i=1;i<=n;++i)
+        {
+            t.insert(ht[i]);
+            if (i>k) t.erase(t.find(ht[i-k]));
+            ans=max(ans,*t.begin());
+        }
+
+        cout<<ans;
+
+        return 0;
+    }
+    ```
+
+### 是否有某字符串在文本串中至少不重叠地出现了两次
+
+可以二分目标串的长度 $|s|$ ,将 $h$ 数组划分成若干个连续 LCP 大于等于 $|s|$ 的段,利用 RMQ 对每个段求其中出现的数中最大和最小的下标,若这两个下标的距离满足条件,则一定有长度为 $|s|$ 的字符串不重叠地出现了两次。
 
 ### 连续的若干个相同子串
 
-我们可以枚举连续串的长度 $|s|$ ,按照 $|s|$ 对整个串进行分块,对相邻两块的块首进行 LCP 与 LCS 查询,具体可见 [ **这篇论文** ](https://wenku.baidu.com/view/5b886b1ea76e58fafab00374.html) 
+我们可以枚举连续串的长度 $|s|$ ,按照 $|s|$ 对整个串进行分块,对相邻两块的块首进行 LCP 与 LCS 查询,具体可见 [[2009]后缀数组——处理字符串的有力工具][2]。
 
-### 并查集
+### 结合并查集
 
 某些题目求解时要求你将后缀数组划分成若干个连续 LCP 长度大于等于某一值的段,亦即将 $h$ 数组划分成若干个连续最小值大于等于某一值的段并统计每一段的答案。如果有多次询问,我们可以将询问离线。观察到当给定值单调递减的时候,满足条件的区间个数总是越来越少,而新区间都是两个或多个原区间相连所得,且新区间中不包含在原区间内的部分的 $h$ 值都为减少到的这个值。我们只需要维护一个并查集,每次合并相邻的两个区间,并维护统计信息即可。
 
 经典题目: [「NOI2015」品酒大会](http://uoj.ac/problem/131) 
 
-### 线段树
+### ç»\93å\90\88线段æ \91
 
 某些题目让你求满足条件的前若干个数,而这些数又在后缀排序中的一个区间内。这时我们可以用归并排序的性质来合并两个结点的信息,利用线段树维护和查询区间答案。
 
-### 单调栈
-
-有些题目让我们求关于一个位置与之前所有位置的 LCP 的长度情况。利用 $LCP_{i,j}=\min\{h_{i+1},h_{i+2},...,h_j\}$ 的性质,我们发现这个 $LCP$ 的值是单调递减的。那么我们可以用一个单调栈来维护这些 LCP 值:在新加入元素 $h_{i}$ 时,我们先把所有大于等于 $h_i$ 的值弹出,统计其个数,并将 $h_{i}$ 压入单调栈,个数为弹出的值的总个数加 $1$ 。
+### 结合单调栈
+
+例题:[「AHOI2013」差异](https://loj.ac/problem/2377)
+
+??? note "题解"
+    被加数的前两项很好处理,为 $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)\}$ 对答案的贡献。
+
+    考虑每个位置对答案的贡献是哪些后缀的 LCP,其实就是从它开始向左若干个连续的 $height$ 大于它的后缀中选一个,再从向右若干个连续的 $height$ 不小于它的后缀中选一个。这个东西可以用 [单调栈](../ds/monotonous-stack.md) 计算。
+
+    单调栈部分类似于 [Luogu P2659 美丽的序列](https://www.luogu.org/problem/P2659) 以及 [悬线法](../misc/largest-matrix.md)。
+
+??? note "参考代码"
+    ```cpp
+    #include <iostream>
+    #include <cstdio>
+    #include <cstring>
+
+    using namespace std;
+
+    const int N=500010;
+
+    char s[N];
+    int n,sa[N],rk[N<<1],oldrk[N<<1],id[N],px[N],cnt[N],ht[N],sta[N],top,l[N];
+    long long ans;
+
+    bool cmp(int x,int y,int w){ return oldrk[x]==oldrk[y]&&oldrk[x+w]==oldrk[y+w]; }
+
+    int main()
+    {
+      int i,k,w,p,m=300;
+      
+      scanf("%s",s+1);
+      n=strlen(s+1);
+      ans=1ll*n*(n-1)*(n+1)/2;
+      for (i=1;i<=n;++i) ++cnt[rk[i]=s[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;
+          for (i=1;i<=n;++i) if (sa[i]>w) id[++p]=sa[i]-w;
+          memset(cnt,0,sizeof(cnt));
+          for (i=1;i<=n;++i) ++cnt[px[i]=rk[id[i]]];
+          for (i=1;i<=m;++i) cnt[i]+=cnt[i-1];
+          for (i=n;i>=1;--i) sa[cnt[px[i]]--]=id[i];
+          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;
+      }
+      
+      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;
+      }
+      
+      for (i=1;i<=n;++i)
+      {
+        while (ht[sta[top]]>ht[i]) --top;
+        l[i]=i-sta[top];
+        sta[++top]=i;
+      }
+      
+      sta[++top]=n+1;
+      ht[n+1]=-1;
+      for (i=n;i>=1;--i)
+      {
+        while (ht[sta[top]]>=ht[i]) --top;
+        ans-=2ll*ht[i]*l[i]*(sta[top]-i);
+        sta[++top]=i;
+      }
+      
+      cout<<ans;
+      
+      return 0;
+    }
+    ```
 
»\8få\85¸é¢\98ç\9b®æ\9c\89 [ã\80\8cAHOI2013ã\80\8då·®å¼\82](https://loj.ac/problem/2377) å\92\8c [「HAOI2016」找相同字符](https://loj.ac/problem/2064) 。
±»ä¼¼ç\9a\84é¢\98ç\9b®ï¼\9a[「HAOI2016」找相同字符](https://loj.ac/problem/2064) 。
 
 ## 习题
 
@@ -462,4 +605,16 @@ $$
 -    [Codeforces - Tricky and Clever Password](http://codeforces.com/contest/30/problem/E) 
 -    [LA 6856 - Circle of digits](https://icpcarchive.ecs.baylor.edu/index.php?option=onlinejudge&page=show_problem&problem=4868) 
 
+## 参考资料
+
 本页面中( [4070a9b](https://github.com/24OI/OI-wiki/pull/950/commits/4070a9b3db8576db16c74d3ec33806ad10476eef) 引入的部分)主要译自博文 [Суффиксный массив](http://e-maxx.ru/algo/suffix_array) 与其英文翻译版 [Suffix Array](https://cp-algorithms.com/string/suffix-array.html) 。其中俄文版版权协议为 Public Domain + Leave a Link;英文版版权协议为 CC-BY-SA 4.0。
+
+论文:
+
+1. [[2004]后缀数组 by.徐智磊][1]
+
+2. [[2009]后缀数组——处理字符串的有力工具 by.罗穗骞][2]
+
+[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