【递归学习大法好】字典树&&AC自动机

字典树

字典树其实很好理解啦,字面意思。
大概就是建一棵树,让它像字典一样!
它可以利用公共前缀,存很多很多很多字符串~

建树


首先,我们需要有一棵字典树!
怎么种一棵字典树呢?
通常情况下,我们使用……模板……来种!
先来看一下模板的各种变量的定义!
//使用了这里的模板,谢谢这位dalao啦!
maxn题目是不会给的,要根据它给的内存,可劲儿往大了开!
tree,就是我们阔耐的字典树啦!是不是长得非常亲民!

const int maxn =2e6+5;//如果是64MB可以开到2e6+5,尽量开大
int tree[maxn][30];//tree[i][j]表示节点i的第j个儿子的节点编号
bool flagg[maxn];//表示以该节点结尾是一个单词
int tot;//总节点数

现在就可以开始正式种树啦!
每次插进去一个字符串,树就会听话地长高高啦~

void insert_(char *str)
{
   int  len=strlen(str);
   int root=0;
   for(int i=0;i<len;i++)
   {
       int id=str[i]-'0';
       if(!tree[root][id]) tree[root][id]=++tot;
       root=tree[root][id];
   }
   flagg[root]=true;
}

查询


现在我们有了一棵字典树,该怎么查询呢?
当然是……继续使用模板,哈哈哈!
这个返回的应该是是否作为前缀出现过哦!

bool find_(char *str)//查询操作,按具体要求改动
{
    int len=strlen(str);
    int root=0;
    for(int i=0;i<len;i++)
    {
        int id=str[i]-'0';
        if(!tree[root][id]) return false;
        root=tree[root][id];
    }
    return true;
}

清空


模板来源的dalao特意让最后清空~

void init()//最后清空,节省时间
{
    for(int i=0;i<=tot;i++)
    {
       flagg[i]=false;
       for(int j=0;j<10;j++)
           tree[i][j]=0;
    }
   tot=0;//RE有可能是这里的问题
}

来做几道题吧~


也就是本蒟所谓的递归学习法~在应用中才能真正明白东西啦!
题目来源仍是模板来源的dalao,dalao真是太好了~

HDU1251


题目会先给一堆字符串,然后再给一堆字符串,看看第二堆中的每一个,可以作为第一堆的前缀多少次
据说是个模板题,但是仍和我们的模板不太一样哦!
首先是建树部分,因为我们原来就是个赤裸裸莫得树皮的字典树,现在需要求前缀次数了……自己稍微画画就可以发现,对于每一个节点,建树的时候路过它多少次,它就是多少单词的前缀
于是可以在建树的时候,把这个记录下来!

void insert_(char *str)
{
   int  len=strlen(str);
   int root=0;
   for(int i=0;i<len;i++)
   {
       int id=str[i]-'a';
       if(!tree[root][id]) tree[root][id]=++tot;
       sum[tree[root][id]] ++;
       root=tree[root][id];
   }
   flagg[root]=true;
}

现在就好办多啦,查询的时候并没有什么变化,先找到要找到节点,如果不存在,那它就是0个字符串的前缀,否则就返回它的sum就阔以啦!

HDU2072


题目大概是要数出现过多少单词,思路还是很简单哒,对于每一个读进来的单词,如果出现过就不再插入,否则就插入,并且计数器++!

首先输入的时候,引入一个很好用的东西!

#include<sstream>
stringstream ss(str);

就像输入流一样的东西,字符串流!
这样我们的输入(主程序)就变成了这样!

	ios::sync_with_stdio(false);
	while(getline(cin,str)){
		if(str=="#") break;
		int ans = 0;
		stringstream ss(str);
		while(ss >> s){
			if(!find_(s)){
				ans ++;
				insert_(s);
			}
		}
		cout<<ans<<endl;
		init();
	}

其次,会发现原始模板的find函数返回的是“是否作为前缀出现过”,怎么才能变成“是否作为一个单词出现过”呢?
return flagg[root];
嘿嘿嘿~

HDU5687


乱入一道题(澜佬推荐的)!乱入原因:它要删除!
嗷呜呜。。字典树居然还需要删除。。太过分了
无奈之下,更新了一下模板,哎。。。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
const int maxn = 1e6+5;//如果是64MB可以开到2e6+5,尽量开大
using namespace std;
int trie[maxn][26];//tree[i][j]表示节点i的第j个儿子的节点编号
int num[maxn]={-1};//节点作为前缀出现次数 
bool flagg[maxn];//表示以该节点结尾是一个单词
int root;
int tot;//总节点数 
int t,n;

void init(){
    memset(trie,0,sizeof(trie));
    memset(num,0,sizeof(num));
    tot=0;
}

void insert_(char *s){
    root = 0;
    int slen = strlen(s);
    for(int i=0; i<slen; i ++){
        int id = s[i]-'a';
        if(!trie[root][id])  trie[root][id] = ++tot;
        root = trie[root][id];
        num[root] ++;
    }
    flagg[root] = true;
}

int find_(char *s){ //以s为前缀的单词的个数
    root = 0;
    int slen = strlen(s);
    for(int i=0; i<slen; i++){
        int id = s[i]-'a';
        if(!trie[root][id])  return 0;
        root=trie[root][id];
    }
    return num[root];
}

void delete_(char *s)//删除所有前缀等于s的单词 
{
    root=0;
    int cnt=0;
    int slen=strlen(s);
    for(int i=0;i<slen;i++){
        int id=s[i]-'a';
        if(!trie[root][id]) return;
        root=trie[root][id];
    }
    cnt=num[root];
    flagg[root] = false; //这个是我强行加的,不对的话大佬们一定要指正啊QAQ
    root=0;
    for(int i=0;i<slen;i++){
        int id=s[i]-'a';
        root=trie[root][id];
        num[root]=num[root]-cnt;
    }
    for(int i=0;i<=25;i++) trie[root][i]=0;
}

int main()
{
    char s[10],a[50];
    scanf("%d",&t);
    init();
    while(t--){
        scanf("%s",s);
        scanf("%s",a);
        if(s[0]=='i') insert_(a);
        else if(s[0]=='s'){
            int flag = find_(a);
            if(flag==0) printf("No\n");
            else printf("Yes\n");
        }
        else{
            delete_(a);
        }
    }
    return 0;
}

小结


到这里,字典树就告一段落啦~
想刷更多题目的小盆友可以去模板来源的那位大佬那里哦!

AC自动机

模板参考这位大佬的博客
洛谷日报这一期讲得很好哦!
AC自动机:Aho-Corasick automaton,AC是两个人名Alfred V. Aho和Margaret J.Corasick!才不是自动AC机的意思嘞!!!?
那,AC自动机是干什么的呢?
放在这里说它,肯定是用来处理字符串的啦!

一个常见的例子就是给出n个单词,再给出一段包含m个字符的文章,让你找出有多少个单词在文章里出现过。
                                                                                                   ——度娘

具体的欢迎大家看上边两位大佬的文章,嘿嘿嘿~

喜闻乐见の大佬の模板


#include<cstdio>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int maxn=500010;

struct Trie
{
    int nxt[maxn][26],//字典树,
	fail[maxn],//相当于kmp中的nxt数组
	end[maxn]; //end对单词结尾做标记 
    int root,L;  //L相当于字典树中的sz ,root为根节点(即;0) 
    int newnode() //初相当于始化一个字典树上的节点 
    {
        for ( int i=0;i<26;i++ ) nxt[L][i]=-1;
        end[L++]=0;
        return L-1;
    }
    void init()
    {
        L=0;
        root=newnode();
    }
    void insert(char buf[]) //大致与字典树插入过程相同 
    {
        int len=strlen(buf);
        int now=root;
        for ( int i=0;i<len;i++ )
        {
            int x=buf[i]-'a';
            if ( nxt[now][x]==-1 ) nxt[now][x]=newnode();
            now=nxt[now][x];
        }
        end[now]++; //在单词结尾处做标记 
    }
    void build()  //相当于kmp的操作 
    {
        queue<int>que;
        fail[root]=root; //根节点初始化为0(即其本身)
        for (int i=0;i<26;i++ )
        {
            if ( nxt[root][i]==-1 ) nxt[root][i]=root;
            else //Trie中已经构建的节点 
            {
                int x=nxt[root][i];
                fail[x]=root;
                que.push(x);
            }
        }
        while ( !que.empty() )
        {
            int now=que.front();
            que.pop();
            for ( int i=0;i<26;i++ )
            {
                if ( nxt[now][i]==-1 ) //无后继点 
                    nxt[now][i]=nxt[fail[now]][i];//类似于kmp中求nxt数组一样 
                else //存在下一个节点 
                {
                    int x=nxt[now][i];
                    fail[x]=nxt[fail[now]][i];//失配指针指向他父节点的失配指针的下一个相同字符处 
                    que.push(x);
                }
            }
        }
    }
    int query(char buf[]) 
    {
        int len=strlen(buf);
        int now=root;
        int res=0;
        for ( int i=0;i<len;i++ ) 
        //沿着整个文本串移动,每移动到一个字符(节点) 时,
        {//通过失配指针不断找寻模式串 ,重点为源头,找到一个就将其标记清除 
            now=nxt[now][buf[i]-'a'];
            int tmp=now;
            while ( tmp!=root )
            {
                res+=end[tmp];
                end[tmp]=0;
                tmp=fail[tmp];
            }
        }
        return res; //返回单词个数 
    }
    void debug(){
        for ( int i=0;i<L;i++ ){
            printf("%id=%3d,fail=%3d,end=%3d,chi=[",i,fail[i],end[i]);
            for ( int j=0;j<26;j++ ) printf("%2d",nxt[i][j]);
            printf("]\n");
        }
    }
};
char buf[maxn*2];
Trie ac;
int main()
{
    int T,n;
    scanf("%d",&T);
    while ( T-- )
    {
        scanf("%d",&n);
        ac.init();
        for ( int i=0;i<n;i++ )
        {
            scanf("%s",buf);
            ac.insert(buf);
        }
        ac.build();
        scanf("%s",buf);
        printf("%d\n",ac.query(buf));
    }
    return 0;
}

大概懂了之后就可以做题啦


递归学习嘛~
嘿嘿嘿~
请自行跳转!//我觉得这个博主真的很棒!!!
把所有题目都写掉,不对的仔细对照dalao的程序,认真思考一下 ,AC自动机就是你的啦!~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值