KPKPKMP

KMP算法

真のKMP算法

在线性时间复杂度内匹配字符串(判断串BB是否是串AA的子串,并找出串BB在串AA中出现的位置)。

暴力匹配方法:
BB的第一位与AA的每一位进行比较,若匹配则继续向后比较,否则将BB后移一位。
时间复杂度:O(n2)O(n^2)
【字符串】KMP算法
当比较到BB的某个位置与AA不匹配时,这个位置前的部分已经被比较过了,若继续逐个比较就会导致效率极低。效率高的做法是,若BB的某个前缀在已匹配部分的其它位置出现过(如图中的"AB"),可以直接将BB移至这些位置;否则已匹配部分中的字符都不可能与BB的首字符匹配,直接跳过。
而KMP算法通过针对BB计算出一张回退表来实现这个过程。

搜索词BB A B C D A B D
回退表failfail 0 0 0 0 1 2 0

【字符串】KMP算法
BB中每个位置的回退值就是当前已匹配部分的前缀与后缀集合中最长相同元素的长度。例如"ABCDABD"中第六位的回退值为22,因为"ABCDAB"的前缀与后缀集合中最长相同元素为"AB",其长度为22
根据回退表,移动位数==已匹配的字符数-对应的回退值

记两个串长度分别为n,mn,m。分别给串A,BA,B两个指针i,ji,j,始终满足BB的前jj个字符正好匹配AA的以A[i]A[i]结尾的长度为jj的子串,即B[1j]B[1\dots j]A[ij+1i]A[i-j+1\dots i]匹配。

  • A[i+1]=B[j+1]A[i+1]=B[j+1],则可以增加i,ji,j的值,继续往后比较。若某个时候j=mj=m,说明BBAA的字串。
  • A[i+1]B[j+1]A[i+1]\neq B[j+1],则将jj改为fail[j]fail[j],相当于BB向右移动了jfail[j]j-fail[j]位。

如何求串BBfailfail数组?显然fail[1]=0fail[1]=0。然后从i=2i=2开始,拿两个BB串进行KMP,一边匹配一边记下每个位置的回退值。因为ii始终大于jj,当jj需要改为fail[j]fail[j]时,fail[1j]fail[1\dots j]都已经算好了。
时间复杂度:O(n)O(n)

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"是一种长度为66的DNA序列。
告诉你若干条DNA序列,请找出最长的一段连续DNA序列,该序列出现在了给出的所有DNA序列中(注:也就是求最长公共子串)。

输入格式
第一行,一个整数nn,表示下面有nn组测试数据(n20n\leqslant 20)。
对于每组测试数据:
第一行,一个整数mm2m102\leqslant m\leqslant 10),表示告诉你了mm条DNA序列。
接下来mm行,每行表示一条DNA序列,每行的长度不超过6060

输出格式
对于每组测试数据,输出它的最长公共子串。
如果子串的长度小于33,输出"no significant commonalities"。
如果有多条长度相等的最长子串,输出字典序最小的那条。

样例输入
3
2
GATACCAGATACCAGATACCAGATACCAGATACCAGATACCAGATACCAGATACCAGATA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
3
GATACCAGATACCAGATACCAGATACCAGATACCAGATACCAGATACCAGATACCAGATA
GATACTAGATACTAGATACTAGATACTAAAGGAAAGGGAAAAGGGGAAAAAGGGGGAAAA
GATACCAGATACCAGATACCAGATACCAAAGGAAAGGGAAAAGGGGAAAAAGGGGGAAAA
3
CATCATCATCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC
ACATCATCATAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AACATCATCATTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTT

样例输出
no significant commonalities
AGATAC
CATCATCAT

数据规模较小,可适当暴力。
枚举其中一个串的所有子串,用KMP判断它是否是其它所有串的子串。
子串长度从最短字符串的长度递减枚举到33,找到的第一个公共子串一定是最长公共子串,但字典序不一定最小,所以要将这个长度的子串讨论完。
由于有提取子串和比较字典序等操作,善用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;
}

代码中有几个特别恶心的地方:

  • maxmin函数(只要是模板函数)必须保证参数的数据类型一致,否则会报编译错误。
    比如str.length()竟然不是int类型,需要强制转换成int
    同样,一个long long类型不能直接跟0取最大/小,应把0写成0ll
  • cout输出善用括号,不然编译器可能会把<<当成左移再次报编译错误。

求最短循环节

failfail数组有一个奇妙的性质:对于长度为lenlen的字符串,其最短循环节长度为lenfail[len]len-fail[len]
但这样算出的循环节可能在原串中是不完整的。例如"ABCAB"算出的最短循环节为"ABC",长度为33。所以有些题目需要特判。

相关文章: