KMP,字典树与AC自动机

KMP中的一个重要思想与AC自动机非常相似,就是失配指针。在kmp算法中指匹配失败后该返回到字符串的哪个位置,也就是俗称的特定位置的复活出生点。

KMP模板如下:

//#include<pch.h>
#include <iostream>
#include <cstdio>
#include <bits/stdc++.h>
#include<queue>
#include <map>
#include <algorithm>
#include <stack>
#include <iomanip>
#include <cstring>
#include <cmath>
#define DETERMINATION main
#define lldin(a) scanf_s("%lld", &a)
#define println(a) printf("%lld\n", a)
#define reset(a, b) memset(a, b, sizeof(a))
#define debug cout<<"procedures above are available"<<endl;
using namespace std;
const int INF = 0x3f3f3f3f;
const double PI = acos(-1);
typedef long long ll;
typedef unsigned long long ull;
typedef long double ld;
const int mod = 1000000007;
const int tool_const = 19991126;
const int tool_const2 = 33;
inline ll nextlong()
{
    ll tmp = 0, si = 1;
    char c;
    c = getchar();
    while (c > '9' || c < '0')
    {
        if (c == '-')
            si = -1;
        c = getchar();
    }
    while (c >= '0' && c <= '9')
    {
        tmp = tmp * 10 + c - '0';
        c = getchar();
    }
    return si * tmp;
}
/**Maintain your determination.Nobody knows the magnificent landscape
at his destination before the arrival with stumble.**/
/**Last Remote**/
string text,sub;
ll nexxt[500000];
void getnext()
{
    ll pt = 0, pt2 = -1;
    nexxt[0] = -1;//初始出生点为边缘外
    while (pt < sub.size())
    {
        if (pt2 == -1 || sub[pt2] == sub[pt])
        {
            nexxt[++pt] = ++pt2;//如果某个字母在字符串中重复出现,那么确定它的复活出生点
        }
        else
            pt2 = nexxt[pt2];//否则判定死亡,返回上一个出生点
    }
}
ll cnt = 0;
void kmp()
{
    getnext();
    ll pt1 = 0, pt2 = 0;
    while (pt1 < text.size())
    {
        //cout << pt1 << " " << pt2 << endl;
        if (pt2 == -1 || sub[pt2] == text[pt1])
            pt2++, pt1++;
        else
            pt2 = nexxt[pt2];
        if (pt2 == sub.size())
        {
            cnt++;
            pt2 = 0;
        }
    }
}
int DETERMINATION()
{
    ios::sync_with_stdio(false);
    cin.tie(0), cout.tie(0);
    while (cin >> text)
    {
        if (text == "#")
            break;
        cin >> sub;
        reset(nexxt, 0);
        cnt = 0;
        kmp();
        cout << cnt << endl;
    }
    return 0;
}

AC自动机是用于求解已知模式串(已知录入信息)在待测文本的出现情况的算法。比如说有一些单词,再给定一篇文章,问在这篇文章里出现了多少已知单词。

在本算法中,较一般算法起到主要优化作用的是fail指针(失配指针),这个指针的作用是在失配之后找到另一个合适的地方试图进行下一轮匹配(类似于复活点),一个字母的失配指针一般选择的是父节点对应的失配指针所对应的字母。(回到最近公共祖先掌控下的另一条数据链)。

AC自动机是基于字典树和KMP算法中的失配指针(存档点)构建的。具体操作如下:

  1. 首先将所有模式串全部存入字典树中,并在字典树中附加数组记录你所需要的信息(比如说在字典树链的末尾记录该单词出现的次数)。
  2. 初始化fail指针。首先把字典树根节点下第一层全部扔到队列中,并初始化这些点的fail指针为根节点(除了回城没有别的复活点了),并对每个节点的下一层进行fail指针的赋值。一般某个节点如果存在子结点,就把它的fail指针赋值为父节点的fail指针对应的字母所在的节点编号。如果这个字母不存在的话就直接对该结点赋值为父节点掌控下这个字母对应点的编号。

以上是建立自动机,如果要查询的话需要进行如下操作:

  • 对文本而言,从根节点开始对应查询文本中每一个字母,如果某个字母组合在字典树中不存在的话,因为上边已经初始化好的缘故,当前节点会跳到另一个地方试图继续匹配。对于每一次节点分析,都会有一个过程试图更新答案,并依据fail指针来回跳动,直到fail指针跳回根节点为止。如果当前字母组合确实代表一个存在的模式串,那么答案更新,将此处的答案值赋值为-1,表示已经计算过了。并开始分析下一个字母组合。

 

//#include<pch.h>
#include <iostream>
#include <cstdio>
#include <bits/stdc++.h>
#include<queue>
#include <map>
#include <algorithm>
#include <stack>
#include <iomanip>
#include <cstring>
#include <cmath>
#define DETERMINATION main
#define lldin(a) scanf_s("%lld", &a)
#define println(a) printf("%lld\n", a)
#define reset(a, b) memset(a, b, sizeof(a))
#define debug cout<<"procedures above are available"<<endl;
using namespace std;
const int INF = 0x3f3f3f3f;
const double PI = acos(-1);
typedef long long ll;
typedef unsigned long long ull;
typedef long double ld;
const int mod = 1000000007;
const int tool_const = 19991126;
const int tool_const2 = 33;
inline ll nextlong()
{
	ll tmp = 0, si = 1;
	char c;
	c = getchar();
	while (c > '9' || c < '0')
	{
		if (c == '-')
			si = -1;
		c = getchar();
	}
	while (c >= '0' && c <= '9')
	{
		tmp = tmp * 10 + c - '0';
		c = getchar();
	}
	return si * tmp;
}
/**Maintain your determination.Nobody knows the magnificent landscape
at his destination before the arrival with stumble.**/
/**Last Remote**/
int tries[1500010][28],cnt=0,sum[1500010];
int fail[1500010];
string modules[500000];
string text;
void construct(string t)//把模式串放入字典树中
{
	ll rt = 0;
	for (int i = 0; t[i]; i++)
	{
		ll nxt = t[i] - 'a' + 1;
		if (tries[rt][nxt] == 0)
			tries[rt][nxt] = ++cnt;
		rt = tries[rt][nxt];
	}
	sum[rt]++;//如果要记录单词数量的话,就这么做
}
void get_fail()
{
	queue<ll>q;
	for (int i = 1; i <= 26; i++)
	{
		if (tries[0][i])
		{
			fail[tries[0][i]] = 0;//根节点下第一层的节点的fail指针都要指向根节点
			q.push(tries[0][i]);
		}
	}
	while (!q.empty())
	{
		ll current = q.front();
		q.pop();
		for (int i = 1; i <= 26; i++)
		{
			if (tries[current][i])
			{
				fail[tries[current][i]] = tries[fail[current]][i];
				q.push(tries[current][i]);//确实存在某个字母节点,就把该节点的fail指针赋值为父节点fail指针所对应的该字母节点编号。
			}
			else
				tries[current][i] = tries[fail[current]][i];
        //如果不存在,那就试图去找父节点下是否有这样一个字母。
		}
	}
}
ll query(string quest)
{
	ll current = 0, ans = 0;
	for (int i = 0; i < quest.size(); i++)
	{
		ll t = quest[i] - 'a' + 1;
		current = tries[current][t];//迭代
		for (int j = current; j != 0 && sum[j] != -1(当前位置不是根节点且该节点没有被找过); j = fail[j])
		{
			ans += sum[j];
			sum[j] = -1;//已经找过了
		}
	}
	return ans;
}
int DETERMINATION()
{
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	ll n;
	cin >> n;
	for (int i = 1; i <= n; i++)
	{
		cin >> modules[i];
		construct(modules[i]);
	}
	get_fail();
	cin >> text;
	cout << query(text) << endl;
	return 0;
}

 

posted @ 2019-08-09 23:17  完全墨染的樱花  阅读(233)  评论(0)    收藏  举报