题目链接: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;
}