首页 > 编程语言 >算法提高课

算法提高课

时间:2022-12-13 20:46:50浏览次数:38  
标签:ch int 提高 ne 节点 ++ 算法 我们

给定 nn 个长度不超过 5050 的由小写英文字母组成的单词,以及一篇长为 mm 的文章。

请问,其中有多少个单词在文章中出现了。

注意:每个单词不论在文章中出现多少次,仅累计 11 次。

输入格式

第一行包含整数 TT,表示共有 TT 组测试数据。

对于每组数据,第一行一个整数 nn,接下去 nn 行表示 nn 个单词,最后一行输入一个字符串,表示文章。

输出格式

对于每组数据,输出一个占一行的整数,表示有多少个单词在文章中出现。

数据范围

1≤n≤1041≤n≤104,
1≤m≤1061≤m≤106

输入样例:

1
5
she
he
say
shr
her
yasherhs

输出样例:

3
难度:中等
时/空限制:1s / 64MB
总通过数:7347
总尝试数:13048
来源:《信息学奥赛一本通》 , HDU2222 , kuangbin专题
算法标签

挑战模式

核心思路:首先我们可以联想KMP,但是我们会发现这里有多组数据匹配,所以使用kmp最后是一定会超时的。所以我们可以选择建树来进行优化,也就是建立一颗字典树,然后在这个上面使用kmp。然后我们回想一下kmp,我们会发现这个的主要思路还是求ne数组,也就是我们的回退数组。然后在这个树上怎么将我们的ne数组进行映射呢,其实我们可以发现其实就是将代码由一维的形式转变为二维的形式。其实最重要的一个点就是我们可以=通过这样的一个方式把我们的后缀字符串一网打尽,比如我们求she,就可以把he,e这列后缀字符串给找出来。为什么可以这样呢,因为我们的kmp就是的ne数组的定义就是最长的前后缀的下标

这里我们首先需要回顾一下\(tr[i][j]\)所对应的具体含义,它指的是以i号节点延申出字母j所对应的节点编号,其中我们的i号节点其实也就会对应着一个相应的字符串。其实也是可以看成从根节点出发,以编号i结尾的一个字符串。一定要注意我们这个字母是我们的边权

下面先看代码:

KMP做法:
for(int i=2;i<=m;i++)
{
int j=next[i-1];//这个代码的意思其实我们的j肯定是由上一个状态递推出来的,因为在代码的结尾有一个next[i]=j,然后因为是结尾,所以最后i需要++.所以这里就是减一

next[i]=j.
while(j&&p[i]!=p[j+1])
j=ne[j];
if(s[i]==p[j+1])
j++;
next[i]=j;
}
类比到tire树的做法:
//因为我们的自动机在tire中式先求前i-1层的信息,再求i层的信息。所以这和我么们的bfs是很相近的,所以我们可以采用bfs建树
while(hh<=tt)
{
t=q[hh++];取队列的头节点,其实这个对应的就是i-1层的idx所对应的编号,也就是next[i-1]中的i-1
for(int i=0;i<26;i++)
{
p=tr[t][i];这里的p是对应的字母t的下一个字母,又因为t对应的是i-1,所以p对应的就是i
j=next[t];这句话就是我们KMP中的第一句话,也就是j=next[i-1]
while(j&&!tr[j][i]) 其实这句话是我们的j这个节点延申一个字母i对应的结点不存在,也就是p[i]
p[j+1]
j=next[j];//回退
if(tr[j][i])
j=tr[j][i];//延申到对应的节点上面去
next[p]=j;//与上面的最后一句相对应
}
}

所以这整个过程其实就是相当于在树上完成了转移,也就是字符串的回退.这也就是董晓算法中的回退边的由来。那董晓算法中的转移边是怎么来的呢,于是我们看这个代码可不可以进一步进行优化。

我们可以看到这段代码:

while(j&&!tr[j][i])j=ne[j]

我们发现这个j只要没有匹配那么他就会一直往会跳,所以我们怎么可以使得他他少跳几步呢。这里就可以假设循环到i层的时候,前i-1层都求对了。那么ne[t]就是那个ne指针最终需要跳到的位置.

这里其实会发现优化之后的代码还是比较难理解,这里可以参照董晓算法。他那里引入了转移边和回退边的定义。回退边就是我们的ne数组,转移边的作用是让我们每次没必要退回根节点,而是可以通过转移边进行转移。就是可以帮助我们更快的找到找到那个字符串的最长后缀字符串所对应的前缀字符串,比如:her er.

左边是回跳边,符合四边形法则。右边是转移边,符合三角形法则.

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
#define IOS std::ios::sync_with_stdio(0),cin.tie(0),cout.tie(0)
const int INF = 0x3fff;
const int N = 500010;
int n;char str[1000010];
int ch[N][26], cnt[N], idx;
int ne[N];
void insert(char* s)
{
	int p = 0;
	for (int i = 0;s[i];i++)
	{
		int j = s[i] - 'a';
		if (!ch[p][j])
		{
			ch[p][j] = ++idx;
		}
		p = ch[p][j];
	}
	cnt[p]++;
}
void build()
{
	queue<int> q;
	for (int i = 0;i < 26;i++)
	{
		if (ch[0][i])//寻找从0出发边权为字母i的节点
		{
			q.push(ch[0][i]);
		}
	}
	while (q.size())
	{
		int u = q.front();
		q.pop();
		for (int i = 0;i < 26;i++)
		{
			int v = ch[u][i];
			if (v)
			{
				ne[v] = ch[ne[u]][i];//父节点帮助子节点建立回跳边
				q.push(v);
			}
			else
			{
				ch[u][i] = ch[ne[u]][i];//自建转移边.
			}
		}
	}
}
int query(char* s)
{
	int ans = 0;
	for (int k = 0, i = 0;s[k];k++)
	{
		i = ch[i][s[k] - 'a'];//注意我们的ch数组指向的是我们的节点编号
		for (int j = i;j && cnt[j] != -1;j = ne[j])//这里目的是把我们的查找到的单词的后缀单词串给一网打尽
		{
			ans += cnt[j];
			cnt[j] = -1;//相当于vis数组吧。表示把这个单词访问完了.
		}
	}
	return ans;
}
int main()
{
	IOS;
	int T;
	cin >> T;
	while (T--)
	{
		cin >> n;
		for (int i = 0;i < n;i++)
		{
			cin >> str;
			insert(str);
		}
		build();
		cin >> str;
		cout << query(str) << endl;
	}
}

标签:ch,int,提高,ne,节点,++,算法,我们
From: https://www.cnblogs.com/xyh-hnust666/p/16980552.html

相关文章