首页 > 其他分享 >倍增求后缀数组

倍增求后缀数组

时间:2024-10-31 21:21:50浏览次数:1  
标签:排序 后缀 text rank 关键字 数组 排名 倍增

倍增求后缀数组

1. 一些定义

后缀 \(i\):子串 \([\text{len}(S)-i+1,\text{len}(S)]\);

\(\text{SA}(i)\):排名为 \(i\) 的后缀;

\(\text{rank}(i)\):后缀 \(i\) 的排名,\(\forall i>n,\text{rank}(i)=0\)。

后缀数组即 \(\text{SA}\)。

2. 求法

先对每个单独的字符从小到大排序,得到每个字符的排名和后缀数组,记排名为 \(\text{rank}_1\)

再把 \(i\) 和 \(i+1\) 拼在一起,得到长度为 \(2\) 的串再排序。

以 \(\text{rank}_1(i)\) 为第一关键字,\(\text{rank}_1(i+1)\) 为第二关键字从小到大排序。

这样可以得到每个长度为 \(2\) 的串的排名和后缀数组,记排名为 \(\text{rank}_2\)

再把 \(i\) 和 \(i+2\) 拼在一起,得到长度为 \(4\) 的串再排序。

以 \(\text{rank}_2(i)\) 为第一关键字,\(\text{rank}_2(i+2)\) 为第二关键字从小到大排序。

这样可以得到每个长度为 \(4\) 的串的排名和后缀数组,记排名为 \(\text{rank}_4\)。

以此类推,对于长度 \(w\),将 \(i\) 和 \(i+w\) 拼在一起,得到长度为 \(2w\) 的串再排序。

以 \(\text{rank}_{w}(i)\) 为第一关键字,\(\text{rank}_w(i+w)\) 为第二关键字从小到大排序。

这样可以得到每个长度为 \(2w\) 的串的排名和后缀数组,记排名为 \(\text{rank}_{2w}\)。

一直倍增直到 \(w > n\) 即可,时间复杂度:\(O(n\log^2n)\)。

这样时间复杂度的瓶颈在于 \(O(n\log n)\) 的排序。

考虑到排名的值域为 \(O(n)\),可以使用基数排序优化至 \(O(n)\)。

这样总时间复杂度就为 \(O(n\log n)\)。

3. 基数排序

对于有 \(k\) 个关键字的排序,我们从第 \(k\) 个关键字枚举到第 \(1\) 个关键字,

每次对当前关键字做一次稳定的排序,便可完成整个排序。

一般选组计数排序作为【稳定的排序】,时间复杂度:\(O((n+w)k)\)。

这样为什么是对的呢?

对于第 \(i\) 个关键字,\([i+1,k]\) 的关键字都已排好。

由于第 \(i\) 个关键字的优先级更高,如果第 \(i\) 个关键字不等,直接按第 \(i\) 个排即可。

如果第 \(i\) 个关键字相等,由于计数排序为稳定的排序,相同元素的顺序不会改变,仍为 \([i+1,k]\) 关键字的排序结果。

4. 实现细节

在倍增时,可能会出现相同的字符串,这时它们的排名必须相等。

所以处理排名时还需要一个去重操作。

还有一个优化,如果某次处理时已经没有重复的字符串,

那最终排名和后缀数组都已确定,结束整个算法就行,实测优化很大。

代码

#include <bits/stdc++.h>
using namespace std;

const int N = 1e6 + 5;

int n, sa[N], rk[N << 1]; // 避免 i + w 越界
char S[N];
struct node {int key[2], id;};
node a[N], b[N];
int cnt[N];
void sort(int p, int w) { // 计数排序
	memset(cnt, 0, sizeof(cnt));
	for (int i = 1; i <= n; i ++) cnt[a[i].key[p]] ++;
	for (int i = 1; i <= w; i ++) cnt[i] += cnt[i - 1];
	for (int i = n; i >= 1; i --) b[cnt[a[i].key[p]] --] = a[i];
	for (int i = 1; i <= n; i ++) a[i] = b[i];
}

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin >> (S + 1);
	n = strlen(S + 1);
	for (int i = 1; i <= n; i ++) rk[i] = S[i]; // 第一遍排序
	for (int w = 1; w <= n; w <<= 1) {
		int maxw = 0;
		for (int i = 1; i <= n; i ++) // i 为第一关键字, i + w 为第二关键字
			a[i] = {{rk[i], rk[i + w]}, i},
			maxw = max(maxw, rk[i]);
		sort(1, maxw); sort(0, maxw); // 第二关键字先排序, 第一关键字后排序
		int p = 0;
		for (int i = 1; i <= n; i ++) {
			if (a[i].key[0] == a[i - 1].key[0] // 去重操作
			 && a[i].key[1] == a[i - 1].key[1]) rk[a[i].id] = p;
			else rk[a[i].id] = ++ p;
			sa[i] = a[i].id; // 统计后缀数组
		}
		if (p == n) break; // 没有重复字符串, 优化
	}
	for (int i = 1; i <= n; i ++) cout << sa[i] << " ";
	return 0;
}

标签:排序,后缀,text,rank,关键字,数组,排名,倍增
From: https://www.cnblogs.com/maniubi/p/18518909

相关文章

  • 深入理解计算机系统 3.6 数组分配和访问
    C语言中的数组是一种将标量数据聚集成更大数据类型的方式。C语言实现数组的方式非常简单,因此很容易翻译成机器代码。C语言一个不同寻常的特点是可以产生指向数组中元素的指针,并对这些指针进行运算。在机器代码中,这些指针会被翻译成地址计算。3.6.1 基本原则对于数据类型T和......
  • 每日算法一练:剑指offer——数组篇(7)
    1.文物朝代确认        展览馆展出来自13个朝代的文物,每排展柜展出5个文物。某排文物的摆放情况记录于数组 places,其中 places[i] 表示处于第 i 位文物的所属朝代编号。其中,编号为0的朝代表示未知朝代。请判断并返回这排文物的所属朝代编号是否能够视为连......
  • 数组排序简介-计数排序(Counting Sort)
    基本思想        通过统计数组中每个元素在数组中出现的次数,根据这些统计信息将数组元素有序的放置到正确位置,从而达到排序的目的。算法步骤计算排序范围:遍历数组,找出待排序序列中最大值元素 nums_max和最小值元素 nums_min,计算出排序范围为 nums_max−nums_min......
  • 数组排序简介-快速排序(Quick Sort)
    基本思想        采用经典的分治策略,选择数组中某个元素作为基准数,通过一趟排序将数组分为独立的两个子数组,一个子数组中所有元素值都比基准数小,另一个子数组中所有元素值都比基准数大。然后再按照同样的方式递归的对两个子数组分别进行快速排序,以达到整个数组有序。......
  • [SCOI2014] 方伯伯的玉米田(树状数组优化 DP)
     loj传送门https://loj.ac/p/2211洛谷题目传送门https://www.luogu.com.cn/problem/P3287解题思路首先,我们可以贪心地思考一下:对于每一次区间的加一操作,右端点是在末尾会比右端点在中间的情况更好。因为,当你的右端点在序列中间的时候,相对之下,后面的数就更小了一些,这样是......
  • 108-二叉树-将有序数组转换为二叉搜索树
    树|二叉搜索树|数组|分治|二叉树二叉搜索树(BinarySearchTree,简称BST)和平衡二叉搜索树(BalancedBinarySearchTree)是计算机科学中非常重要的数据结构,广泛应用于各种算法和系统中。理解它们的定义、特点和性质对于掌握数据结构和算法至关重要。下面,我们将详细......
  • 线段树 & 树状数组
    线段树常用于维护区间值代码和题解有很大差异,但是过了就好voidPushup(intx){ s[x]=(s[x<<1]+s[x<<1|1]);}voidPushdown(intx,intl,intr){ s[x]=(s[x]+ad[x]*(r-l+1)); if(l!=r)ad[x<<1]=(ad[x<<1]+ad[x]); if(l!=r)ad[x<<1|1]=(ad[x<<1|1]+ad[x......
  • DAY48|| 300.最长递增子序列 | 674. 最长连续递增序列 | 718. 最长重复子数组
     300.最长递增子序列300.最长递增子序列-力扣(LeetCode)给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。 示例......
  • 总结 JavaScript 中8种数组常用的操作 API,array.push,pop,shift,unshift,slice,splice
    前言JavaScript中数组是一个重要的数据结构,它相比于字符串有更多的方法,在一些算法题中我们经常需要将字符串转化为数组,使用数组里面的API进行操作。本篇文章总结了JavaScript中有许多数组常用的操作API,以下是一些常见的操作及其示例:1.push():在数组末尾添加一个或多个元素,并......
  • leetcode560 和为k的子数组
    leetcode560和为k的子数组packagejava2024_10.day30;importjava.util.HashMap;publicclassleetcode560{/*思路:前缀和+哈希表a[j]-a[i]=k即a[i]=a[j]-k遍历到下标j的时候,先判a[j]==k,相等就ans++,然后查哈希表中a[j]-k的数的个数,然后把a[j]放入哈希......