Trie树
字典树,是一种存储字符串的方法,最核心的思想就是把前缀一样的字符串的前缀共用。然后把每一个串的结尾标记一下。
模板题
我们来看一下代码
#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
int n,m;
struct node{
int cnt;
int son[26];
bool have;
node(){
cnt=0;
memset(son,0,sizeof(son));
have=false;
}
}trie[800000];
int num=0;
void insert(char *s){
int v,len=strlen(s);
int u=0;
for(int i=0;i<len;i++){
v=s[i]-'a';
if(!trie[u].son[v])
trie[u].son[v]=++num;
u=trie[u].son[v];
}
trie[u].have=1;
}
int find(char *s){
int v,u=0,len=strlen(s);
for(int i=0;i<len;i++){
v=s[i]-'a';
if(!trie[u].son[v]){
return 3;
}
u=trie[u].son[v];
}
if(!trie[u].have) return 3;
if(!trie[u].cnt){
trie[u].cnt++;
return 1;
}
return 2;
}
//int n,m;
int main(){
char ch[100];
ios::sync_with_stdio(0);
cin>>n;
for(int i=1;i<=n;i++){
cin>>ch;
insert(ch);
}
cin>>m;
for(int i=1;i<=m;i++){
cin>>ch;
int k=find(ch);
if(k==3){
cout<<"WRONG"<<endl;
}
else if(k==1){
cout<<"OK"<<endl;
}
else if(k==2){
cout<<"REPEAT"<<endl;
}
}
}
其实它的核心思想在于,对于每一个串,如果当前的部分有公共的前缀,就直接跑trie上的前缀直到不相同了为止,然后开始添加新的。
KMP
update:2019.8.11
Kmp算法用于单模式串匹配。即在一个文本串中找一个模式串出现了几次和出现的位置。
我们可以暴力思考一下。复杂度是O(n*m)的。
我们优化在于每一个字符重新开始匹配的位置。在暴力中,我们只是向后移一位,而在kmp中,我们通过预处理,来优化这个过程。
我们定义prefix[i]为模式串(1-i)中最长公共前缀后缀。我们在每次失去匹配后就可以直接跳到prefix[i]的位置上。
我们在预处理的时候是模式串的自匹配。
我们每次记录的当前位匹配的最大公共前后缀len,如果当前位匹配成功,那么len++,prefix[i]=len;
如果不匹配len=prefix[len-1],但是还是要注意两个问题,在i=1,len=0的时候会陷入死循环。所以我们要特判一下。
我们习惯于将数组向后移一位,将第一位置为-1。
我们将文本串的长度设为m,模式串长度n。
我们匹配到n-1位时,我们记录一下,然后还要匹配后面的文本串,所以j=prefix[j].
#include<bits/stdc++.h>
using namespace std;
int prefix[1000010];
char text[1000010];
char pattern[1000010];
int n;
void prefix_table(){
prefix[0]=0;
int len=0;
int i=1;
while(i<n){
if(pattern[i]==pattern[len]){
len++;
prefix[i]=len;
i++;
}
else{
if(len>0){
len=prefix[len-1];
}
else{
prefix[i]=len;
i++;
}
}
}
}
void move_prefix(){
int i;
for(int i=n;i>0;i--){
prefix[i]=prefix[i-1];
}
prefix[0]=-1;
}
void kmp(){
int n=strlen(pattern);
int m=strlen(text);
int i=0;
int j=0;
while(i<m){
if(j==n-1 && text[i]==pattern[j]){
printf("%d\n",i-j+1);
j=prefix[j];
}
if(text[i]==pattern[j]){
i++;j++;
}
else{
j=prefix[j];
if(j==-1){
i++;j++;
}
}
}
}
int main(){
cin>>text;
cin>>pattern;
n=strlen(pattern);
prefix_table();
move_prefix();
kmp();
for(int i=1;i<=n;i++){
printf("%d ",prefix[i]);
}
}
AC自动机
-
概述
AC自动机就是把trie和kmp放在一起。也就是在trie上跑kmp,所以AC自动机又叫tr
ie图。它和kmp
的不同是,kmp是处理一个字串然后用处理的子串匹配主串,就是说你要匹配的串只有一个。但是有多个你就要用ac自动机了。 -
算法过程
你首先要把trie树建出来。然后根据题目不同的要去开不同的结构体。建图的过程就是从根节点开始,找到和这个串前缀相同最长的那个,然后把它的后缀插在它的后面。
然后你就要处理失败指针。(next数组)
这个处理的过程由于是在树上,所以有点类似bfs的感觉。开个队列,手写、STL区别不大。然后第一件事就是处理第一层。把所有点的fail指针指向根节点,然后把它们加入队列。其实就是bfs。核心代码就是下面两行吧。
while(!q.empty()){
int u=q.front();
q.pop();
for(int i=0;i<26;i++){
if(ac[u].vis[i]){
ac[ac[u].vis[i]].fail=ac[ac[u].fail].vis[i];
q.push(ac[u].vis[i]);
}
else
ac[u].vis[i]=ac[ac[u].fail].vis[i];
}
}
如果你有这个节点,那么它的失败指针就是它父节点失败指针的这个儿子。
否则它自己指向,它父节点失败指针的这个儿子。
查询的过程就比较简单了。
int ac_query(string s){
int l=s.length();
int now=0,ans=0;
for(int i=0;i<l;i++){
now=ac[now].vis[s[i]-'a'];
for(int j=now;j&&ac[j].end!=-1;j=ac[j].fail){
ans+=ac[j].end;
ac[j].end=-1;
}
}
return ans;
}
这样字符串部分的主流算法(提高范围内)就是有哈希了。
- 哈希
其实哈希就是一个加密的过程,就是把一个字符串通过一些方式把它变成一个数。下面来看 最简单最简单的哈希题。
这个题可以用最基础最基础的进制哈希。
你为了尽量避免哈希冲突,所以你这个基数就要是个质数(想一想为什么),而且从理论上说这个数的设置不一定跟它的大小有直接关系,所以你要好好思考这个东西取什么。
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
typedef unsigned long long ull;
ull base=131;
ull a[100010];
char s[100010];
int n,ans=1;
ull hash(char s[])
{
int len=strlen(s);
ull ans=0;
for (int i=0;i<len;i++)
ans=ans*base+(ull)s[i];
return ans&0x7fffffff;
}
int main()
{
scanf("%d",&n);
for (int i=1;i<=n;i++)
{
scanf("%s",s);
a[i]=hash(s);
}
sort(a+1,a+n+1);
for (int i=2;i<=n;i++)
if (a[i]!=a[i-1])
ans++;
printf("%d\n",ans);
}