• 0x16 Tire


    参考链接:https://www.cnblogs.com/TheRoadToTheGold/p/6290732.html

    题目链接:https://www.acwing.com/problem/content/description/144/

    一、引入

    字典是干啥的?查找字的。

    字典树自然也是起查找作用的。查找的是啥?单词。

    看以下几个题:

    1、给出n个单词和m个询问,每次询问一个单词,回答这个单词是否在单词表中出现过。

    答:简单!map,短小精悍。

    好。下一个

    2、给出n个单词和m个询问,每次询问一个前缀,回答询问是多少个单词的前缀。

    答:map,把每个单词拆开。

    judge:n<=200000,TLE!

    这就需要一种高级数据结构——Trie树(字典树)

    二、原理

    在本篇文章中,假设所有单词都只由小写字母构成

    对cat,cash,app,apple,aply,ok 建一颗字典树,建成之后如下图所示

    由此可以看出:

    1、字典树用边表示字母

    2、有相同前缀的单词公用前缀节点,那我们可以的得出每个节点最多有26个子节点(在单词只包含小写字母的情况下)

    3、整棵树的根节点是空的。为什么呢?便于插入和查找,这将会在后面解释。

    4、每个单词结束的时候用一个特殊字符表示,图中用的‘′,那么从根节点到任意一个‘’所经过的边的所有字母表示一个单词。

    三、基本操作

    A、insert,插入一个单词

    1.思路

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

      这就产生一个问题:往哪儿插?计算机不会自己选择位置插,我们需要给它指定一个位置,那就需要给每个字母编号。

      我们设数组trie[i][j]=k,表示编号为i的节点的第j个孩子是编号为k的节点。

     什么意思呢?

     这里有2种编号,一种是i,k表示节点的位置编号,这是相对整棵树而言的;另一种是j,表示节点i的第j的孩子,这是相对节点i而言的。

     不理解?看图

     还是单词cat,cash,app,apple,aply,ok 

     我们就按输入顺序对其编第一种号,红色表示编号结果。因为先输入的cat,所以c,a,t分别是1,2,3,然后输入的是cash,因为c,a是公共前缀,所以从s开始编,s是4,以此类推。

    注意这里相同字母的编号可能不同

     

     第二种编号,相对节点的编号,紫色表示编号结果。

    因为每个节点最多有26个子节点,我们可以按他们的字典序从0——25编号,也就是他们的ASCLL码-a的ASCLL码。

    注意这里相同字母的编号相同

     实际上每个节点的子节点都应该从0编到——25,但这样会发现许多事根本用不到的。比如上图的根节点应该分出26个叉。节约空间,用到哪个分哪个。

     这样编号有什么用呢?

    回到数组trie[i][j]=k。 数组trie[i][j]=k,表示编号为i的节点的第j个孩子是编号为k的节点。

    那么第二种编号即为j,第一种编号即为i,k


    Trie(字典树)是种用于实现字符串快速检索的多叉树结构。Trie的每个节点都拥有若干个字符指针,若在插入或检索字符串时扫描到一个字符c, 就沿着当前节点的c字符指针,走向该指针指向的节点。下面我们来详细讨论Trie的基本操作过程。初始化
    一棵空Trie 仅包含一个根节点,该点的字符指针均指向空。

    插入
    当需要插入一个字符串S时,我们令一个指针P起初指向根节点。然后,依次扫描S中的每个字符c:
    1.若P的c字符指针指向一个已经存在的节点Q,则令P=Q.
    2.若P的c字符指针指向空,则新建一个节点Q, 令P的C字符指针指向Q,然后令P=Q。

    当S扫描完后,在当前节点P上标记他是一个末尾字符串。

    检索

    当需要检索一个字符串S在Trie中是否存在时,我们令一个指针P起初指向根节点,然后依次扫描S中的每个字符c:

    1.若P的c字符指针指向空,则说明S没有被插入过Trie,结束检索。

    2.若P的c字符指针指向一个已经存在的节点Q,则令P=Q.

    当S中的字符扫描完毕时,若当前节点p被标记为一个字符串的末尾,则说明在Trie中存在,否则说明s没有被插入过Trie。

    在上图所示的例子中,需要插入和检索的字符串都由小写字母构成,所以Trie树每个节点具有26个字符指针,分别为a到z。上图展示了在一棵空 Trie中依次插人“cab"“cos”“car'”“'cat”“cate" 和“rain" 后的Trie 的形态,灰色标记了单词的末尾节点。可以看出在Trie中,字符数据都体现在树的边(指针)上,树的节点仅保存一些额外信息,例如单词结尾标记等。其空间复杂度是0(NC), 其中N是节点个数,c是字符集的大小。

    void insert(char *s)//插入单词s
    {
        len=strlen(s);//单词s的长度
        root=0;//根节点编号为0
        for(int i=0;i<len;i++)
        {
            int id=s[i]-'a';//第二种编号
            if(!trie[root][id])//如果之前没有从root到id的前缀 
                trie[root][id]=++tot;//插入,tot即为第一种编号
            root=trie[root][id];//顺着字典树往下走
        }
        end[root]=true;
    }
    

      


    bool find(char *s)
    {
        len=strlen(s);
        root=0;//从根结点开始找
        for(int i=0;i<len;i++)
        {
            int x=s[i]-'a';//
            if(trie[root][x]==0)   return false;//以root为头结点的x字母不存在,返回0 
            root=trie[root][x];//为查询下个字母做准备,往下走 
        }
        return true;//找到了
    }
    

    字典树的完整代码

    #include <iostream>
    using namespace std;
    
    const int N = 1e5 + 10;
    int son[N][26]; // 其中存放的是:子节点对应的idx。其中son数组的第一维是:父节点对应的idx,第第二维计数是:其直接子节点('a' - '0')的值为二维下标。
    int cnt [N];    // 以“abc”字符串为例,最后一个字符---‘c’对应的idx作为cnt数组的下标。数组的值是该idx对应的个数。
    int idx;        // 将该字符串分配的一个树结构中,以下标来记录每一个字符的位置。方便之后的插入和查找。内存计数器,内存用到了哪个
    char str[N];
    
    void insert(char *str)
    {
        int p = 0;
        for (int i = 0; str[i]; i++)
        {
            int u = str[i] - 'a';
            if (!son[p][u]) son[p][u] = ++idx;
            p = son[p][u];
        }
        // 此时的p就是str中最后一个字符对应的trie树的位置idx。
        cnt[p]++;
    }
    
    int search(char *str)
    {
        int p = 0;
        for (int i = 0; str[i]; i++)
        {
            int u = str[i] - 'a';
            if (!son[p][u]) return 0;
            p = son[p][u];
        }
        return cnt[p];
    }
    bool startsWith(char *str)
    {
        int p=0;//从根结点开始找
        for (int i = 0; str[i]; i++)
        {
            int u = str[i] - 'a';
            if (!son[p][u]) return 0;//以p为头结点的u结尾字母不存在,返回0
            p = son[p][u];//为查询下个字母做准备,往下走
        }
        return true;//找到了
    }
    int main()
    {
        int n;
        scanf("%d", &n);
        char op[2];
        while (n--)
        {
            scanf("%s%s", op, str);
            if (op[0] == 'I') insert(str);
            else if(op[0]=='F') printf("%d
    ",startsWith(str));
            else printf("%d
    ", search(str));
        }
        return 0;
    }

      

    前缀统计

    把这N个字符串插入一棵Trie树,Trie 树的每个节点上存储一个整数cnt, 记录该节点是多少个字符串的末尾节点。(为了处理插入重复字符串的情况,这里要记录个数,而不能只做结尾标记)
    对于每个询问,在Trie树中检索T,在检索过程中累加途径的每个节点的cnt值,就是该询问的答案。

    #include<iostream>
    #include<string.h>
    using namespace std;
    const int SIZE=100050;
    int trie[SIZE][26],tot=1;
    int END[SIZE];
    int cnt;
    void insert(const char* str){
        int len=strlen(str),p=1;
        for (int k = 0; k < len; ++k) {
            int ch=str[k]-'a';
            if(trie[p][ch]==0)
                trie[p][ch]=++tot;
            p=trie[p][ch];
        }
        END[p]++;
    }
    
    int search(const char* str){
        cnt=0;
        int len=strlen(str),p=1;
        for (int k = 0; k < len; ++k) {
            int ch=str[k]-'a';
            p=trie[p][ch];
            if(p==0)
                return cnt;
            cnt+=END[p];
        }
        return cnt;
    }
    
    int main(){
        int n,m;
        cin>>n>>m;
        while(n--){
            string s;
            cin>>s;
            insert(s.c_str());
        }
        while(m--){
            string s;
            cin>>s;
            search(s.c_str());
            cout<<cnt<<endl;
        }
        return 0;
    }
    

      

    加油啦!加油鸭,冲鸭!!!
  • 相关阅读:
    mysql 分页查询的优化
    将某盘下所有文件名存在一个文件下面
    java 时间处理经典案例
    完整的发邮件并且生成测试报告的例子
    python 定时任务的执行
    打飞机游戏第一天,诸神归位
    数据库中插入几百万条数据
    面向对象的总结
    Python关于文件操作的总结
    python自动化,自动登录并且添加一个门店
  • 原文地址:https://www.cnblogs.com/clarencezzh/p/10776849.html
Copyright © 2020-2023  润新知