基础数据结构 - 字符串

字符串匹配问题,即求出字符串 A A A 在字符串 B B B 中各次出现的位置。

普通的暴力求解,其时间复杂度就是 O ( N M ) O(NM) O(NM) ,字符串哈希可以 O ( N ) O(N) O(N) 处理,而 K M P KMP KMP 算法能更高效、更准确地处理这类问题。

KMP 模式匹配

K M P KMP KMP 算法分两步:

  1. 对于字符串 A A A 进行自我 “匹配” ,求出一个 next 数组,其中 next[i] 表示 “ a a a 中以 i i i 结尾的非前缀子串” 与 “ A A A 的前缀” 能够匹配的最大长度,即:
    n e x t [ i ] = m a x { j } , j < i   a n d   A [ i − j + 1 ∼ i ] = A [ 1 ∼ j ] next[i] = max\{j\},j<i ~and~A[i-j+1\thicksim i] = A[1\thicksim j] next[i]=max{j},j<i and A[ij+1i]=A[1j]
    当不存在能匹配的前缀时, n e x t [ i ] = 0 next[i] = 0 next[i]=0
  2. 对字符串 A A A B B B 进行匹配,求出一个 f f f 数组,其中 f [ i ] f[i] f[i] 表示 “ B B B 中以 i i i 结尾的子串” 与 “ A A A 的前缀” 能够匹配的最大长度,即:
    f [ i ] = m a x { j } , j < i   a n d   B [ i − j + 1 ∼ i ] = A [ 1 ∼ j ] f[i] = max\{j\},j<i ~and~B[i-j+1\thicksim i] = A[1\thicksim j] f[i]=max{j},j<i and B[ij+1i]=A[1j]

最后在 f [ i ] = n f[i] = n f[i]=n 的位置就是字符串 A A A B B B 完美匹配的地方。

现在看如何计算 n e x t next next 数组,第一种方法肯定就是暴力。

枚举 j ∈ [ 1 , i − 1 ] j\in [1,i -1] j[1,i1] ,检查 A [ i − j + 1 ∼ i ] A[i-j+1\thicksim i] A[ij+1i] A [ 1 ∼ j ] A[1\thicksim j] A[1j] 是否相等。在枚举中所有满足条件的最大值就是 n e x t [ i ] next[i] next[i] 了。但是时间复杂度是 O ( n 2 ) O(n^2) O(n2) ,显然不行。如何优化呢?

引理

j 0 j_0 j0 n e x t [ i ] next[i] next[i] 的一个 “候选项”,即 j 0 < i j_0 < i j0<i A [ i − j 0 + 1 ∼ i ] = A [ 1 ∼ j 0 ] A[i - j_0 + 1 \thicksim i] = A[1\thicksim j_0] A[ij0+1i]=A[1j0] ,则小于 j 0 j_0 j0 的最大的 n e x t [ i ] next[i] next[i] 的 “候选项” 是 n e x t [ j 0 ] next[j_0] next[j0] 。换而言之, n e x t [ j 0 ] + 1 ∼ j 0 − 1 next[j_0] + 1 \thicksim j_0 - 1 next[j0]+1j01 之间的数都不是 n e x t [ i ] next[i] next[i] 的 “候选项” 。

书上证明是采用反证法。就是假设中途有一个 j 1 j_1 j1 也是 “候选项” ,但根据推理,不可能存在这个 j 1 j_1 j1 ,所以矛盾了,因此证毕。

使用引理优化 n e x t next next 数组求法

根据引理,当 n e x t [ i − 1 ] next[i -1] next[i1] 计算完毕时,我们就已经得知了 n e x t [ i − 1 ] next[i - 1] next[i1] 的所有 “候选项” 从大到小依次是 n e x t [ i − 1 ] next[i - 1] next[i1] n e x t [ n e x t [ i − 1 ] ] next[next[i - 1]] next[next[i1]] ,··· 而如果整数 j j j n e x t [ i ] next[i] next[i] 的 “候选项” 的话,那么 j − 1 j - 1 j1 肯定是 n e x t [ i − 1 ] next[i - 1] next[i1] 的 “候选项” 了,这个可以画图理解。因此在计算 n e x t [ i ] next[i] next[i] 时,只需将 n e x t [ i − 1 ] + 1 next[i - 1] + 1 next[i1]+1 n e x t [ n e x t [ i − 1 ] ] + 1 next[next[i - 1]] + 1 next[next[i1]]+1 ,··· 作为 “候选项” 就行了,如果 A [ i ] = = A [ j + 1 ] A[i] == A[j + 1] A[i]==A[j+1] 的话,显然 n e x t [ i ] = n e x t [ j ] + 1 next[i] = next[j] + 1 next[i]=next[j]+1 了。

因此 K M P KMP KMP 算法求 n e x t next next 数组 :

  1. 初始化 n e x t [ 1 ] = j = 0 next[1] = j = 0 next[1]=j=0 ,假设 n e x t [ 1 ∼ i − 1 ] next[1\thicksim i - 1] next[1i1] 已求出,下面求解 n e x t [ i ] next[i] next[i]
  2. 不断扩展匹配长度 j j j ,如果扩展失败(下一个字符不相等),令 j j j 变为 n e x t [ j ] next[j] next[j] ,直到 j = 0 j = 0 j=0
  3. 如果扩展成功,匹配长度 j j j 就增加 1 1 1 n e x t [ i ] = j next[i] = j next[i]=j

代码如下:

next[1] = 0;
for(int i = 1, j = 0; i <= n; ++i) {
	while(j > 0 && a[i] != a[j + 1]) j = next[j];
	if(a[i] == a[j + 1]) j++;
	next[i] = j;
}

因为定义相似,所以求解 f f f 与求解 n e x t next next 基本一致。

代码如下:

for(int i = 1, j = 0; i <= n; ++i) {
	while(j > 0 && (j == n || b[i] != a[j + 1])) j = next[j];
	if(b[i] == a[j + 1]) j++;
	f[i] = j;
	//if(f[i] == n) 此时就是匹配成功的位置。
}

【例题】周期

一个字符串的前缀是从第一个字符开始的连续若干个字符,例如 abaab 共有 5 5 5 个前缀,分别是 aababaabaaabaab

我们希望知道一个 N N N 位字符串 S S S 的前缀是否具有循环节。

换言之,对于每一个从头开始的长度为 i i i i > 1 i>1 i>1)的前缀,是否由重复出现的子串 A A A 组成,即 A A A … A AAA…A AAAA A A A 重复出现 K K K 次, K > 1 K>1 K>1)。

如果存在,请找出最短的循环节对应的 K K K 值(也就是这个前缀串的所有可能重复节中,最大的 K K K 值)。

数据范围
2 ≤ N ≤ 1000000 2≤N≤1000000 2N1000000

分析:

因为是循环节,所以肯定与 K M P KMP KMP n e x t next next 数组有关。而且也确实有这么个性质,即 S [ 1 ∼ i ] S[1\thicksim i] S[1i] 具有长度为 l e n < i len < i len<i 的循环元的充要条件是 l e n len len 能整除 i i i 并且 S [ l e n + 1 ∼ i ] = S [ 1 ∼ i − l e n ] S[len + 1\thicksim i] = S[1\thicksim i - len] S[len+1i]=S[1ilen] (即 i − l e n i - len ilen n e x t [ i ] next[i] next[i] 的 “候选项” )。

书上证明是先从必要性,显然具有循环元的,自然为候选项。然后是充分性,可以通过画图推导。这个 n e x t next next 数组里的 l e n len len 如果不能被 i i i 整除则表示是一个不完整的长度为 l e n len len 的循环元,后面的习题会用上这个性质。

因此我们求出 n e x t [ i ] next[i] next[i] 后,判断 i % ( i − n e x t [ i ] ) = = 0   a n d   i / ( i − n e x t [ i ] ) > 1 i \% (i - next[i]) == 0 ~and ~i/(i-next[i]) > 1 i%(inext[i])==0 and i/(inext[i])>1 就行了。

代码如下:

#include <bits/stdc++.h>
using namespace std;
const int N = 1000010;

char a[N];
int ne[N];
int n;

void init() {
    ne[1] = 0;
    for(int i = 2, j = 0; i <= n; ++i) {
        while(j > 0 && a[i] != a[j + 1]) j = ne[j];
        if(a[i] == a[j + 1]) j ++;
        ne[i] = j;
    }
}

int main()
{
    int tt = 1;
    while(scanf("%d", &n) && n) {
        
        scanf("%s", a + 1);
        init();
        printf("Test case #%d\n", tt++);
        for(int i = 2; i <= n; ++i) {
            int m = ne[i];
            if(i % (i - m) == 0 && i / (i - m) > 1) {
                cout << i << ' ' << i / (i - m) << endl;
            }
        }
        
        puts("");
    }    
    
    return 0;
}

最小表示法

给定一个字符串 S [ 1 ∼ n ] S[1\thicksim n] S[1n] ,如果不断把它最后一个字符放在开头,最终会得到 n n n 个字符串,称这 n n n 个字符串是循环同构的。而其中字典序最小的一个,称为字符串的最小表示。

朴素求法很容易求,不过时间复杂度为 O ( n 2 ) O(n^2) O(n2) 。所有产生了一种 O ( n ) O(n) O(n) 算法。

其算法过程就是,先将字符串 S S S 变成两倍,这样我们就只需要在这个新串上找到字典序最小的、长度为 n n n 的子串就行了,而在找的过程中使用双指针可以发现如果 i + k i + k i+k 的字符 和 j + k j + k j+k 字符不一样,那么如果 S S [ i + k ] > S S [ j + k ] SS[i +k] > SS[j+ k] SS[i+k]>SS[j+k] ,就说明 i + 1 ∼ i + k i + 1\thicksim i+k i+1i+k 开头的所有子串都不可能是最小的啦,那么根据这个单调性,就可以优化到 O ( n ) O(n) O(n) 了。

代码如下:

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

string get_min(string str) {
    string S = str + str;
    int n = str.size();
    int i = 0, j = 1, k;
    while (i < n && j < n) {
        for(k = 0; k < n && S[i + k] == S[j + k]; ++k);
        
        if(k == n) break;
        
        if(S[i + k] > S[j + k]) {
            i = i + k + 1;
            if(i == j) i ++ ;
        } else {
            j = j + k + 1;
            if(i == j) j++;
        }
    }
    
    return S.substr(min(i, j), n);
}

int main() {
    string str;
    cin >> str;
    cout << get_min(str) << endl;
    return 0;
}
  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值