字符串匹配(多模式匹配篇)「建议收藏」
字符串匹配(多模式匹配篇)
摘要:
问题的提出:众所周知,KMP算法在O(n)的时间中solve单模式串匹配问题。但怎样solve多模式串匹配问题呢? Solve:本文用简要记叙了使用trie树,trie图(AC自动机)solve该问题的方法。
关键字:
字符串,多模式串匹配,trie树,trie图,AC自动机。
前言:
KMP算法是一种极其优秀的单模式串匹配算法,它通过前缀函数fail来减少匹配次数,以达到O(n)的单串匹配。但当KMP算法用于解决多模式串匹配问题时,时间复杂度为O(nq),十分低效。 因此,我们去探索一些更适合于多模式串匹配问题的算法用以解决这个问题。 第1节主要介绍trie树。 第2节主要介绍trie图。 第三节给出一些例题。
1.trie树
1.0问题的引入:
给定一个原串s,n个模式串st[i],求st[i]是否出现在s中。
1.1字典树的定义:
又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。
——来源于百度百科
1.2字典树的性质:
根节点不包含字符,除根节点外每一个节点都只包含一个字符; 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串; 每个节点的所有子节点包含的字符都不相同。
每一个节点u都包含next[v],value。
next[v]表示节点u的v边指向的节点编号。
value表示危险节点所属的模式串编号,若value=0表示这不是一个危险节点。
根节点表示空串。
这样一棵trie树的深度为O(max(len)+1)
可以发现当且仅当s1是s2的前缀,那么在trie树上,s2的路径是包含s1的路径的。
1.2字典树的实现:
字典树的操作是十分简单的(建议读者根据性质自己推导实现过程)。
插入:
void build_trie_tree(char *st,int len,int p)
int x=1;
for (int i=0;i<len;i++)
if (trie[x].next[st[i]]==0) //若该边不存在,新建点和边。
nodenum++;
trie[x].next[st[i]]=nodenum;
x=trie[x].next[st[i]]; //跳转至下一个节点。
trie[x].value=p; //标记是否为危险节点。
}
(删除一个子树的操作不再叙述,基本思想是增加一个tag懒标记,表示此节点及其子树被删除。)
查询(查询某一字符串是否在trie树中):
bool query_trie_tree(char *st,int len)
int x=1;
for (int i=0;i<len;i++) x=trie[x].next[st[i]]; //跳转至目标节点
if (!trie[x].value) return 1;
}
查询2(多串查询):
我们只需要枚举起始位置i,寻找st[i->j](i<=j<=len)是否出现即可。
void query_trie_tree(char *st,int k,int len)
int x=1;
for (int i=k;i<len;i++)
x=trie[x].next[st[i]]; //跳转
if (x) return; //匹配失败退出
if (trie[x].value) fst[trie[x].value]=k; //fst[i]记录i模式串出现在原串中的起始位置
void work(char *st,int len)
for (int i=0;i<len;i++)
query_trie_tree(st,i,len); //枚举所有起点位置
}
1.4trie树的分析:(注:下文中|SIGMA|表示字符集大小,而sigma表示求和函数)
构造出trie树的结构,构造时间是O(sigma(len)),单串匹配的时间复杂度很明显是O(len)级别的。
多串匹配需要枚举原串的起始点u,再从trie树中查询,时间为O(lens*max(len))。
比起这个,更让我们关心的是空间复杂度,O(|SIGMA|n)。倘若空间不足,可以将next[v]用边表的形式记录下来,或者用左儿子,右兄弟的方法记录。
这样的数据结构无论从时间或是空间上都和KMP相差无几,但更加形象具体了。那么如何改变这个数据结构使它能够完成多串匹配任务呢?
注:将trie树从上到下,从左到右标号,根为1
我们发现在trie树上多串匹配,会产生许多浪费。
比如模式串为ab。
以上图中的trie树来匹配,跳转顺序是
1->2->5(ab)
1->3(b)
而匹配ab时已经将b匹配了一遍,但在做完ab之后却返回了根,重新匹配了b,得不偿失。
所以想要优化trie树,就要使每一次精确跳转到最有效的位置。
进入到trie图时代。
2.trie图
2.0trie图的引入——解决上述问题:
什么是精确跳转呢?
在这个图中,abc的精确跳转应该是bc。 abd的精确跳转为根(空串) 字符串s的精确跳转节点是trie树中存在的s最长的后缀,称为后缀fail。
2.1trie图的概念:
在trie树上添加前缀指针fail并补齐trie树的边,所构造的图。
2.2trie图的性质:
trie图的目的是让每一次都精确地跳转。
如该图中的的fail应该指向bc。
节点u的fail应该为u的父亲节点的fail点的该边。(fail[u]=trie[father[fail[u]]].next[ch])
读者可以计算一下当trie图中只有单串的时候,fail和KMP的next两个数组有什么特殊的联系。
能够很轻易地看出:若trie图按0开始编号,next[i]=fail[i]。
2.3trie图的实现:(建议读者自行思考后对照)
构造trie图:
void build_trie_graph()
trie[1].fail=1; //根的后缀为根
for (int i=1;i<=p;i++)
if (trie[1].next[i])
trie[trie[1].next[i]].fail=1;
que.push(trie[1].next[i]);
} //根的儿子的后缀为根
else trie[1].next[i]=1; //根的新边为根
while (!que.empty())
int q=que.front();
que.pop();
for (int i=1;i<=p;i++)
int v=trie[q].next[i];
if (v)
trie[v].fail=trie[trie[q].fail].next[i];
que.push(v);
else trie[q].next[i]=trie[trie[q].fail].next[i];
if (trie[trie[q].fail].value) trie[q].value=trie[trie[q].fail].value;
}
遍历:
void query_trie_tree(char *st,int len)
int x=1;
for (int i=0;i<len;i++)
if (!trie[x].value) solve_probleam();
x=trie[x].next[st[i]]; //跳转至目标节点
}
3.来几道例题练练手!
3.1 Video Game
Bessie is playing a video game! In the game, the three letters ‘A’, ‘B’,
and ‘C’ are the only valid buttons. Bessie may press the buttons in any order she likes; however, there are only N distinct combos possible (1 <= N <= 20). Combo i is represented as a string S_i which has a length between 1 and 15 and contains only the letters ‘A’, ‘B’, and ‘C’.
Whenever Bessie presses a combination of letters that matches with a combo, she gets one point for the combo. Combos may overlap with each other or even finish at the same time! For example if N = 3 and the three possible combos are “ABA”, “CB”, and “ABACB”, and Bessie presses “ABACB”, she will end with 3 points. Bessie may score points for a single combo more than once.
Bessie of course wants to earn points as quickly as possible. If she presses exactly K buttons (1 <= K <= 1,000), what is the maximum number of points she can earn?
给你个模式串(每个长度≤15,1≤N≤20),串中只含有“ABC”三种字母。求一长度为K(1≤K≤1000)的字符串,使得匹配数最大(重复匹配计多次),输出最大值。
题解:先构造trie图,然后动态规划。
f[step][u]表示第step步走到u点经过的最多危险节点数量。
f[step+1][trie[u].next[k]]=max{f[step+1][trie[u].next[k]],f[step[u]+trie[trie[u].next[k]].value }
#include<bits/stdc++.h>
using namespace std;
const int MAXANS=10000000;
struct node
int ch[5],fail,value;
} trie[505];
int nodenum=1,n,m;
int f[1005][505];
char st[25];
queue<int> que;
int smax(int x,int y){return x>y?x:y;}
void build_trie_tree(char *st,int len)
int u=1;
for (int i=0;i<len;i++)
if (!trie[u].ch[st[i]-64])
nodenum++;
trie[u].ch[st[i]-64]=nodenum;
u=trie[u].ch[st[i]-64];
trie[u].value++;
void build_trie_graph()
trie[1].fail=1;
for (int i=1;i<=3;i++)
if (trie[1].ch[i])
trie[trie[1].ch[i]].fail=1;
que.push(trie[1].ch[i]);
else trie[1].ch[i]=1;
while (!que.empty())
int u=que.front();
que.pop();
for (int i=1;i<=3;i++)
if (trie[u].ch[i])
trie[trie[u].ch[i]].fail=trie[trie[u].fail].ch[i];
que.push(trie[u].ch[i]);
else trie[u].ch[i]=trie[trie[u].fail].ch[i];
if (trie[trie[u].fail].value)
trie[u].value+=trie[trie[u].fail].value;
void solve()
for (int step=0;step<=m;step++)
for (int i=1;i<=nodenum;i++)
f[step][i]=-MAXANS;
f[0][1]=0;
for (int step=0;step<m;step++)
for (int i=1;i<=nodenum;i++)
for (int j=1;j<=3;j++)
int v=trie[i].ch[j];
f[step+1][v]=smax(f[step+1][v],f[step][i]+trie[v].value);
int ans=0;
for (int i=2;i<=nodenum;i++) ans=smax(ans,f[m][i]);
printf("%d\n",ans);
int main()
scanf("%d%d",&n,&m);
for (int i=1;i<=n;i++)
scanf("%s",st);
build_trie_tree(st,strlen(st));
build_trie_graph();
solve();
return 0;
}
OJ上实测速度很快。只用了20MS(rank1
膨胀
逃)。
3.2阿狸的打字机
BZOJ2434 阿狸的打字机 阿狸喜欢收藏各种稀奇古怪的东西,最近他淘到一台老式的打字机。打字机上只有28个按键,分别印有26个小写英文字母和’B’、’P’两个字母。 经阿狸研究发现,这个打字机是这样工作的: ·输入小写字母,打字机的一个凹槽中会加入这个字母(这个字母加在凹槽的最后)。 ·按一下印有’B’的按键,打字机凹槽中最后一个字母会消失。 ·按一下印有’P’的按键,打字机会在纸上打印出凹槽中现有的所有字母并换行,但凹槽中的字母不会消失。 例如,阿狸输入aPaPBbP,纸上被打印的字符如下: 我们把纸上打印出来的字符串从1开始顺序编号,一直到n。打字机有一个非常有趣的功能,在打字机中暗藏一个带数字的小键盘,在小键盘上输入两个数(x,y)(其中1≤x,y≤n),打字机会显示第x个打印的字符串在第y个打印的字符串中出现了多少次。 阿狸发现了这个功能以后很兴奋,他想写个程序完成同样的功能,你能帮助他么? Input 输入的第一行包含一个字符串,按阿狸的输入顺序给出所有阿狸输入的字符。 第二行包含一个整数m,表示询问个数。 接下来m行描述所有由小键盘输入的询问。其中第i行包含两个整数x, y,表示第i个询问为(x, y)。 Output 输出m行,其中第i行包含一个整数,表示第i个询问的答案。 Sample Input aPaPBbP Sample Output