今天主要学习了KMP算法,trie树和AC自动机
1.KMP算法
KMP算法是用来解决字符串匹配问题,时间复杂度O(n+m) (其中n,m分别为长、短字符串的长度)
直接上模板,后面有注释
1)求fail数组
void getFail(int n) { //n表示短字符串的长度
int i = 0, j = -1; //i表示短字符串当前位置,j表示当前位置之前的部分前缀和后缀相同的个数-1
fail[0] = -1; //fail数组表示当前位置匹配失败后从短字符串之前哪个位置继续匹配
while(i < n) {
if(j==-1 || temp[i]==temp[j]) //temp数组用来存储短字符串
{
++i, ++j;
if(temp[i] != temp[j]) fail[i] = j;
else fail[i] = fail[j];
}
else j = fail[j];
}
}
2)KMP
int kmp(int n, int m) { //n表示长字符串的长度,m表示短字符串的长度
int res = 0; //res表示长字符串中有多少个短字符串
int i = 0, j = 0; //i表示长字符串当前位置,j表示短字符串当前位置
while(i < n) {
if(j==-1 || str[i]==temp[j]) { //temp存储短字符串,str存储长字符串
++i, ++j;
if(j == m) { //当发现长字符串中有一个短字符串时
++res;
j = 0;
}
}
else j = fail[j];
}
return res;
}
如果考虑字符串可以覆盖,eg. temp=aza, str=azazaza 考虑覆盖,str中有三个temp,这是只需要修改while循环中的一小步就行
int kmp(int n, int m) { //n表示长字符串的长度,m表示短字符串的长度
int res = 0; //res表示长字符串中有多少个短字符串
int i = 0, j = 0; //i表示长字符串当前位置,j表示短字符串当前位置
while(i < n) {
if(j==-1 || str[i]==temp[j]) { //temp存储短字符串,str存储长字符串
++i, ++j;
}
else j = fail[j];
if(j == m) { //当发现长字符串中有一个短字符串时
++res;
}
}
return res;
}
2.trie树(也叫字典树)
引例:
已知n扇门,每扇门对应一个密码(用一个字符串表示,密码已知)
例如:0对应say,1对应she,2对应shr,3对应her
每次询问时,输入一个密码,求它对应几号门(0,1, …, n-1)
如果把输入的密码和每扇门的密码一一比较,时间复杂度约为O(n*k),k为密码的长度
如果询问q次,时间复杂度为O(q*n*k) 多次询问可能会超时
分析:
构建一棵树如下图 在树的叶子节点存储对应的几号门 可以将时间复杂度降为O(q*k)
建树时间复杂度:O(k*m) 查询时间复杂度:O(m)
其中k为需要插入字符串的个数(插入一个为O(m)),m为字符串的长度
1)Trie树----需要的数据结构
const int INDEX_N = 26; //一个字符串中最多有多少种字符
struct Node { //树的节点
int num; //num表示是几号门
Node *next[INDEX_N]; //next数组指向子节点
Node(): num(-1) { //构造函数
for(int i=0; i<INDEX_N; i++)
next[i] = NULL;
}
};
Node *root = new Node; //Trie树的根节点
2)Trie树----插入字符串
void insert(string str, int num) { //str表示要插入的密码,num表示对应的几号门
Node *ptr = root;
for(unsigned int i=0; i<str.size(); i++) {
int pos = str[i]-'A';
if(!ptr->next[pos]) ptr->next[pos] = new Node;
ptr = ptr->next[pos];
}
ptr->num = num;
}
3)查找
void search(string str) {
Node *ptr = root;
for(unsigned int i=0; i<str.size(); i++) {
int pos = str[i]-'A';
if(!ptr->next[pos]) return;
ptr = ptr->next[pos];
}
cout << ptr->num << endl;
}
3.AC自动机
思路:
Trie树结合KMP,就成了AC自动机 也就是加上fail指针(如下图)
但是AC自动机的fail指针算法有所不同
重要性质:任何一个点的fail指针,一定是root 或着它父亲fail指针形成的链上的某个点的儿子 且这个儿子对应的字符与该节点字符相同
1)需要用到的数据结构
struct Node {
int index; //index与num类似,某些节点需要保存的信息
Node *fail; //当前节点匹配失败后指向下一个匹配节点
Node *next[INDEX_N];
Node(): index(0), fail(NULL) { //构造函数
for(int i=0; i<INDEX_N; i++)
next[i] = NULL;
}
};
Node *root = new Node; //根节点
2)插入短字符串
void Insert(string str, int index) { //str表示插入的短字符串
Node *ptr = root;
for(unsigned int i=0; i<str.size(); i++) {
int pos = str[i]-'A';
if(!ptr->next[pos]) ptr->next[pos] = new Node;
ptr = ptr->next[pos];
}
ptr->index = index;
}
3)fail指针
void GetFail() {
queue<Node*> que;
que.push(root);
while(!que.empty()) {
Node *ptr = que.front();
que.pop();
for(int i=0; i<INDEX_N; i++)
if(ptr->next[i]) {
Node *temp = ptr->fail;
while(temp && !temp->next[i])
temp = temp->fail;
if(temp) ptr->next[i]->fail = temp->next[i];
else ptr->next[i]->fail = root;
que.push(ptr->next[i]);
}
}
}
4)AC自动机
void Solve(string str)
{
Node *ptr = root;
for(unsigned int i=0; i<str.size(); i++)
{
if(str[i]<'A' || str[i]>'Z')
{
ptr = root;
continue;
}
int pos = str[i]-'A';
while(ptr!=root && !ptr->next[pos]) ptr = ptr->fail;
ptr = ptr->next[pos];
if(!ptr)
{
ptr = root;
continue;
}
for(Node *temp=ptr; temp!=root; temp=temp->fail)
if(temp->index)
num[temp->index]++;
}
}
构造时间复杂度:与Trie树类似 查询时间复杂度:O(n + k*m)
其中n、m分别为长字符串和短字符串的长度,k为短字符串的个数
4.KMP----fail数组另一个作用
n-fail[n]表示短字符串最小循环节的长度
其中n表示短字符串的长度