字典树

  又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希表高。

可见根节点不包含字符,除根节点外每一个节点都只包含一个字符; 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串; 每个节点的所有子节点包含的字符都不相同。

用途:

1.就是用来查找单词的。

比如有一堆单词,给定一个单词要求判断是否出现过。

显然可以使用map或set什么的,但是对于多组**数据,是会T的,这就需要使用字典树了。

2.查询某前缀是否出现过,或者出现了几次。

显然还可以用map(STL大法好),不过还是可以用字典树跑得快些。

实现:

从上图中我们可以发现:

我们令根节点为空,其余节点为字符,我们顺着某一条路径走下去就能找到一个单词。

这就牵扯到了两个操作:插入,查找。

插入:

从图中可以直观看出,从左到右扫这个单词,如果字母在相应根节点下没有出现过,就插入这个字母;否则沿着字典树往下走,看单词的下一个字母。

具体操作: 我们给每个节点一个编号,显然一个节点最多有26个子节点(以下无特殊说明均认为只包含小写字母) 我们开一个数组,trie[i][j]表示编号为i的节点的j子节点的编号是什么(1<=j<=26),我们给每个点开26个子节点,不管是否用上(反正用不上就是编号为0)。

查找:

从左往右以此扫描每个字母,顺着字典树往下找,能找到这个字母,往下走,否则结束查找,即没有这个前缀;

前缀扫完了,表示有这个前缀。

具体: 其实有了我们的trie数组,一切都好解决了

代码实现(查前缀是否出现过):

void _insert(string s)
{
    int now=0;
    int size=s.size();
    for(int i=0;i<size;i++)
    {
        int x=s[i]-'a';
        if(!trie[now][x])
        trie[now][x]=++total;
        now=trie[now][x];
    }
}
bool  find(string s)
{
    int now=0;
    int size=s.size();
    for(int i=0;i<size;i++)
    {
        int x=s[i]-'a';
        if(!trie[now][x])
        return 0;
        now=trie[now][x];
    }
    return 1;
}

那么我们怎么查单词呢?

我们记录_word[i]表示以i为结尾是不是单词,显然在插入单词时将结尾元素的_word变成1就好了。同时,在find时将return 1;改为return word[now];

例题:

统计难题:

描述:cym最近遇到一个难题,Z交给他很多单词(只有小写字母组成,不会有重复的单词出现),现在Z要他统计出以某个字符串为前缀的单词数量(单词本身也是自己的前缀).

输入: 输入数据的第一部分是一张单词表,每行一个单词,单词的长度不超过10,它们代表的是Z交给cym统计的单词,

一个空行代表单词表的结束.

第二部分是一连串的提问,每行一个提问,每个提问都是一个字符串.

注意:本题只有一组测试数据,处理到文件结束.

输出: 对于每个提问,给出以该字符串为前缀的单词的数量。

 

这道题不同之处在于要查找某个前缀出现的次数,我们可以在_insert()的同时记录以每个节点为结尾的前缀的出现次数sum,在find()时返回sum[now]即可。

void _insert(string s)
{
    int now=0;
    int size=s.size();
    for(int i=0;i<size;i++)
    {
    int x=s[i]-'a';
    if(!trie[now][x])
    trie[now][x]=++total;
    sum[trie[now][x]]++;
    now=trie[now][x];
    }
}
int find(string s)
{
    int now=0;
    int size=s.size();
    for(int i=0;i<size;i++)
    {
    int x=s[i]-'a';
    if(!trie[now][x])
    return 0;
    now=trie[now][x];
    }
    return sum[now];
}
int main()
{
    string s;
    while(getline(cin,s))
    {
        if(s=="")break;
        _insert(s);    
    }    
    while(cin>>s)
    cout<<find(s)<<endl;
}

 P2580 于是他错误的点名开始了

 

这道题需要两个操作:

1.查找单词

2.判重

关于第一个操作想必不用说了

关于第二个操作,我想用map(STL大法好)

于是乎,为了介绍字典树,我还是给了字典树一点空间(要不都用map了)

 
void _insert(string s)
{
    int now=0;
    int size=s.size();
    for(int i=0;i<size;i++)
    {
        int x=s[i]-'a';
        if(!trie[now][x])
        trie[now][x]=++total;
        now=trie[now][x];
    }
    _word[now]=1;
}
bool find(string s)
{
    int now=0;
    int size=s.size();
    for(int i=0;i<size;i++)
    {
        int x=s[i]-'a';
        if(!trie[now][x])
        return 0;
        now=trie[now][x];
    }
    return _word[now];
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        string s;
        cin>>s;
        _insert(s);
    }
    scanf("%d",&m);
    for(int i=1;i<=m;i++)
    {
        string s;
        cin>>s;
        if(_map[s])
        {
            cout<<"REPEAT"<<endl;
            continue;
        }
        if(find(s))
        {
            cout<<"OK"<<endl;
            _map[s]=1;
        }
        else
        cout<<"WRONG"<<endl;
    }
}

[USACO08DEC]秘密消息

  

这道题的查找分两部分:

1.之前密码长度更长: 即查找前缀s的出现次数

2.之前密码长度更小: 即查找有多少单词是s的前缀。

重点放在2.

我们令s1表示s的前若干位,并去查找有多少单词等于s1

答案即为两部分之和

注:题目没说不会有重复密码

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<string>
#include<map>
using namespace std;
int n,m,trie[500010][3],_word[500010],sum[500010],total,point;
map<string,int>_time;
string ss[500010];
void _insert(string s)
{
    int now=0;
    int size=s.size();
    for(int i=0;i<size;i++)
    {
        int x;
        if(s[i]=='0')x=0;else x=1;
        if(!trie[now][x])trie[now][x]=++total;
        sum[trie[now][x]]+=_time[s];//预处理的成果在这里 
        now=trie[now][x];
    }
    _word[now]=1;
}
int find_sum(string s)
{
    int now=0;
    int size=s.size();
    for(int i=0;i<size;i++)
    {
        int x;
        if(s[i]=='0')x=0;else x=1;
        if(!trie[now][x])return 0;
        now=trie[now][x];
    }
    return sum[now];
}
bool find(string s)
{
    int now=0;
    int size=s.size();
    for(int i=0;i<size;i++)
    {
        int x;
        if(s[i]=='0')x=0;else x=1;
        if(!trie[now][x])return 0;
        now=trie[now][x];
    }
    return _word[now];
}
int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)//我们记录每个单词出现的次数(因为字典树是不会插入重复的单词的),进行预处理:
    {
        int x;
        string s="";
        scanf("%d",&x);
        for(int j=1;j<=x;j++)
        {
            char c;
            cin>>c;
            s+=c;
        }
        _time[s]++;
        if(_time[s]==1)
        ss[++point]=s;
    }
    for(int i=1;i<=point;i++)_insert(ss[i]);
    for(int i=1;i<=m;i++)//重点就是下面一段以及两个find函数 
    {
        int x,ans=0;
        string s="",s1="";
        scanf("%d",&x);
        for(int j=1;j<=x;j++)
        {
            char c;
            cin>>c;
            s+=c;
        }
        ans+=find_sum(s);
        for(int j=0;j<x-1;j++)
        {
            s1+=s[j];
            if(find(s1))ans+=_time[s1];
        }
        printf("%d\n",ans);
    }
}

前缀单词

 

字典树上的DP问题。

如果令f[i]表示以i为根的子树上的集合数,那么显然f[i]=所有儿子的f的乘积,如果以i为结尾是一个单词,那么f[i]+=1.(选了i就不能选他的儿子了),注意要考虑空集

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<string>
using namespace std;
int n,trie[10010][26],v[10010],total;
void _insert(string s)
{
    int now=0;
    int size=s.size();
    for(int i=0;i<size;i++)
    {
        int x=(int)s[i]-'a';
        if(!trie[now][x])
        trie[now][x]=++total;
        now=trie[now][x];
    }
    v[now]=1;
}
long long DP(int now)
{
    long long ans=1;
    for(int i=0;i<26;i++)
    if(trie[now][i])
    {
        ans*=DP(trie[now][i]);
    }
    return ans+v[now];
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        string s;
        cin>>s;
        _insert(s);
    }
    printf("%lld",DP(0));
}

 

总结:

操作: 找前缀,找单词

技巧: 结合map(逃)

从入门到精通到入土: 多刷题吧,多学习一些技巧。

posted @ 2018-05-05 20:42  _ZZH  阅读(222)  评论(0编辑  收藏  举报