KPKPKMP
KMP算法
真のKMP算法
在线性时间复杂度内匹配字符串(判断串是否是串的子串,并找出串在串中出现的位置)。
暴力匹配方法:
把的第一位与的每一位进行比较,若匹配则继续向后比较,否则将后移一位。
时间复杂度:
当比较到的某个位置与不匹配时,这个位置前的部分已经被比较过了,若继续逐个比较就会导致效率极低。效率高的做法是,若的某个前缀在已匹配部分的其它位置出现过(如图中的"AB"),可以直接将移至这些位置;否则已匹配部分中的字符都不可能与的首字符匹配,直接跳过。
而KMP算法通过针对计算出一张回退表来实现这个过程。
| 搜索词 | A | B | C | D | A | B | D |
|---|---|---|---|---|---|---|---|
| 回退表 | 0 | 0 | 0 | 0 | 1 | 2 | 0 |
串中每个位置的回退值就是当前已匹配部分的前缀与后缀集合中最长相同元素的长度。例如"ABCDABD"中第六位的回退值为,因为"ABCDAB"的前缀与后缀集合中最长相同元素为"AB",其长度为。
根据回退表,移动位数已匹配的字符数对应的回退值。
记两个串长度分别为。分别给串两个指针,始终满足的前个字符正好匹配的以结尾的长度为的子串,即与匹配。
- 若,则可以增加的值,继续往后比较。若某个时候,说明是的字串。
- 若,则将改为,相当于向右移动了位。
如何求串的数组?显然。然后从开始,拿两个串进行KMP,一边匹配一边记下每个位置的回退值。因为始终大于,当需要改为时,都已经算好了。
时间复杂度:
void kmp(char*a,char*b,int*fail){
int n=strlen(s+1),m=strlen(b+1);
fail[1]=0;
for(int i=2,j=0;i<=m;i++){
while(j>0&&b[j+1]!=b[i])j=fail[j];
if(b[j+1]==b[i])j++;
fail[i]=j;
}
for(int i=1,j=0;i<=n;i++){
while(j>0&&b[j+1]!=a[i])j=fail[j];
if(b[j+1]==a[i])j++;
if(j==m){cout<<i-m+1<<endl;j=fail[j];}
}
}
蓝色牛仔裤(NKOJ 1479)
问题描述
IBM和《国家地理》杂志共同研究的一个名为蓝色牛仔裤的项目,就是分析成千上万个捐赠的DNA,以便找出世界的人口是怎样构成和分布的。
作为IBM的一名研究员,你的任务就是写一个程序来研究不同DNA片段间的联系。
一个DNA序列由A、T、G、C四个字母来表示,比如"TAGACC"是一种长度为的DNA序列。
告诉你若干条DNA序列,请找出最长的一段连续DNA序列,该序列出现在了给出的所有DNA序列中(注:也就是求最长公共子串)。
输入格式
第一行,一个整数,表示下面有组测试数据()。
对于每组测试数据:
第一行,一个整数(),表示告诉你了条DNA序列。
接下来行,每行表示一条DNA序列,每行的长度不超过。
输出格式
对于每组测试数据,输出它的最长公共子串。
如果子串的长度小于,输出"no significant commonalities"。
如果有多条长度相等的最长子串,输出字典序最小的那条。
样例输入
3
2
GATACCAGATACCAGATACCAGATACCAGATACCAGATACCAGATACCAGATACCAGATA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
3
GATACCAGATACCAGATACCAGATACCAGATACCAGATACCAGATACCAGATACCAGATA
GATACTAGATACTAGATACTAGATACTAAAGGAAAGGGAAAAGGGGAAAAAGGGGGAAAA
GATACCAGATACCAGATACCAGATACCAAAGGAAAGGGAAAAGGGGAAAAAGGGGGAAAA
3
CATCATCATCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
ACATCATCATAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AACATCATCATTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT
样例输出
no significant commonalities
AGATAC
CATCATCAT
数据规模较小,可适当暴力。
枚举其中一个串的所有子串,用KMP判断它是否是其它所有串的子串。
子串长度从最短字符串的长度递减枚举到,找到的第一个公共子串一定是最长公共子串,但字典序不一定最小,所以要将这个长度的子串讨论完。
由于有提取子串和比较字典序等操作,善用string。
#include<iostream>
#include<string>
#include<algorithm>
using namespace std;
const int maxn=60+5;
string str[15];
int fail[maxn],n,m;
bool kmp(string&a,string b){
int n=a.length()-1,m=b.length()-1;
fail[1]=0;
for(int i=2,j=0;i<=m;i++){
while(j>0&&b[j+1]!=b[i])j=fail[j];
if(b[j+1]==b[i])j++;
fail[i]=j;
}
for(int i=1,j=0;i<=n;i++){
while(j>0&&b[j+1]!=a[i])j=fail[j];
if(b[j+1]==a[i])j++;
if(j==m)return 1;
}
return 0;
}
int main(){
cin>>n;
while(n--){
cin>>m;
int minlen=100,tmplen;
string ans="";
for(int i=1;i<=m;i++){
cin>>str[i];
str[i]=" "+str[i],minlen=min(minlen,(int)str[i].length()-1);
}
tmplen=str[1].length()-1;
for(int k=minlen;k>=3;k--){
for(int j=1;j+k-1<=tmplen;j++){
bool ok=1;
for(int i=2;i<=m;i++)
if(!kmp(str[i]," "+str[1].substr(j,k))){ok=0;break;}
if(!ok)continue;
string tmp=str[1].substr(j,k);
if(!ans.length()||ans>tmp)ans=tmp;
}
if(ans.length())break;
}
cout<<(ans.length()?ans:"no significant commonalities")<<endl;
}
return 0;
}
代码中有几个特别恶心的地方:
-
max和min函数(只要是模板函数)必须保证参数的数据类型一致,否则会报编译错误。
比如str.length()竟然不是int类型,需要强制转换成int。
同样,一个long long类型不能直接跟0取最大/小,应把0写成0ll。 - 用
cout输出善用括号,不然编译器可能会把<<当成左移再次报编译错误。
求最短循环节
数组有一个奇妙的性质:对于长度为的字符串,其最短循环节长度为。
但这样算出的循环节可能在原串中是不完整的。例如"ABCAB"算出的最短循环节为"ABC",长度为。所以有些题目需要特判。