KMP的next[]数组的深入理解3: / 或 二分 + 字符串哈希:匹配统计

该博客探讨了如何利用KMP算法解决字符串匹配问题,特别是如何优化算法以求解特定匹配长度的问题。博主详细分析了KMP算法中next数组的含义,并解释了如何通过next数组快速计算匹配长度至少为指定值的子串数量。此外,还介绍了二分搜索结合字符串哈希的另一种解决方案。博客内容深入浅出,适合对字符串算法感兴趣的读者。
摘要由CSDN通过智能技术生成

题目链接:https://www.acwing.com/problem/content/description/162/

题目:

阿轩在纸上写了两个字符串,分别记为 A 和 B。

利用在数据结构与算法课上学到的知识,他很容易地求出了“字符串 A 从任意位置开始的后缀子串”与“字符串 B”匹配的长度。

不过阿轩是一个勤学好问的同学,他向你提出了 Q 个问题:

在每个问题中,他给定你一个整数 x,请你告诉他有多少个位置,满足“字符串 A 从该位置开始的后缀子串”与 B匹配的长度恰好为 x。

例如:A=aabcde,B=ab,则 A有 aabcde、abcde、bcde、cde、de、e这 6个后缀子串,它们与 B=ab 的匹配长度分别是 1、2、0、0、0、0

因此 A 有 4 个位置与 B 的匹配长度恰好为 0,有 1个位置的匹配长度恰好为 1,有 1 个位置的匹配长度恰好为 2。

输入格式

第一行输入三个整数 N,M,Q,分别表示 A串长度、B 串长度、问题个数。

第二行输入字符串 A,第三行输入字符串 B。

接下来 Q 行每行输入 1 个整数 x,表示一个问题。

输出格式

输出共 Q 行,依次表示每个问题的答案。

数据范围

1≤N,M,Q,x≤200000

输入样例:

6 2 5
aabcde
ab
0
1
2
3
4

输出样例:

4
1
1
0
0

方法1:KMP的next[]数组的深入应用:

分析(1):

 进行KMP操作到a[i] == b[j]时

 而这过程中,我们先了解B中next[j]的含义是什么便于后面步骤的理解。

next[j]的具体含义是B的子字符串1~j中,最大长度为next【j】长的B的前缀与最大长度为next【j】长的后缀匹配。

也就是:

 从而可以知道

 所以可以了解到:

①以i为结尾的后缀A最长有j的长度与B的前缀长度为j匹配

②同时,也有以i为结尾的后缀A有next[j]长度与B的前缀长度为next[j]匹配。

③以此类推, next[ next[j] ]长度....一直到0.

分析(2):那么问题来了?知道了这三个有什么用呢?想要做什么呢?

创建一个数组f[len], f[len]的含义为:记录下A与B 匹配长度至少为len的情况的个数。

其中特别要注意的就是这个至少为len。为什么设定的是至少为len而不是向方法二一样统计刚好为len呢?我们题目要求的就是恰好为len的情况个数呀?

因为我们由上面的分析 (1)可以知道,以i为结尾,与B匹配的长度最长为 j, 也可以为ne[j],ne[ ne[j] ]....直到0.

而我们通过kmp的方式只能够确定到a[i] == b[j],然后继续看a[i + 1] 是否等于 b[j + 1].

所以实际上可能a[i + 1] == b[j + 1], a[i + 2 ] == b[j + 2].....

所以我们a[i] == b[j]只能够确定至少 i - j + 1 ~ i 可以与B的前 j 个匹配, 但有可能 a[i + 1] == b[j + 1].则就匹配了j + 1个。 

所以只能确定至少为j(此时j就是长度len了,因为a[i] == b[j])成立,从而让f[ j ]++进行统计.

分析3:那么f[len]怎么求呢?难道不就是f[len]++即可嘛?

实际上并不是的。

如上面分析(1)和分析(2)所说, a[i] == b[j],说明匹配的最大长度为j,而只要j存在,则ne[j],ne[ne[j]]就一定存在,直到ne[ne[ne[.....ne[j]]]]为0.  而长度为ne[j]也成立,长度为ne[ne[j]]也成立。代表的分别时 以i-j + 1为起点的后缀A与字符串匹配的长度至少为j.f[j]++.  

ne[j]则代表以 i - ne[j] + 1 为起点的后缀A的长度至少为ne[j],所以f[ne[j]]++.

以此类推。

j虽然包含了ne[j],ne[ne[j]]存在,但是j > ne[j] > ne[ne[j]]所以它们的A的匹配起点不同,而我们求的正是以A的各个点作为起点,所能匹配B的最大长度。

所以j存在,则f[j],f[ne[j]],f[ne[ne[j]]]....都要 ++.

但是这其中又有一个需要注意的点,如果每求出以此a[i] == b[j],就去使得f[j],f[ne[j]],f[ne[ne[j]]]等等去++,那么时间复杂度就变为O(N*M)了。显然这样是过不了这个题目的。 

分析4:所以需要对f[j],f[ne[j]],f[ne[ne[j]]]..++的这个步骤进行优化。

 画图后我们发现:

 原先暴力的时候,f[j]++,则f[ne[j]]++ , ..以此类推。

现在,我们先求完所有f[j], 然后 f[ne[j]]  += f[j], 不就将f[ne[j]]本该加的1都加了嘛。

从而使得时间复杂度由O(N * M)变为O(N).

分析5:最后,由于f[len]为匹配长度至少为len的总数,但我们题目求的是每一次恰好匹配长度只能为len的总个数,那么该怎么求呢?

长度为len ==  长度至少为len - 长度至少为 len + 1;

所以:答案就是 f[len] - f[len + 1]

代码实现:

# include <iostream>
using namespace std;
const int N = 200010;

int f[N];  // f[i],含义为 能够匹配的最小长度为i
int ne[N];
char a[N],b[N];

int n,m,q;

int main()
{
    scanf("%d %d %d",&n,&m,&q);
    cin >> a + 1 >> b + 1;
    
    // 求b的next[]数组
    for(int i = 2 , j = 0 ; i <= m ; i++)
    {
        while(j && b[i] != b[j + 1])
        {
            j = ne[j];
        }
        if(b[j + 1] == b[i])
        {
            j++;
        }
        ne[i] = j;
    }
    
    // 对A进行kmp
    for(int i = 1 ,j = 0; i <= n ; i++)
    {
        while(j && b[j + 1] != a[i])
        {
            j = ne[j];
        }
        if(b[j + 1] == a[i])  // A中i为结尾对于的后缀能够匹配的B中前缀的最大长度为j
        {
            j++;
        }
        f[j]++;  // 暴力的话应该为 f[j]++,f[ne[j]]++,f[ne[ne[j] ]]++,...
    }
    
    for(int i = m ; i > 0 ; i--)  // 为什么i不能为0呢?ne[1]的含义是B的以下标为1的字符串为结尾的后缀有0个字符串B的前缀和他相等,而ne[0]无实际含义,不存在。
    {
        f[ ne[i] ] += f[i];
    }
    
    while(q--)
    {
        int x;
        scanf("%d",&x);
        printf("%d\n",f[x] - f[x + 1]); // 匹配长度至少为x - 匹配长度至少为 x + 1 的结果就是匹配长度恰好为x的值
    }
    return 0;
}

方法2:二分 + 字符串哈希求解(相较于方法1而言要好理解的多)

分析:

 首先,暴力做法为:

以字符串A(长字符串)的每一个字符下标i  作为匹配字符串B的字符起点,然后通过一个一个进行比较 A[i] == B[1],A[i + 1] == B[2], 从而以i作为起点的子字符串与B匹配的最大长度。

统计下这个长度以后,对这个长度存入到数组中。total[长度]++

这样你要某个长度的时候就可以直接total[对应长度]。

 对暴力做法进行优化,因为是要找其长度的最大值,那么我们可以使用二分进行。

为什么可以使用二分呢? 因为,当mid长度作为以i为起点的字符串与字符串b匹配的时候,则1~mid的长度一定满足匹配字符串b,那么直接查找mid + 1 ~ 最后是否与字符串匹配即可。所以是单调的可以使用二分。

mid成立, 则 l = mid;  mid 不成立,则 r = mid - 1;

而对于某一个字符串中某一子串进行匹配的话,则又可以使用字符串哈希进行优化。

如将A中i ~ i + mid - 1 的字符串 与 B中 1 ~ mid的字符串进行匹配,则直接比较 A[i~i + mid - 1]的字符串哈希值与B[1~mid]的字符串哈希值是否相等即可。

相等,则二分成立, l = mid ,  不等,则二分不成立,r = mid - 1. 从而找到最大的匹配长度后,

total[长度]++. 

综上:此题可以使用 二分 + 字符串哈希求解。

代码实现: 

# include <iostream>
using namespace std;

const int N = 200010 , P = 131;

unsigned long long h1[N],h2[N],p[N];

int total[N];  //统计匹配的长度

int n,m,q;

unsigned long long get(unsigned long long * h,int l,int r)
{
    return h[r] - h[l - 1] * p[r - l + 1];
}

int main()
{
    scanf("%d %d %d",&n,&m,&q);
    p[0] = 1;
    for(int i = 1 ; i <= n ; i++)
    {
        char temp;
        cin >> temp;
        p[i] = p[i - 1] * P;
        h1[i] = h1[i - 1] * P + temp;
    }
    for(int j = 1 ; j <= m ; j++)
    {
        char temp;
        cin >> temp;
        h2[j] = h2[j - 1] * P + temp;
    }
    for(int i = 1 ; i <= n ; i++) // i作为匹配字符串的起点,最大字符串长度
    {
        int l = 0 , r = m;
        while(l < r)
        {
            //printf("拉拉%d %d\n",l,r);
            int mid = (l + r + 1) / 2; 
            if(get(h1,i,i + mid - 1) == get(h2,1, mid))
            {
                //cout << get(h1,i,i + 1) << get(h2,1,2) << endl;
                l = mid;
            }
            else
            {
                r = mid - 1;
            }
        }
        total[l]++; // 对应的最大长度加1
    }
    while(q--)
    {
        int temp;
        scanf("%d",&temp);
        printf("%d\n",total[temp]);
    }
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值