字典树(Trie树)和几道题目

本文深入解析字典树的数据结构原理,展示如何利用字典树进行字符串的高效存储与查询,涵盖建树、查询、匹配计数等核心操作,并通过多个实战题目,如Luogu P2580、HDU1251、USACO Secret Message及洛谷P2292,详细演示字典树的应用场景与解题技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

补了补昨天rdc学长讲的关于字符串的知识点。字典树就是在树上存了一些单词(字符串),可以用来查询某个单词在其中是否存在,或者查询树上所有单词和某个字符串的公共前缀,问题形式也不算很多,学长说这主要是作为AC自动机的组成部分来考察。
树的树根为一空节点,从树根开始遍历一条路就能得到一个单词,而建树时是基于某些字符串的公共前缀来优化建树空间和时间的。下面是建树代码,trie[i][id]表示字母在树上i位置后的字母id存储的位置,cnt用来统计树上的节点数。

void insert(string s)
{
    int len=s.length();
    int h=0;
    for(int i=0;i<len;i++)
    {
        int id=s[i]-'a';
        if(!trie[h][id])//前缀到h为止,后面没有节点
        {
            trie[h][id]=++cnt;//树上存下字母id,节点数加一
        }
        h=trie[h][id];//继续沿此路向后建树,直到存下整个单词。这里可能会写错,切记不能写在if里面,因为字母id无论是否存在,都要向后继续走

    }
    return ;
}

查询操作的代码与上面大致相似,沿树上某条路走到叶子节点后如果还未跑完整个待查询单词,说明树上没有这个单词,否则查询成功。

bool query(string s)
{
    int len=s.length();
    int h=0;
    for(int i=0;i<len;i++)
    {
        int id=s[i]-'a';
        if(trie[h][id])//h后有id这个字母
        {
            h=trie[h][id];
        }
        else
        {
            return 0;
        }
    }
    return 1;
}

这样字典树的建树和查找的模板就码完了,看题:
LuoguP2580
这应该才是真正的板子题(难度等级也太低了吧),给一些名字,问后面每次点的名是否存在 \ 是否点重复了。
建树,按名字匹配,匹配成功时记得存一下树上这个位置已经到达过了,以后再匹配成功时就可以判断是否重复。
(改一下上面的板子就够了,不贴码了)
HDU1251
先输入一些单词组成字典,之后再给一些前缀,问每个前缀在字典中出现的次数。
简单分析:建树时,若单词有公共前缀会重复走到一些点,可以开一个新数组,sum[u]表示建树时重复走到节点u的次数,查询的时候在树上跑完每个前缀,直接输出其终点的sum就ok了。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include<cmath>
#include <algorithm>
#include <cstdlib>
#define ll long long
using namespace std;
const int maxn=500000+5;
int trie[maxn][26];
int cnt;
int sum[maxn];
void insert(string s)
{
    int len=s.length();
    int h=0;
    for(int i=0;i<len;i++)
    {
        int id=s[i]-'a';
        if(!trie[h][id])
        {
            trie[h][id]=++cnt;
        }
        h=trie[h][id];
        sum[h]++;
    }
    return ;
}
int query(string s)
{
    int len=s.length();
    int h=0;
    for(int i=0;i<len;i++)
    {
        int id=s[i]-'a';
        if(trie[h][id])
        {
            h=trie[h][id];
        }
        else
        {
            return 0;
        }
    }
    return sum[h];
}
int main()
{
    string s;
    while(getline(cin,s))
    {
        if(!s.length())break;
        insert(s);
    }
    string p;
    while(getline(cin,p))
    {
        printf("%d\n",query(p));
    }
    return 0;
}

[USACO08DEC]秘密消息Secret Message
和上一题类似,给一些已知前缀,再给一些新字符串,对每个新字符串,问给出的前缀中可能与其匹配的有多少。
题意有点绕,要注意可能存在新字符串比前缀还要短的情况(乱乱乱),而且这种情况下只要新字符串的能匹配成功还是要计入答案的!
引入新数组endh,endh[i]表示在字典树上位置i结束的单词有多少个,只需要在建树函数中插入单词结束时更新一下就可以了。
然后分别考虑:Ⅰ,树上的存的单词比当前查询的字符串短,回头看一下query函数,当查询到失配位置时退出,
答案为每个匹配成功位置的endh累加(因为要求的是树上满足条件的单词个数)而且这样是不会重复统计某个答案的。
Ⅱ,树上存的单词长度大于等于当前查询的字符串,一旦某个位置失配,其实和情况Ⅰ中失配情况一样,输出到当前失配位置前累加的endh。当跑完当前查询的字符串时,有可能在树的某条路上还没走到重点,所以这时要再加上我们上题中提到的sum[u]。
很合理,提交,WA了。
到这里忽略了一个细节,回忆一下那两个数组的意义,sum[u]记录经过这个点的有多少单词,endh[u]记录以这个点为终点的有多少单词,而经过这个点的单词一定包括以这个点为终点的单词,所以此时答案要减去endh[u]。数组维护时出错了就没办法了

#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include<cmath>
#include <algorithm>
#include <cstdlib>
#define ll long long
using namespace std;
const int maxn=500000+5;
int trie[maxn][2];
int cnt;
int n,m,k;
int sum[maxn],endh[maxn];
int b[maxn];
int getnum()
{
    int ans=0;
    bool f=1;
    char c=getchar();
    while(!isdigit(c))
    {
        if(c=='-')f=0;
        c=getchar();
    }
    while(isdigit(c))
    {
        ans=ans*10+c-'0';
        c=getchar();
    }
    return f ? ans : -ans;
}
void insert(int num[])
{
    int h=0;
    for(int i=1;i<=k;i++)
    {
        int id=num[i];
        if(!trie[h][id])
        {
            trie[h][id]=++cnt;
        }
        h=trie[h][id];
        sum[h]++;
    }
    endh[h]++;
    return ;
}
int query(int num[])
{
    int h=0;
    int ans=0;
    for(int i=1;i<=k;i++)
    {
        int id=num[i];
        if(!trie[h][id])
        {
            return ans;
        }
        h=trie[h][id];
        ans+=endh[h];
    }
    ans=ans+sum[h]-endh[h];
    return ans;
}
int main()
{
    n=getnum();
    m=getnum();
    for(int i=1;i<=n;i++)
    {
        k=getnum();
        for(int j=1;j<=k;j++)
        {
            b[j]=getnum();
        }
        insert(b);
    }
    for(int i=1;i<=m;i++)
    {
        k=getnum();
        for(int j=1;j<=k;j++)
        {
            b[j]=getnum();
        }
        printf("%d\n",query(b));
    }
    return 0;
}

(补个题,更新于2019.7.29)
洛谷P2292
n个字符串组成字典,m个模式串,询问每个模式串的前缀能被理解的长度,能被理解的前缀指这个前缀是否是由若干个字典中的单词组成,即符合要求的前缀是从模式串开头到最后一个完整单词的末尾位置。
注意我们在字典树上查询前缀的时候会查完一个单词就直接退出,因为这个单词的结尾没有连接下一个单词的开头,但我们可能要处理若干个单词。
解决的办法是预先存好每个单词结束位置在树上的节点编号,之后查询时每跑完一个单词,ans更新为当前在模式串上走过的长度,之后再处理后面的子串,看哪里是下一个单词的结束位置。
有点啰嗦了,上码


#include <iostream>
#include <cstdio>
#include <queue>
#include <cstdlib>
#include <cstring>
#include <string>
#define ll long long
const int maxn=1000000+5;
using namespace std;
int trie[maxn][26];
int cnt;
int n,m;
bool flag,isend[maxn],isendinp[maxn];
string s,p;
void gettrie(string ts)
{
    int h=0;
    int len=ts.length();
    for(int i=0;i<len;i++)
    {
        int id=ts[i]-'a';
        if(!trie[h][id])
        {
            trie[h][id]=++cnt;
        }
        h=trie[h][id];
    }
    isend[h]=1;
    return ;
}
int search(string tp)
{
    int h=0;
    int ans=0;
    int len=tp.length();
    memset(isendinp,0, sizeof(isendinp));
    for(int i=0;i<len;i++)
    {
        int id=tp[i]-'a';
        if(!trie[h][id])break;
        h=trie[h][id];
        if(isend[h])isendinp[i]=1;
    }
    for(int i=0;i<len;i++)
    {
        if(!isendinp[i])continue;
        else ans=i+1;
        h=0;
        for(int j=i+1;j<len;j++)
        {
            int id=tp[j]-'a';
            if(!trie[h][id])break;
            h=trie[h][id];
            if(isend[h])isendinp[j]=1;
        }
    }
    return ans;
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        cin>>s;
        gettrie(s);
    }
    for(int j=1;j<=m;j++)
    {
        cin>>p;
        cout<<search(p)<<"\n";
    }
    return 0;
}

今天还是学到不少东西的,过两天再写树剖的笔记OmO⬅背叛的眼神

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值