SA
引入及定义
令字符串为
后缀数组(Suffix Array, SA)是解决一类与字符串后缀有关问题的算法。
两个基本的数组为
不难发现
这个数组求解后有什么意义呢?注意到后缀
下面介绍后缀数组的求解方法,以及相关应用。
求解
求解后缀数组的算法中,最常见的是倍增+基数排序的
首先每个后缀的长度都是不同的,最小是
当然实现上并不需要体现这一点,因为数组默认是
考虑进行一个增的倍。从小到大枚举
而在某一轮中排序也很简单。我们知道
怎样分别比较前半段和后半段?它们的长度都是
这里很重要的一点是,如果两个不同位置的字串完全相同,那么它们的排名应该相同。
于是可以写出这样的代码:
for (int k = 1; k <= n; k <<= 1) {
iota(sa + 1, sa + n + 1, 1);
sort(sa + 1, sa + n + 1,
[&](int x, int y) {
return rk[x] == rk[y] ? rk[x + k] < rk[y + k] : rk[x] < rk[y];
});
memcpy(b, rk, sizeof b);
int m = 0;
for (int i = 1; i <= n; ++ i )
if (i != 1 && b[sa[i]] == b[sa[i] - 1] && b[sa[i] + k] == b[sa[i - 1] + k]) {
rk[sa[i]] = m;
} else {
rk[sa[i]] = ++ m;
}
}
由于内部的排序以及外部的倍增,总复杂度是
注意到排序是双关键字排序,且每个关键字的值域都不超过
在基数排序中,首先将数据按第二关键字排序,然后按第一关键字做稳定排序。这里稳定是指不改变第一关键字相同的元素排序前后的相对位置关系。这很类似二维数点中将数据离线后按某一维排序,将这一维转化为时间维进行下一步对另一维的操作。
因为值域不超过
我们将所有数据按照某一关键字拍到桶(
那么所有等于
此时再遍历一遍原数组。对于某个数
因为要做稳定排序,但是上面遇到相同数时,越靠前的排名越大,所以最后赋排名时倒过来枚举。
即:
// 按 a[i] 排序,p[i] 表示排名为 i 的是第几个
memset(cnt, 0, sizeof cnt);
for (int i = 1; i <= n; ++ i ) cnt[a[i]] ++ ;
for (int i = 1; i <= m; ++ i ) cnt[i] += cnt[i - 1]; // m 是值域
for (int i = n; i; -- i ) p[cnt[a[i]] -- ] = i;
把两边这个东西替换掉上面的 std::sort 即可。
然后是一些常数优化。首先如果某一轮循环结束后
然后我们考虑按
for (int i = n - k + 1; i <= n; ++ i ) id[ ++ nums] = i;
对于剩下的
for (int i = 1; i <= n; ++ i ) if (sa[i] > k) id[ ++ nums] = sa[i];
因此总代码为:
m = 130;
for (int i = 1; i <= n; ++ i ) cnt[rk[i] = s[i]] ++ ;
for (int i = 2; i <= m; ++ i ) cnt[i] += cnt[i - 1];
for (int i = n; i; -- i ) sa[cnt[rk[i]] -- ] = i;
for (int k = 1; k <= n; k <<= 1) {
nums = 0;
for (int i = n - k + 1; i <= n; ++ i ) id[ ++ nums] = i;
for (int i = 1; i <= n; ++ i )
if (sa[i] > k) id[ ++ nums] = sa[i] - k;
memset(cnt, 0, sizeof cnt);
for (int i = 1; i <= n; ++ i ) cnt[rk[i]] ++ ;
for (int i = 2; i <= m; ++ i ) cnt[i] += cnt[i - 1];
for (int i = n; i; -- i ) sa[cnt[rk[id[i]]] -- ] = id[i];
memcpy(b, rk, sizeof b);
m = 0;
for (int i = 1; i <= n; ++ i )
if (b[sa[i]] == b[sa[i - 1]] && b[sa[i] + k] == b[sa[i - 1] + k]) {
rk[sa[i]] = m;
} else {
rk[sa[i]] = ++ m;
}
if (m == n) break;
}
高度数组
我们定义高度数组
引理 1:
\operatorname{LCP}(i,j) = \min \operatorname{LCP}(i,k), \operatorname{LCP}(k,j) ,\forall i \le k \le j 。即将所有后缀排序后,任意两个后缀的最长公共前缀,是它们之间的某个后缀与自己的最长公共前缀的最小值。
证明之前明确一个事实。从
考虑分别证明不大于和不小于。
-
若 $i \le k \le j$,那么从 $s$ 的 $sa_i,sa_j,sa_k$ 位置往后数 $\operatorname{LCP}(i,j)$ 个字符形成的子串都是相同的。这个想一下就很清楚而且严谨证明也不难(夹逼定理(?))。 所以 $s[sa_i,sa_i + \operatorname{LCP}(i,j)-1] = s[sa_k,sa_k + \operatorname{LCP}(i,j)-1]$,那么一定有 $\operatorname{LCP}(i,k) \ge \operatorname{LCP}(i,j)$。$\operatorname{LCP}(k,j)$ 同理。 -
根据定义容易得到,从 $i,k$ 往后数 $\operatorname{LCP}(i,k)$ 个字符是相同的,从 $k,j$ 往后数 $\operatorname{LCP}(k,j)$ 个字符是相同的,所以从 $i,j$ 往后数 $\min \operatorname{LCP}(i,k),\operatorname{LCP}(k,j)$ 个字符是相同的。所以 $\operatorname{LCP}(i,j) \ge \min \operatorname{LCP}(i,k),\operatorname{LCP}(k,j)$。
由此可得:
引理 1 推论:
\operatorname{LCP}(i,j) = \min \operatorname{LCP}(i,i+1),\operatorname{LCP}(i+1,i+2),\dots,\operatorname{LCP}(j-1,j) 。进一步的
\operatorname{LCP}(i,j) \le \operatorname{LCP}(j-1,j) 。
这个是显然的,归纳一下即可。
引理 2:
h_{rk_i} \ge h_{rk_{i-1}}-1 。
这里会有点绕。
设「比后缀
-
-
否则
h_{rk_{i-1}} \ge 1 。那么「后缀j 和后缀i-1 的\operatorname{LCP} 」可以看作「后缀j+1 和后缀i 的\operatorname{LCP} 」在左边添加一个字符(s_{j} 或s_{i-1} )。即\operatorname{LCP}(rk_{j},rk_{i-1}) = \operatorname{LCP}(rk_{j+1},rk_{i})+1 。同时根据字典序排序的定义可得,因为后缀
j 小于后缀i-1 ,所以后缀j+1 小于后缀i 。再根据上面的推论,
\operatorname{LCP}(rk_{j+1},rk_i) \le \operatorname{LCP}(rk_{i}-1,rk_i) 。即\operatorname{LCP}(rk_{j},rk_{i-1})-1 \le \operatorname{LCP}(rk_{i}-1,rk_i) ,即h_{rk_i} \ge h_{rk_{i-1}}-1 。
根据引理 2 可以得到一个简单的线性求解高度数组的方法:
for (int i = 1, k = 0; i <= n; ++ i ) {
if (rk[i] == 1) h[rk[i]] = 0;
else {
if (k) -- k;
int j = sa[rk[i] - 1];
while (i + k <= n && j + k <= n && s[i + k] == s[j + k]) k ++ ;
h[rk[i]] = k;
}
}