SA 后缀数组

后缀数组 SA

定义

我们先定义如下两个数组:

  1. \(sa_i\) 代表排名为 \(i\) 的后缀的编号;
  2. \(rk_i\) 代表编号为 \(i\) 的后缀的排名。

此处排名指的是将所有后缀按字典序从小到大排序。

那么根据定义,显然可得结论:\(rk_{sa_i} = sa_{rk_i} = i\)

举例:

对于字符串 $s = $ abac,有

后缀 \(rk\)
\(suf(1)\) abac \(1\)
\(suf(2)\) bac \(3\)
\(suf(3)\) ac \(2\)
\(suf(4)\) c \(4\)

注:\(suf(i)\) 代表第 \(i\) 个后缀,即从 \(i \sim n\) 的字符串。

那么如上图可得 \(sa = \{1,3,2,4\}\)

算法

考虑几种做法,排列方式由时间复杂度从劣至优。

\(\mathcal{O(n^2 \log n)}\)

暴力,直接排序。

\(\mathcal{O(n \log^2 n)}\)

这里考虑一种倍增加上排序的方法,我们先算出字符串长度为 \(k = 1\) 时的 \(sa,rk\),然后将 \(k\) 倍增,并合并两个子串的排名,据此计算出最终的 \(sa,rk\)

具体过程如下:

执行 \(\log_2 n\) 次以下操作。
以字符串子串长度为 \(k / 2\) 时的 \(rk_i\) 为第一关键字,\(rk_{i + k / 2}\) 为第二关键字,进行排序,这样我们可以得到子串长度为 \(k\)\(rk\)\(sa\)
注意此处一个重点:

\[rk_{i + k / 2} = \left\{\begin{matrix} -\infty \quad \quad \hfill i + k / 2 > n \\ rk_{i + k / 2} \quad \hfill \text{otherwise.} \end{matrix}\right. \]

点击查看代码
il void SA() {
	rep(i,1,n) sa[i] = i,rk[i] = s[i];
	for (k = 1; k <= n; k <<= 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];
		});
		swap(rk,rk2);
		int tot = 0;
		rep(i,1,n) {
			if (rk2[sa[i]] != rk2[sa[i - 1]] || rk2[sa[i] + k] != rk2[sa[i - 1] + k]) ++ tot;
			rk[sa[i]] = tot;
		}
		if (tot == n) break;
	}
}

\(\mathcal{O(n \log n)}\)

我们考虑在以上倍增算法中的排序上做优化,优化成基数排序可将时间复杂度优化至 \(\mathcal{O(n \log n)}\)

具体过程如下:

  1. 我们先以 \(k = 1\) 来算出初始 \(sa,rk\),采用桶 + 前缀和,比如 ababc,进行前缀和过后 \(cnt_{`a`} = 2,cnt_{`b`} = 4,cnt_{`c`} = 5\),此时 \(cnt_i\) 表示元素 \(i\) 的最大排名,那么我们倒序枚举子串,调用 \(rk_i\) 得到此元素在哪个桶中,再调用 \(cnt_{rk_i}\) 获取单个 \(s_i\) 的排名,并初始化 \(sa\)
rep(i,1,n) ++ cnt[rk[i] = s[i]];
rep(i,1,m) cnt[i] += cnt[i - 1];
rep1(i,n,1) sa[cnt[rk[i]] --] = i;
  1. 对于 \(s_{n - k + 1,\dots ,n}\),显然他们的第二关键字均为 \(-\infty\),所以我们将其顺序放入 \(rk2\) 即存放第二关键字的数组中,再顺序遍历 \(sa_i\),因为我们是做倍增合并,所以当且仅当 \(sa_i > k\) 时,才能够作为编号为 \(sa_i - k\) 的子串的第二关键字,因为是顺序遍历的 \(sa\),所以此时 \(rk2\) 存放的就是以第二关键字排序好的子串编号;
int tot = 0;
rep(i,n - k + 1,n) rk2[++ tot] = i;
rep(i,1,n) if (sa[i] > k) rk2[++ tot] = sa[i] - k;
  1. 将当前状态的 \(rk\) 放入桶中,作前缀和,此时我们仅按照第一关键字来排了序,需要再使用第二关键字来排序,在桶中优先枚举第二关键字大的即可。
il void SA() {
	m = 200;
	rep(i,1,n) ++ cnt[rk[i] = s[i]];
	rep(i,1,m) cnt[i] += cnt[i - 1];
	rep1(i,n,1) sa[cnt[rk[i]] --] = i;
	for (int k = 1; k <= n; k <<= 1 ) {
		int tot = 0;
		rep(i,n - k + 1,n) rk2[++ tot] = i;
		rep(i,1,n) if (sa[i] > k) rk2[++ tot] = sa[i] - k;
		rep(i,1,m) cnt[i] = 0;
		rep(i,1,n) ++ cnt[rk[i]];
		rep(i,1,m) cnt[i] += cnt[i - 1];
		rep1(i,n,1) sa[cnt[rk[rk2[i]]] --] = rk2[i];
		swap(rk,rk2);
		rk[sa[1]] = 1; tot = 1;
		rep(i,2,n) {
			if (rk2[sa[i]] != rk2[sa[i - 1]] || rk2[sa[i] + k] != rk2[sa[i - 1] + k]) ++ tot;
			rk[sa[i]] = tot;
		}
		if (tot == n) break;
		m = tot;
	}
}

我们可以对比一下两条提交记录:

\(\mathcal{O(n \log^2 n)}\)

\(\mathcal{O(n \log n)}\)

时间快了将近 \(3\) 倍。

SA 可以通过拼接串等技巧解决许多问题。

应用和 \(height\) 先鸽一段时间。

posted @ 2024-01-22 11:34  songszh  阅读(13)  评论(0编辑  收藏  举报