字符串是编程语言中表示文本的数据类型。许多字符串的问题,比如DP,统计方案数等,本质上都是要先解决字符串匹配问题。
本篇主要讲解5种算法:
将从算法的基础概念切入,循序渐进详解算法处理与实现,助你系统学习。其间还会穿插经典例题讲解,讲练结合,快速高效地掌握字符串相关知识,并能在实际编程中运用自如。
字符串最常见的应用就是字符串的匹配。
目前竞赛中解决这类问题的方法主要有:
哈希法:
KMP 算法:
扩展 KMP 算法:
Manacher 算法:
AC 自动机:
哈希表的引入:
如果要存储和使用线性表(1, 75, 324, 43, 1353, 90, 46)一般情况下我们会使用一个数组 A[1…7]来顺序存储这些数。但是这样的存储结构会给查询算法带来 O(n) 的时间开销。
对 A 排序,使用二分查询法,时间复杂度变为 O(logn)也可以用空间换时间的做法,用数组 A[1…1353] 来表示每个数是否出现,查找时间复杂度变为 O(1),但是空间上的开销变得巨大。
优化上一种做法,建立一个哈希函数 h(key) = key % 23. (1, 75, 324, 43, 1353, 90) -> (1, 6, 2, 20, 19, 21)
我们只要用一个 A[0…22] 数组就可以快速的查询。这种线性表的结构就称为哈希表(Hash Table)
可以看出,哈希表的基本原理是用一个下标范围相对比较大的数组 A 来存储元素。
设计一个函数 h,对于要存储的线性表的每个元素 node,取一个关键字 key,算出函数值 h(key) 然后把这个值作为下标,用 A[h(key)] 来存储 node。最常见的 h 就是模函数,也就是选定一个 m,令 h(key) = key % m。
哈希表的冲突:
可能存在两个 key: k1, k2 使得 h(k1) = h(k2), 这时也称产生了"冲突"。
解决冲突有很多种办法:
假设我们使用第二种方法解决冲突:
对于插入元素 (node, key):
哈希表的代码实现:
struct node{
int next, info;
}hashNode[N];
int tot; // 哈希表节点计数
int h[M]; // 初始化为 -1
// 插入操作
void insert(int key, int info){
int u = key % M;
hashNode[tot] = (node){h[u], info};
hu] = tot ++;
}
// 查找操作
int find(int key, int info){
int u = key % M;
for(int i=h[u]; i!=-1; i=hashNode[i].next){
if (hashNode[i].info == info)
return 1;
}
return 0;
}
边学边练:
已知 X[1…4] 是 [-T, T] 中的整数,求出满足方程:
AX[1] + BX[2] + CX[3] + DX[4] = P.
的解有多少组?
注:
∣ P ∣ ≤ 1 0 9 , ∣ A ∣ 、 ∣ B ∣ 、 ∣ C ∣ 、 ∣ D ∣ ≤ 1 0 4 , T ≤ 500. |P| \leq 10^{9}, |A|、|B|、|C|、|D| \leq 10^{4}, T \leq 500. ∣P∣≤109,∣A∣、∣B∣、∣C∣、∣D∣≤104,T≤500.
解:
最直观的方法枚举 X[1…4],时间复杂度 O ( n 4 ) O(n^{4}) O(n4)。
适当优化,枚举了 X[1…3] 之后,实际上 X[4] 已经确定了,时间复杂度 O ( n 3 ) O(n^{3}) O(n3)。
继续优化,采用 meet in the middle 策略:
枚举 X[1], X[2],然后算出 P-AX[1]-BX[2] 把这个值存入一个哈希表,注意要统计次数。这一步时间复杂度 O ( n 2 ) O(n^{2}) O(n2)。
然后枚举 X[3], X[4],算出 CX[3] + DX[4] 去哈希表里面查找这个值出现了几次。
把次数加进答案,这一步时间复杂度 O ( n 2 ) O(n^{2}) O(n2) 因此,总的时间复杂度是 O ( n 2 ) O(n^{2}) O(n2).
字符串中的哈希:
假设有 n 个长度为 L 的字符串,问其中最多有几个字符串是相等的。直接比较两个长度为 L 的字符串是否相等时间复杂度是 O(L)的。
因此,需要枚举 O ( n 2 ) O(n^{2}) O(n2) 对字符串进行比较,时间复杂度 O ( n 2 L ) O(n^{2}L) O(n2L) 。如果我们把每个字符串都用一个哈希函数映射成一个整数。问题就变成了查找一个序列的众数。时间复杂度变为了 O ( n L ) O(nL) O(nL)。
一个设计良好的字符串哈希函数可以让我们先用 O ( L ) O(L) O(L) 的时间复杂度预处理,之后每次获取这个字符串的子串的哈希值都只要 O ( 1 ) O(1) O(1) 的时间。
这里我们就重点介绍 BKDRHash:
BKDRHash:
BKDRHash 的基本思想就是把一个字符串当做一个 k 进制的数来处理。
int k = 19, M = 1e9+7;
int BKDRHash (char *str){
int ans = 0;
for (int i=0; str[i]; i++){
ans = (1LL*ans*k +str[i]) % M;
}
return ans;
}
假设字符串 s 的下标从 1 开始,长度为 n.
ha[0] = 0;
for (int i=1; i<=n; i++){
ha[i] = (ha[i-1]*k + str[i]) % M;
}
我们知道 ha[i] 就是 s[1…i] 的 BKDRHash,那么现在询问 s[x…y] 的 BKDRHash ,你能快速求解吗?
注意到:
h a [ y ] = s [ 1 ] k y − 1 + s [ 2 ] k y − 2 + . . . + s [ x − 1 ] k y − x + 1 + s [ x ] k y − x + . . . + s [ y ] ha[y] = s[1]k^{y-1} + s[2]k^{y-2} + ... + s[x-1]k^{y-x+1} + s[x]k^{y-x} + ... + s[y] ha[y]=s[1]ky−1+s[2]ky−2+...+s[x−1]ky−x+1+s[x]ky−x+...+s[y]
注意到:
h a [ x − 1 ] = s [ 1 ] k x − 2 + s [ 2 ] k x − 3 + . . . + s [ x − 1 ] ha[x-1] = s[1]k^{x-2} + s[2]k^{x-3} + ... + s[x-1] ha[x−1]=s[1]kx−2+s[2]kx−3+...+s[x−1]
而我们要求的 s[x…y] 的哈希值为 s [ x ] k y − x + . . . + s [ y ] s[x]k^{y-x} + ... + s[y] s[x]ky−x+...+s[y]
可以发现:
s [ x . . . y ] = h a [ y ] − h a [ x − 1 ] k y − x + 1 s[x...y] = ha[y] - ha[x-1]k^{y-x+1} s[x...y]=ha[y]−ha[x−1]ky−x+1.
因此,我们预处理出 ha 数组和 k 的幂次,每次询问 s[x…y] 的哈希值,只要 O(1) 的时间。
边学边练:
阿轩在纸上写了两个字符串,分别记为 A 和 B。利用在数据结构与算法课上学到的知识,他很容易地求出了“字符串 A 从任意位置开始的后缀子串”与“字符串B”匹配的长度。
不过阿轩是一个勤学好问的同学,他向你提出了 Q 个问题:在每个问题中,他给定你一个整数 x,请你告诉他有多少个位置,满足“字符串A从该位置开始的后缀子串”与B匹配的长度恰好为x。
例如:A = aabcde, B = ab, 则 A 有 aabcde、bcde、cde、de、e 这 6 个后缀子串,它们与 B = ab 的匹配长度分别是:1、2、0、0、0、0. 因此 A 有 4 个位置与 B 的匹配长度恰好为 0,有 1 个位置的匹配长度恰好为 1,有 1 个位置的匹配长度恰好为 2.
1 ≤ N , M , Q ≤ 200000. 1 \leq N, M, Q \leq 200000. 1≤N,M,Q≤200000.
核心问题就是:给定两个字符串 A, B。
求出 A 的每个后缀子串和 B 的最长公共前缀。标准做法就是扩展 KMP,时间复杂度为线性。我们先来用 Hash 试试看:
前面已经提到,我们可以用 O(n) 预处理, O(1) 处理处一个子串的哈希值。
求字符串 A[i…n] 与字符串 B[1…m] 的最长公共前缀?
二分!
二分长度 mid
计算出 A[i…i+mid-1] 和 B[i…mid] 的哈希值,比较是否相等。
因此,时间复杂度是 O(logn) 的!
ll getha (int x, int y){
return ha[y] - ha[x-1] * p[y-x+1];
}
ll gethb (int x, int y){
return hb[y] - hb[x-1] * p[y-x+1];
}
int main() {
scanf("%d%d%d", &n, &m, &);
scanf("%s", a+1);
scanf("%s", b+1);
p[0] = 1;
for (int i=1; i<=max(n,m); i++) p[i] = p[i-1] * P;
for (int i=1; i<=n; i++) ha[i] = ha[i-1] * P + a[i];
for (int i=1; i<=m; i++) hb[i] = hb[i-1] * P + b[i];
for (int i=1; i<=n; i++){
int L = 1, R = min(m, n-i+1), mid;
while (L <= R) {
mid = (L + R) >> 1;
if (getha(i, i+mid-1) == gethb(1, mid)){
L = mid + 1;
}else {
R = mid - 1;
}
}
cnt[R]++;
}
}