字符串匹配(kmp+trie+aho-corasic automaton+fail tree)
kmp
对于一个字符串\(s_{0\dots n}\),称\(s_{0\dots i}(0 \leq i < n)\) 为它的前缀, 称\(s_{i\dots n}(0 < i \leq n)\)为它的后缀
例如字符串\(abcdef\)的前缀有\(a, ab, abc, abcde, abcdef\), 后缀有\(f, ef, def, cdef, bcdef\)
如果前缀后缀之中有相同的……在匹配中可以起到出其不意的效果。
例如对于模式串\(t=ababacb\), 文本串\(s=abababaababacb\)
设匹配进行到文本串的第\(i\)位,模式串的第\(j\)位,即模式串第\(j\)位之前已经匹配成功(开头是第1位开始算的话)
然后我们就发现程序很顺利进行到\(i = 5, j = 5\)并且匹配成功,这个时候\(i++, j++\), \(i = 6, j = 6\)
然后就发现\(t[6] = 'c' \neq s[6] = 'b'\)按照正常暴力思路就是回到\(i = 2, j = 1\)重新开始暴力
但是我们其实是可以发现已经成功匹配的模式子串\(t_{1\dots5} = "ababa"\)有相同的前缀后缀\(a, aba\),即\(t_1 = t_5, t_{1\dots3} = t_{3\dots5}\),且\(t_{1\dots4} \neq t_{2\dots5}\)(这样的话前缀后缀应该有四位才对)
因为成功匹配,所以会有\(s_{1\dots5} = t_{1\dots5}\), 所以我们就可以知道\(s_{1\dots3} = t_{1\dots3} = t_{3\dots5} = s_{3\dots5}\), \(s_{3\dots5} = t_{1\dots3} \neq s_{2\dots4}\)
这就是说在模式串的前三位我们又快速匹配成功了,根据上一次失败的匹配的结果。这里我们就可得到现在\(i = 6, j = 3 + 1 = 4\)开始匹配
这里\(j=3+1\)的3是根据已经匹配的模式子串的最长前缀后缀长度为3得到的
那么怎么快速求最长前缀后缀呢
对于字符串\(t_{1\dots n}\),注意这次从1开始(当然从0开始也没问题),我们要匹配,就要求出所有前缀 的最长前缀后缀长度。
也就是要求出所有的\(t_{1\dots i}(1\leq i\leq n)\)的最长前缀后缀的长度。
我们就设数组\(f[i]\)表示\(t_{1\dots i}\)的最长前缀后缀的长度,很容易得到\(f[0] = 0\)(明显犯规了嘛), \(f[1] = 0\)(就一个字母哪来前缀后缀)
接下来就可以递推了
对于已经求得的\(f[i-1] = j\),他表示\(t_{1\dots j} = t_{i - j\dots i - 1}\), 那么如果\(t_{j+1} = t_i\),就会有\(t_{1\dots j+1} = t_{i-j\dots i}\),这就是一个相同的前缀后缀且一定是最长的(因为\(f[i-1]\)表示的也是最长的),\(f[i] = j + 1\)
但是如果\(t_{j+1} \neq t_i\),是不是直接判\(f[i] = 0\)呢。
我们也可以考虑看一下\(t_{1\dots j}\)的最长相同前缀后缀。因为我们是一路推过来的,而且根据定义必定有前缀后缀的长度小于字符串的长度,即\(i > f[i]\), 那么我们就可以知道\(t_{1\dots j}\)的最长前缀后缀一定是已经求出来的,为\(f[j]\),他表示\(t_{1\dots f[j]} = t_{j - f[j] + 1\dots j}\),同时又因为\(t_{1\dots j} = t_{i - j\dots i - 1}\),所以我们可以得到\(t_{j - f[j] + 1\dots j} = t_{i - f[j]\dots i - 1} = t_{1\dots f[j]}\),
\(t_{1\dots f[j]} = t_{i-f[j]\dots i-1}\),也就是说长度为\(f[j]\)的子串\(t_{1\dots f[j]}\)也是模式子串\(t_{1\dots i - 1}\)的相同前缀后缀(只不过不是最长而已)
同理可得,\(j, f[j], f[f[j]], f[f[f[j]]]\dots\)均为\(t_{1\dots i-1}\)的相同前缀后缀的长度,我们就可以一一看看这样的长度的相同前缀后缀能不能多接一个。边界就是0的时候,空串是不会有前缀后缀的了……
写成代码就是这样
for (int i=2;i<=lb;i++)
{
int j = kmp[i-1];
while(j&&b[i]!=b[j+1]) j=kmp[j];
if(b[j+1]==b[i])kmp[i]=j+1;
else kmp[i]=0;
}
trie
类似于字典的构造,建立一棵树,每个节点表示一个字母,层深度表示串的长度,把有相同前缀的串放在同一个子树内。可以快速完成多个文本串中匹配一个模式串的任务。
比如这棵trie,打了红色标记就表示有一个串到尾了。所以这棵trie存储了文本串\(abc, abd, abcd, b, bcd, efg, hi?\)
对于有相同前缀\(ab\)的串\(abc, abd, abcd\),都放在了字数\(ab\)内,那么如果我们找的模式串前缀也是\(ab\),那么只需要在\(ab\)子树内寻找就可以了。
aho-corasic automaton
trie上kmp,一个文本串中匹配多个模式串
众所周知kmp是因为有回溯指针才能快速匹配,那么我们把多个模式串建立一个trie,然后用文本串一一匹配,如果找到标记了结束的红点就说明找到了一个模式串。
那么其实我们也可以建立一个回溯指针的。如果匹配失败就看看回溯指针那边能否匹配成功。
在AC自动机里面也叫做失配指针。
这里就是搞定之后的失配指针
对于建立了的字典树,第一层(也就是第一个字母)的失配指针全部指向根节点(就一个字母你指什么指了也是自己还是失配)并且扔入队列(对就用BFS)
然后每次就从队头取出元素,我们可以叫它\(j\), 如果\(j\)的\(nxt[i]\)存在,那么我们就可以去寻找这个\(nxt[i]\)的失配指针。这里先说如果不存在,那么可以加一个小优化,把\(nxt[i]\)指向\(j\)的失配指针的\(nxt[i]\), 这样就可以直接连到可能匹配到的模式串。但是这样会破坏字典树的结构,在某些题目用了会WA……
这里失配指针也模仿kmp的\(f\)的求法,如果\(j\)的失配指针有\(nxt[i]\),那么\(j\)的\(nxt[i]\)的失配指针就指向\(j \rightarrow fail \rightarrow nxt[i]\) 如果还是没有就看看\(j \rightarrow fail \rightarrow fail\)有没有……因为失配指针指向的不是根节点就是同一个字母,保证指向的节点所表示的串会等于当前节点的某个后缀。
匹配的时候,就用文本串从根节点开始匹配,匹配成功文本串指针后移,并且看看下一个字母能不能匹配。如果不能,那就跳去失败指针…………如果最后到了根节点还是没有这个字母,那么……没办法,文本串指针还是要后移一位。
每匹配到一位,我们就检查所有的失配指针指向的节点,\(j \rightarrow fail, j \rightarrow fail \rightarrow fail \dots\),如果有标志是某个模式串的末尾,那么就说明, 啊,我匹配到了!
但是与此同时也要把这个标志取消,以免以后重复访问到,重复计算。这样也可以标记访问过,下次不再访问,节省时间。
代码如下:(题目)
#include <cstdio>
#include <cstring>
#include <queue>
namespace Aho_Corasic_automaton
{
class A_C_maton
{
private:
public:
A_C_maton();
~A_C_maton();
int query(const char*);
void set_fail();
const A_C_maton* operator [](const int k) const
{ if(k < 0 or k >= 26) return nxt[26]; return nxt[k]; }
void add(const char*);
protected:
A_C_maton *fail, *nxt[27];
int ed;
};
A_C_maton::A_C_maton()
{
fail = NULL; ed = 0;
for(register int i = 0; i < 27; ++i) nxt[i] = NULL;
}
A_C_maton::~A_C_maton()
{
for(register int i = 0; i < 27; ++i) if(nxt[i]) delete nxt[i];
delete fail;
}
void A_C_maton::add(const char* str)
{
const register int len = std::strlen(str);
A_C_maton* p = this;
for(register int i = 0; i < len; ++i)
{
if(p -> nxt[str[i] - 'a'] == NULL) p -> nxt[str[i] - 'a'] = new A_C_maton;
p = p -> nxt[str[i] - 'a'];
}
++(p -> ed);
}
void A_C_maton::set_fail()
{
this -> fail = this;
std::queue <A_C_maton*> q;
for(register int i = 0; i < 27; ++i)
{
if(this -> nxt[i])
{
this -> nxt[i] -> fail = this;
q.push(this -> nxt[i]);
}else this -> nxt[i] = this;
}
while(not q.empty())
{
A_C_maton* p = q.front(); q.pop();
for(register int i = 0; i < 26; ++i)
{
if(p -> nxt[i])
{
q.push(p -> nxt[i]);
if(p -> fail -> nxt[i]) p -> nxt[i] -> fail = p -> fail -> nxt[i];
}else p -> nxt[i] = p -> fail -> nxt[i];
}
}
}
int A_C_maton::query(const char* str)
{
int ans = 0;
set_fail();
A_C_maton* p = this;
const register int len = std::strlen(str);
for(register int i = 0; i < len; ++i)
{
p = p -> nxt[str[i] - 'a'];
for(register A_C_maton* r = p; r and r -> ed != -1; r = r -> fail)
{
ans += r -> ed;
r -> ed = -1;
}
}
return ans;
}
}
using namespace Aho_Corasic_automaton;
A_C_maton* root = new A_C_maton;
int n;
char str[1000001];
int main()
{
scanf("%d", &n);
while(n--)
{
scanf("%s", str);
root -> add(str);
}
scanf("%s", str);
printf("%d\n", root -> query(str));
return 0;
}
fail tree
Aho-Corasic automaton求的是出现的模式串的个数
如果要求模式串出现的次数呢?
那么其实时间复杂度就有点高了。而且对于\(aaaaaaaaaaaaaaaaaaa\)这样的模式串,不断跳失配指针,是很慢的。
那么怎么办呢。
我们回忆一下Aho-Corasic automaton的工作过程。
首先在Trie树里跳文本串,每跳到一个节点就沿着失配指针遍历下去。如果遍历到某个模式串的末尾,那么就说明这个模式串在文本串中出现了一次,可以打上一个标记。那么正常的Aho-Corasic automaton 就是跳完文本串之后,统计每个模式串的结尾的标记个数,就是这个模式串在文本串中出现的次数。
但是这样即使我们提前存储下每个模式串结尾在Trie中的位置,还是会很慢,因为我们不仅要跳很多个失配指针,而且还可能多次跳过一个节点(因为模式串的出现次数肯定很多orz)
我们就可以考虑一下怎么优化。
我们可以发现,一个节点被打上标记,当且仅当它被文本串直接访问或者通过跳失配指针访问。
所以对于每个被标记的节点,我们都可以沿着网线失配指针爬回去,一定可以访问到某个被文本串直接访问过的节点。
那么如果我们根据失配指针反向建有向图,一个模式串的结尾能走到多少个标记,说明有多少次文本串可以直接访问、或通过失配指针访问到这个模式串。
也就是说,我们只要让文本串直接访问到的节点打上标记,跳完之后就统计每个模式串的结尾节点,通过失配指针反向建的有向边来询问节点,统计标记的个数,就是这个模式串出现的次数。这样每个失配指针只跳了一次,文本串也只跳了一次Trie,复杂度大大降低
但是这里要注意建fail的时候不能用优化。这个优化其实就是改变了Trie的结构,同样也会改变失配指针,使其不满足一棵树的结构,就不能正常计算。
这里就告诉你,为什么根据失配指针反向建图是树的结构。
每个节点只有一个失配指针,所以出度是1。反向建图,每个节点的入度是1,也就是只有一个父亲。
那不就是树吗!
而且由于我们通过失配指针最后访问到的都是根,所以反向建图出来的树的根节点就是原来Trie的根。
所以对于每个模式串的末尾,其实就是统计以这个末尾节点为根的子树里面有多少个标记。
然后我们再想想标记是怎么来的。每次文本串遍历到节点才加上一个标记。
那么我们可以假设所有节点的初值都是0,然后每次文本串访问到一个节点就加上1(单点修改),最后就是查找这个子树里面所有节点的和(区间查询)。
单点修改+区间查询 = 树状数组
但是这是一棵树,怎么换成一段数组呢?
我们可以用前缀表达式,对于\(u\)为根的子树, 大小为\(sz\), 用前缀表达式dfs出dfs序dfn,那么就可以保证\(dfn_u\)到\(dfn_{u + sz - 1}\)全部都是这棵子树的上节点的下标,并且这棵子树上节点全部都在这个序列里。
所以我们通过\(dfn\)来统计子树上标记的个数
看有注释的代码:(题目)
这道题,所谓的文章就是所有单词组合在一起,但是每个单词之间要插入一个分隔符。文本串跳到了分隔符就要跳回根节点重新跳。
#include<bits/stdc++.h>
using namespace std;
template <typename T>
struct Edge
{
Edge *nxt;
T* t;
}; /// 反向建图用链式前向星
struct Trie
{
Trie* nxt[27], *fail;
Edge <Trie> *edge;
int dfn, sz;
Trie* operator [] (int k) const
{
if(k < 0 or k > 26) return nxt[0];
return nxt[k];
}
Trie()
{
for(register int i = 0; i < 27; ++ i) nxt[i] = NULL;
fail = NULL; edge = NULL;
dfn = 0; sz = 1;
}
}*root;
Trie* add(Trie* p, char * str)
{
/// 把str加入到Trie中
for(register int i = 0; str[i]; ++i)
{
if(p -> nxt[str[i] - 'a' + 1] == NULL) p -> nxt[str[i] - 'a' + 1] = new Trie;
p = (*p)[str[i] - 'a' + 1];
}
return p;
}
int n;
char t[1000201];
Trie* ed[210];
void set_fail(Trie* p)
{
/// 建立失配指针
p -> fail = p;
std::queue <Trie*> q;
for(register int i = 0; i < 27; ++i)
{
if(p -> nxt[i])
{
p -> nxt[i] -> fail = p;
q.push(p -> nxt[i]);
}else p -> nxt[i] = p;
// 第一层字母如果就失配就要跳回根节点
}
while(not q.empty())
{
register Trie* j = q.front(); q.pop();
for(register int i = 0; i < 27; ++i)
{
if(j -> nxt[i])
{
q.push(j -> nxt[i]);
Trie* k = j -> fail;
while(k and k != p and k -> fail and k -> nxt[i] == NULL) k = k -> fail;
j -> nxt[i] -> fail = k -> nxt[i];
}/// else p -> nxt[i] = p -> fail -> nxt[i];
/// 这个优化不能要。如果没有这个节点就没有,优化了就破坏了结构,会影响下面建图
}
}
}
void addE(Trie* a, Trie* b)
{
/// 反向建图,加边
Edge<Trie> *e = new Edge<Trie>();
e -> nxt = a -> edge;
e -> t = b;
a -> edge = e;
return ;
}
void set_up(Trie *p)
{
/// 反向建图
queue <Trie*> q;
q.push(p);
/// 通过广搜来遍历每一个节点,通过失配指针反向建图
while(not q.empty())
{
Trie *j = q.front(); q.pop();
for(register int i = 1; i < 27; ++i)
{
if(j -> nxt[i] and j -> nxt[i] != p)
{
// 有这个子节点而且这个节点不是根节点
// 如果根节点进入到队列,那么就会永远在队列中……就MLE了
q.push(j -> nxt[i]);
addE(j -> nxt[i] -> fail, j -> nxt[i]);
}
}
}
}
int dfn;
void dfs(Trie *p)
{
/// 先序遍历求dfs序
p -> dfn = ++dfn;
for(register Edge<Trie>* i = p -> edge; i; i = i -> nxt)
{
dfs(i -> t);
// 对于每一条边都深搜下去
p -> sz += i -> t -> sz;
// 并且维护父节点的大小
}
}
int lowbit(int);
void change(int, int);
int query(int);
int sum[1000201];
// 树状数组求和
void solve(Trie* p, char *str, int len)
{
for(register int i = 0; i < len; ++i)
{
// 标记文本串直接跳到的节点
register const int x = str[i] - 'a' + 1;
while(not p -> nxt[x] and p -> fail != p) p = p -> fail;
p = p -> nxt[x];
change(p -> dfn, 1);
}
}
int len;
int main()
{
root = new Trie;
cin >> n;
for(register int i = 1; i <= n; ++ i)
{
cin >> (t + len);
ed[i] = add(root, t + len);
len += strlen(t + len);
t[len++] = 'a' - 1;
// 储存文章
}
set_fail(root);
set_up(root);
dfs(root);
solve(root, t, len);
for(register int i = 1; i <= n; ++i)
{
int ans = query(ed[i] -> dfn + ed[i] -> sz - 1) - query(ed[i] -> dfn - 1);
cout << ans << endl;
}
return 0;
}
int lowbit(int x)
{
return x & (-x);
}
void change(int n, int x)
{
for(int i = n; i <= dfn; i += lowbit(i)) sum[i] += x;
}
int query(int n)
{
int res = 0;
while(n)
{
res += sum[n];
n -= lowbit(n);
}
return res;
}
字符串匹配(kmp+trie+aho-corasic automaton+fail tree)的更多相关文章
- 字符串匹配KMP算法详解
1. 引言 以前看过很多次KMP算法,一直觉得很有用,但都没有搞明白,一方面是网上很少有比较详细的通俗易懂的讲解,另一方面也怪自己没有沉下心来研究.最近在leetcode上又遇见字符串匹配的题目,以此 ...
- 字符串匹配-KMP
节选自 https://www.cnblogs.com/zhangtianq/p/5839909.html 字符串匹配 KMP O(m+n) O原来的暴力算法 当不匹配的时候 尽管之前文本串和模式串已 ...
- 【Foreign】字符串匹配 [KMP]
字符串匹配 Time Limit: 10 Sec Memory Limit: 256 MB Description Input Output Sample Input 3 3 6 3 1 2 1 2 ...
- zstu.4194: 字符串匹配(kmp入门题&& 心得)
4194: 字符串匹配 Time Limit: 1 Sec Memory Limit: 128 MB Submit: 206 Solved: 78 Description 给你两个字符串A,B,请 ...
- 字符串匹配KMP算法
1. 字符串匹配的KMP算法 2. KMP算法详解 3. 从头到尾彻底理解KMP
- 字符串匹配--kmp算法原理整理
kmp算法原理:求出P0···Pi的最大相同前后缀长度k: 字符串匹配是计算机的基本任务之一.举例,字符串"BBC ABCDAB ABCDABCDABDE",里面是否包含另一个字符 ...
- 字符串匹配KMP算法的C语言实现
字符串匹配是计算机的基本任务之一. 举例来说,有一个字符串"BBC ABCDAB ABCDABCDABDE",我想知道,里面是否包含另一个字符串"ABCDABD" ...
- 字符串匹配KMP算法的讲解C++
转自http://blog.csdn.net/starstar1992/article/details/54913261 也可以参考http://blog.csdn.net/liu940204/art ...
- 字符串匹配KMP算法(转自阮一峰)
转自 http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html 字符串匹配是计算 ...
随机推荐
- 在 Docker 的 CentOS7 镜像 中安装 mysql
在 Docker 的 CentOS7 镜像 中安装 mysql 本来以为是个很简单的过程居然折腾了这么久,之前部署云服务器时也没有好好地记录,因此记录下. 特别提醒:本文的操作环境是在 Docker ...
- RMQ区间最值查询
RMQ区间最值查询 概述 RMQ(Range Minimum/Maximum Query),即区间最值查询,是指这样一个问题:对于长度为n的数列A, 回答若干询问RMQ(A,i,j)(i,j<= ...
- React框架的基本使用和了解
React: React详解: 安装react 脚手架工具: npm install -g create-react-app create-react-app 项目名称 cnpm react-dom ...
- 关于php的ini文件相关操作函数浅析
在小公司,特别是创业型公司,整个服务器的搭建一般也是我们 PHP 开发工程师的职责之一.其中,最主要的一项就是要配置好服务器的 php.ini 文件.一些参数会对服务器的性能产生深远的影响,而且也有些 ...
- PHP出现iconv(): Detected an illegal character in input string
PHP传给JS字符串用ecsape转换加到url里,又用PHP接收,再用网上找的unscape函数转换一下,这样得到的字符串是UTF-8的,但我需要的是GB2312,于是用iconv转换 开始是这样用 ...
- openFeign夺命连环9问,这谁受得了?
1.前言 前面介绍了Spring Cloud 中的灵魂摆渡者Nacos,和它的前辈们相比不仅仅功能强大,而且部署非常简单. 今天介绍一款服务调用的组件:OpenFeign,同样是一款超越先辈(Ribb ...
- vue 学习资料
自学资料地址: https://zhuanlan.zhihu.com/p/26535530项目UI部分1.pc站 UI:(1)考虑自己写成本高,需要花费不少时间,好处是可以自己控制维护!(2)引入第三 ...
- php laravel v5.1 消息队列
* install https://laravel.com/docs/5.1#installationcomposer create-project laravel/laravel msgq &quo ...
- fiddler抓包工具 https抓取 ios手机端抓取
fiddler抓包工具 https抓取 ios手机端抓取 转载链接:https://www.cnblogs.com/bais/p/9118297.html 抓取pc端https请求,ios手机端 ...
- 鸿蒙内核源码分析(物理内存篇) | 怎么管理物理内存 | 百篇博客分析OpenHarmony源码 | v17.01
百篇博客系列篇.本篇为: v17.xx 鸿蒙内核源码分析(物理内存篇) | 怎么管理物理内存 | 51.c.h .o 内存管理相关篇为: v11.xx 鸿蒙内核源码分析(内存分配篇) | 内存有哪些分 ...