SUS Array

SA

SA,后缀数组,对所有后缀进行排序的数组。
数组不止一个。\(sa_i\) 表示字典序第 \(i\) 小的后缀的编号,\(rk_i\) 代表后缀 \(i\) 的排名。
可以做到 \(O(n)\),但不是特别实用。有小常数 \(O(n\log n)\) 倍增做法,是好的。
有便于理解的辅助数组 \(RK_{i,j}\) 表示所有长度为 \(2^i\) 的子串中,\([j,j+2^i-1]\) 的排名。为了避免特判数组可以开两倍长度,如果 \([j,j+2^i-1]\) 为空,\(RK_{i,j}\)\(-\infty\)
发现求出 \(RK_i\) 的所有值后,想要求出 \(RK_{i+1}\) 只需要做一遍双关键字排序。因为值域小所以可以做基数排序。

下面对 OI-wiki 的代码做一下注释。

for (int i = 1; i <= n; i++) cnt[rk[i] = s[i]]++;
for (int i = 1; i <= m; i++) cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; i--) sa[cnt[rk[i]]--] = i;

这是第一轮排序,只用按单字符排,是一个普通的计数排序。稍加思考,发现 \(sa\) 的下标是小于等于某个后缀 \(rk\) 的后缀个数。因为从大到小遍历,所以保证了稳定性。

int cur = 0;
for (int i = n - w + 1; i <= n; i++) id[++cur] = i;
for (int i = 1; i <= n; i++)
  if (sa[i] > w) id[++cur] = sa[i] - w;

这是当前忽悠化。这里优先按照了第二个关键字排序,因为在这里,从后往前的基数排序要好一些。第一个 for 循环的意思是,对于 \([n-w+1,n]\) 这些后缀(\(w\) 是上一轮排序的长度),它们是没有第二个关键字的,所以它们的 \(rk\)\(-\infty\),应该放在前面。
第二个 for 循环就是在正常根据前面的结果放入了:\(rk_i\) 其实就是 \([i-w,i+w-1]\) 第二个关键字的 \(rk\)
\(id\) 是按第二个关键字排序后的值。太显然了。

for (int i = 1; i <= n; i++) cnt[rk[i]]++;
for (int i = 1; i <= m; i++) cnt[i] += cnt[i - 1];
for (int i = n; i >= 1; i--) sa[cnt[rk[id[i]]]--] = id[i];

这是在保证稳定性的情况下按照第一个关键字排序。事实上,这个 \(id\) 就是为了保证稳定性而存在的。

for (int i = 1; i <= n; i++) {
if (oldrk[sa[i]] == oldrk[sa[i - 1]] &&
    oldrk[sa[i] + w] == oldrk[sa[i - 1] + w])
    rk[sa[i]] = p;
else
    rk[sa[i]] = ++p;
}

因为要保证相同串的排名一样,需要在这里单独处理排名。(如果相同串的排名不一样,就无法按照第二个关键字排序,从而会导致排序错误。)

posted @ 2025-03-04 20:18  Just_int_mian  阅读(44)  评论(2)    收藏  举报