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)的更多相关文章

  1. 字符串匹配KMP算法详解

    1. 引言 以前看过很多次KMP算法,一直觉得很有用,但都没有搞明白,一方面是网上很少有比较详细的通俗易懂的讲解,另一方面也怪自己没有沉下心来研究.最近在leetcode上又遇见字符串匹配的题目,以此 ...

  2. 字符串匹配-KMP

    节选自 https://www.cnblogs.com/zhangtianq/p/5839909.html 字符串匹配 KMP O(m+n) O原来的暴力算法 当不匹配的时候 尽管之前文本串和模式串已 ...

  3. 【Foreign】字符串匹配 [KMP]

    字符串匹配 Time Limit: 10 Sec  Memory Limit: 256 MB Description Input Output Sample Input 3 3 6 3 1 2 1 2 ...

  4. zstu.4194: 字符串匹配(kmp入门题&& 心得)

    4194: 字符串匹配 Time Limit: 1 Sec  Memory Limit: 128 MB Submit: 206  Solved: 78 Description 给你两个字符串A,B,请 ...

  5. 字符串匹配KMP算法

    1. 字符串匹配的KMP算法 2. KMP算法详解 3. 从头到尾彻底理解KMP

  6. 字符串匹配--kmp算法原理整理

    kmp算法原理:求出P0···Pi的最大相同前后缀长度k: 字符串匹配是计算机的基本任务之一.举例,字符串"BBC ABCDAB ABCDABCDABDE",里面是否包含另一个字符 ...

  7. 字符串匹配KMP算法的C语言实现

    字符串匹配是计算机的基本任务之一. 举例来说,有一个字符串"BBC ABCDAB ABCDABCDABDE",我想知道,里面是否包含另一个字符串"ABCDABD" ...

  8. 字符串匹配KMP算法的讲解C++

    转自http://blog.csdn.net/starstar1992/article/details/54913261 也可以参考http://blog.csdn.net/liu940204/art ...

  9. 字符串匹配KMP算法(转自阮一峰)

    转自 http://www.ruanyifeng.com/blog/2013/05/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm.html 字符串匹配是计算 ...

随机推荐

  1. 在 Docker 的 CentOS7 镜像 中安装 mysql

    在 Docker 的 CentOS7 镜像 中安装 mysql 本来以为是个很简单的过程居然折腾了这么久,之前部署云服务器时也没有好好地记录,因此记录下. 特别提醒:本文的操作环境是在 Docker ...

  2. RMQ区间最值查询

    RMQ区间最值查询 概述 RMQ(Range Minimum/Maximum Query),即区间最值查询,是指这样一个问题:对于长度为n的数列A, 回答若干询问RMQ(A,i,j)(i,j<= ...

  3. React框架的基本使用和了解

    React: React详解: 安装react 脚手架工具: npm install -g create-react-app create-react-app 项目名称 cnpm react-dom ...

  4. 关于php的ini文件相关操作函数浅析

    在小公司,特别是创业型公司,整个服务器的搭建一般也是我们 PHP 开发工程师的职责之一.其中,最主要的一项就是要配置好服务器的 php.ini 文件.一些参数会对服务器的性能产生深远的影响,而且也有些 ...

  5. PHP出现iconv(): Detected an illegal character in input string

    PHP传给JS字符串用ecsape转换加到url里,又用PHP接收,再用网上找的unscape函数转换一下,这样得到的字符串是UTF-8的,但我需要的是GB2312,于是用iconv转换 开始是这样用 ...

  6. openFeign夺命连环9问,这谁受得了?

    1.前言 前面介绍了Spring Cloud 中的灵魂摆渡者Nacos,和它的前辈们相比不仅仅功能强大,而且部署非常简单. 今天介绍一款服务调用的组件:OpenFeign,同样是一款超越先辈(Ribb ...

  7. vue 学习资料

    自学资料地址: https://zhuanlan.zhihu.com/p/26535530项目UI部分1.pc站 UI:(1)考虑自己写成本高,需要花费不少时间,好处是可以自己控制维护!(2)引入第三 ...

  8. php laravel v5.1 消息队列

    * install https://laravel.com/docs/5.1#installationcomposer create-project laravel/laravel msgq &quo ...

  9. fiddler抓包工具 https抓取 ios手机端抓取

    fiddler抓包工具 https抓取 ios手机端抓取  转载链接:https://www.cnblogs.com/bais/p/9118297.html   抓取pc端https请求,ios手机端 ...

  10. 鸿蒙内核源码分析(物理内存篇) | 怎么管理物理内存 | 百篇博客分析OpenHarmony源码 | v17.01

    百篇博客系列篇.本篇为: v17.xx 鸿蒙内核源码分析(物理内存篇) | 怎么管理物理内存 | 51.c.h .o 内存管理相关篇为: v11.xx 鸿蒙内核源码分析(内存分配篇) | 内存有哪些分 ...