未完成
KMP算法
KMP思想
1.如图所示,在主串s中搜素匹配模式串p的子串的位置时,主串的位置i与模式串的位置j并不匹配。
2.在如上图的不匹配的情况下,就需要将模式串p后移。传统的方法是将模式串p整体后移一个单位。从p的第一个字符开始重新进行比较。
3.上面方法是存在大量冗余的,而KMP算法,旨在利用前一次匹配的结果,在不匹配的情况下,可以直接向后移动若干位,而不是一位。这种思想可以等价于主串s的位置i在满足一定条件的情况下,应该与模式串的哪一位对齐,我们这里设置为第k位。如图
4.如图所示。如果主串中的位置i应该与模式串中的位置k对齐,那么需要满足的条件是:两个直线中间的区域的值是相等的。如图可以得到:
pj−k+1…pj−1=p1…pk−1
其中:
k<j
KMP思想:如果模式串的第j位与主串的第i位不匹配,且在模式串的前j位中,满足前缀与后缀相同的最大位数为k-1,那么应该继续让子串的第k位与主串的第i位比较。
易知:k为模式串的各位所共有的性质,不随主串的改变而改变。那么如何求得每一位对应的K,也就是KMP的核心next[j]?
5.如何求next[i];
归纳法证明:
如果
i=1
,也就是第一个元素就没有匹配,所以应该直接后移,next[i]=0;(当next[i]=0时,直接后移)
如果
i>1
,如图,假设next[i]=j,那么对于next[i+1],就有下面两种情况:
-若
pi=pj
,则next[i+1]=next[i]+1;
-若
pi≠pj
,如图:
需要将下方的模式串后移,移动的距离如图所示,使得next[j]与i位置相对应。
注:代码的实现是是按照字符串第0个单元的数据有效来编辑的。
c++代码实现
void getNext(char pattern[],int* next){
/*next[i]表示长度为i的字符串的next值,所以next的范围为next[0]~next[length-1]*/
int i=0,j=-1;
next[0]=-1;/*next[0]=-1*/
while(i<strlen(pattern)){
/*next[j]=-1说明第一个元素也没有匹配*/
if(j==-1 || pattern[i]==pattern[j]){
i++;
j++;
next[i]=j;
}
else j=next[j];
}
}
KMP代码实现
//============================================================================
// Name : KMP.cpp
// Author : SELOUS
// Version :
// Copyright : Your copyright notice
// Description : KMP Algorithm;Handle String
//============================================================================
#include <iostream>
#include <cstring>
using namespace std;
void getNext(char pattern[],int* next){
/*next[i]表示长度为i的字符串的next值,所以next的范围为next[1]~next[length]*/
int i=0,j=-1;
next[0]=-1;/*next[0]=-1*/
while(i<strlen(pattern)){
/*next[j]=-1说明第一个元素也没有匹配*/
if(j==-1 || pattern[i]==pattern[j]){
i++;
j++;
next[i]=j;
}
else j=next[j];
}
}
int indexKMP(char *sstr,int slen,char *pattern,int plen,int pos,const int* next){
int i = pos;//从pos位置开始匹配
int j = 0;
/**/
while(i<slen && j<plen){
if(j==-1||sstr[i] == pattern[j]){
i++;
j++;
}else{
j=next[j];
}
}
if(j>=plen){
return i-plen;
}
else return 0;
}
int main() {
char sstr[80]="sdddafaffsabaabcacssss\0";
char str[80]="sabaabcac\0";
int next[80];
getNext(str,next);
// for(int i=1;i<=8;i++){
// cout<<next[i]<<endl;
//}
cout<<indexKMP(sstr,strlen(sstr),str,strlen(str),0,next)<<endl;
return 0;
}
字典树
基本讲解
概念:Trie树,是哈希树的一个变种,将给定的字符串按照字典的形式储存,然后在其上进行相应的操作。例如:给定字符串ab,abd,bs,def,degf,建立的字典树如下图所示。
特点: 1.root节点为空节点,不储存相关信息。
2.明显的节省了空间,
3.每个节点回朔到根节点,为该节点对应的字符串。
4.每个节点的直接子节点的数据都不相同。
解决问题:(解决问题引自博客:http://blog.sina.com.cn/s/blog_6002b97001015vyi.html)
(1) 字符串检索
事先将已知的一些字符串(字典)的有关信息保存到trie树里,查找另外一些未知字符串是否出现过或者出现频率。
举例:
@ 给出N 个单词组成的熟词表,以及一篇全用小写英文书写的文章,请你按最早出现的顺序写出所有不在熟词表中的生词。
@ 给出一个词典,其中的单词为不良单词。单词均为小写字母。再给出一段文本,文本的每一行也由小写字母构成。判断文本中是否含有任何不良单词。例如,若rob是不良单词,那么文本problem含有不良单词。
(2)字符串最长公共前缀
Trie树利用多个字符串的公共前缀来节省存储空间,反之,当我们把大量字符串存储到一棵trie树上时,我们可以快速得到某些字符串的公共前缀。
举例:
@ 给出N 个小写英文字母串,以及Q 个询问,即询问某两个串的最长公共前缀的长度是多少?
解决方案:首先对所有的串建立其对应的字母树。此时发现,对于两个串的最长公共前缀的长度即它们所在结点的公共祖先个数,于是,问题就转化为了离线(Offline)的最近公共祖先(Least Common Ancestor,简称LCA)问题。
而最近公共祖先问题同样是一个经典问题,可以用下面几种方法:
利用并查集(Disjoint Set),可以采用采用经典的Tarjan 算法;
求出字母树的欧拉序列(Euler Sequence )后,就可以转为经典的最小值查询(Range Minimum Query,简称RMQ)问题了;
(关于并查集,Tarjan算法,RMQ问题,网上有很多资料。)
(3)排序
Trie树是一棵多叉树,只要先序遍历整棵树,输出相应的字符串便是按字典序排序的结果。
举例:
@ 给你N 个互不相同的仅由一个单词构成的英文名,让你将它们按字典序从小到大排序输出。
(4) 作为其他数据结构和算法的辅助结构
如后缀树,AC自动机等
在使用字典树解决字符串问题的时候,因为处理的是大多是两类字符的问题,比如求A文章出现的单词中,以B为前缀的单词个数。就需要明确是以A还是以B为数据建立字典树。以B为数据建立字典树,前缀固定,文章可以动态改变,建树方便,查询麻烦。若以A为数据建立字典树,文章固定,可以求任意前缀出现的数目,建树麻烦,查询方便,所以需要根据题目不同的要求去权衡选择。
字典树是一个很简单的数据类型,不过能解决很多看起来很复杂的问题,而且时间复杂度很低,在实际应用中也很广泛。
代码实现
题目:http://acm.hdu.edu.cn/showproblem.php?pid=1251
1.树节点和初始化
typedef struct TrieNode{
int v;//information upon trie node
struct TrieNode* next[MAX];
/*constructed function*/
TrieNode(){
//initial v to zero
v=0;
int i=0;
/*assign next[i] to NULL*/
for(;i<MAX;i++){
next[i]=NULL;
}
};
}trieNode,*Trie;
//initial root of trie tree;
void init(Trie &T){
T = new trieNode();
}
2.建树:
//insert a node into tree
void insert(Trie &T,char *str){
trieNode *p = T;
int i = 0;
int len = strlen(str);
int node;//记录数据对应节点的位置
for(;i<len;i++){
node = str[i]-'a';
if(p->next[node]==NULL){
p->next[node]= new TrieNode();
}
p=p->next[node];
p->v++;
}
}
3.查询:
int search(Trie &T,char *prefix){
trieNode *p = T;
int i=0;
int len = strlen(prefix);
for(;i<len;i++){
int node = prefix[i]-'a';
if(p->next[node]==NULL){
return 0;
}
p=p->next[node];
}
return p->v;
}
AC自动机
基本概念讲解
AC自动机的基础是trie树。先通过模式串建立字典树,每个节点都维护一个fail指针,指向没有匹配之后跳转到的节点。fail指针需满足的条件为:s节点的fail指针指向的p节点需满足,字符串[root~p]与[root,s]字符串的后缀相同。
自动机建立过程举例:模式串为she,he,shr,her,say。
1.建立字典树,如图所示:
2.对于root节点的两个子节点s和h,也就是第一个节点,易知s和h的fail指针指向为root。
3.对于第二层的节点a,h,e,三个节点,我们考虑第二层的h节点。通过观察可知,[root,h1]的前缀h,与[root,h2]的后缀h是相同的,所以h2的fail节点是指向h1的。
理论证明:求节点n的fail指针?如果节点n的fail指针指向节点m,那么必定满足[root,n]的后缀,等于[root,m]。那么对于n的父节点n_parent来说,必定满足[root,n_parent]的后缀等于[root,n_parent_fail];因为字典树所固有的前缀相同的性质,所以n节点的fail指针必落在[root,n_parent_fail]这个树的分支上。
一维化表示:
如图,如果节点m和节点n的值相同,那么节点n的fail指针就指向节点m。即n->fail = n->parent->fail[ID[n]];如果两个节点的值不相等。如图:
就让图中的节点m1与其比较,m1的求法为:n->parent->fail->fail的子节点m1。
代码实现
注:ac自动机的代码和kmp代码一样,里面的逻辑很强,本人不是专门搞csp的,所以我只是搞懂了ac自动机的基本思想,关于代码实现也只是凭自己的理解实现的,不想测试了,所以代码可能有问题。如果你是专业的可以下面的代码就不读了。防止误人子弟。
1.根据模式串构建trie树
#define MAX_NODE_NUM 30
#define MAX_CHILD_NUM 26
typedef struct TNode{
int count;//flag of whether the end of string
struct TNode *next[MAX_CHILD_NUM];//child nodes
struct TNode *fail;//fail point
TNode(){
count = 0;
fail = NULL;
memset(next,NULL,sizeof(next));
}
}TNode,*Tree;
TNode node[MAX_NODE_NUM];
int nodeNum = 0;
void build(Tree& T){
T = &node[nodeNum];
nodeNum++;
}
void insert(Tree &T,string s){
TNode *p=T;
int index;
int i = 0;
for(;i < s.length();i++){
index = s[i]-'a';
if(p->next[index]==NULL){
p->next[index]=&node[nodeNum];
nodeNum++;
}
p=p->next[index];
}
p->count=1;
}
2.计算fail值
void build_ac(Tree &T){
queue <Tree> q;
TNode *p = T;
p->fail=NULL;
q.push(p);
while(q.empty()){
TNode *p = q.front();
q.pop();
int i;
for(i=0;i<MAX_CHILD_NUM;i++){
if(p->next[i]!=NULL){
//temp为跳转位置
TNode* temp =p->fail;
//跳转位置为NULL说明没有匹配,即为根节点
while(temp!=NULL){
/*如果n和m匹配*/
if(temp->next[i]!=NULL){
p->next[i]->fail=temp->next[i];
break;
}
temp=temp->fail;
}
if(temp==NULL) p->next[i]->fail=T;
q.push(p->next[i]);//push into the queue
}
}
}
}
AC自动机专题告一段落。之后如果遇到专门需要使用到ac自动机的时候在来重新深入了解吧。
——2017.2.28