【字典树】

概述

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

光说不懂,上引例——

NKOJ 1934 外地人

    你考入大城市沙坪坝的学校, 但是沙坪坝的当地人说着一种很难懂的方言, 你完全
听不懂。 幸好你手中有本字典可以帮你。 现在你有若干个听不懂的方言需要查询字典。
输入格式
第一行,两个整数n和m。
接下来有n行表示字典的内容,每行表示一条字典的记录。每条记录包含两个空格间隔的单词,第一个单词为英文单词,第二个单词为对应的沙坪坝方言。
接下来有m行,每行一个单词,表示你要查询的沙坪坝方言。
输出格式
输出m行,每行一个英文单词,表示翻译后的结果。
如果某个单词字典查不到,输出"eh"
样例输入
5  3
dog  ogday
cat  atcay
pig  igpay
froot  ootfray
loops  oopslay
atcay
ittenkay
oopslay
样例输出
cat
eh
loops
 注:所有单词都用小写字母表示, 且长度不超过10。
传送门http://oi.nks.edu.cn/zh/Problem/Details/1934

Trie Tree的特点

  1. 根节点不包含字符, 除根节点外每一个节点都只包含一个字符。

  2. 从根节点到某一节点, 路径上经过的字符连接起来, 为该节点对应的字符串。

  3. 在trie树中查找一个关键字的时间和树中包含的结点数无关, 而取决于组成关键字的字符数。 也就是查找字符串s的时间为O(s.length())

  4. 如果要查找的关键字可以分解成字符序列且不是很长, 利用Trie树查找速度优于二叉查找树。
  如:若关键字长度最大是5, 则利用Trie树, 利用5次比较可以从265=11881376个可能的关键字中检索出指定的关键字。 而利用二叉查找树至少要进行log2265=23.5次比较。

接下来先给出引例题解的main函数部分(部分初始化未给出)——
struct node {
    int Num; //如果该节点是一个单词的结尾,记录对应单词的编号 int Next[26]; //儿子节点的编号 }trie[1000001]; string s[100001], a; int main() { cin >> n >> m; for (k = 1; k <= n; k ++){ cin >> s[k] >> a; Insert(a, k); } for (k = 1; k <= m; k ++) { cin >> a; ans = Find(a); if (ans)cout << s[ans]; else cout << "eh" << endl; } return 0; }
接着是两个函数的部分——
void Insert(string c, int k) {
    int i, t, len, p = 1; len = c.length(); for (i = 0; i < len; i ++) { t = c[i] - 'a';//将字符c[i]转换成值为0到25的数字,比如'a'转换为0,'b'转换为1,‘c’转换为2…… if (trie[p].Next[t] == 0) { //若p没有值为t的儿子 tot ++; //新增一个编号为tot的节点 trie[p].Next[t] = tot; //记下p的值为t的孩子节点的编号 p = trie[p].Next[t]; //p指向新添加的节点 trie[p].Num = 0; //初始化新添加的节点,将其标记为不是单词的结尾 } else p = trie[p].Next[t]; //若p存在值为t的儿子,p指向该儿子,继续讨论 } trie[p].Num = k; //for循环已执行完,说明第k个单词已加入,在单词结尾做上标记 } 

int Find(string c) {
    int i, t, len, p = 1; len = c.length(); for (i = 0; i < len; i ++) { t = c[i] - 'a'; if (trie[p].Next[t] == 0)return 0; //当前要匹配值为t的字母,若没有则结束 p = trie[p].Next[t]; //若存在值为t的字母,则继续匹配 } return trie[p].Num; //若for循环执行完毕,说明找到了需要的单词,返回其编号 }
以上的代码几乎就是字典树的模板,在不同的题中main函数或许有所不同,可以借此熟悉一下字典树的工作原理,再酌情修改。

trie树简单题:

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

#include<bits/stdc++.h>
using namespace std;
int tot,n,m;
string s;
struct node{
    int num,nxt[30];
}trie[1000005];
map<string,int>mp,mpp;
int Insert(string ss,int k){
    int p=1,len=ss.length();
    for(int i=0;i<len;i++){
        int t=ss[i]-'a';
        if(trie[p].nxt[t]==0){
            tot++;
            p=trie[p].nxt[t]=tot;
            trie[p].num=0;
        }
        else p=trie[p].nxt[t];
    }
    trie[p].num=k;
}
int myfind(string ss)
{
    int len=ss.length();
    int p=1;
    for(int i=0;i<len;i++){
        int t=ss[i]-'a';
        if(trie[p].nxt[t]==0) return 0;
        p=trie[p].nxt[t];
    }
    return 1;
}
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++){
        cin>>s;
        Insert(s,i);
    }
    cin>>m;
    for(int i=1;i<=m;i++){
        cin>>s;
        if(myfind(s)==0) cout<<"WRONG"<<endl;
        else if(myfind(s)&&mpp[s]==0) {cout<<"OK"<<endl;mpp[s]++;}
        else cout<<"REPEAT"<<endl;
    }
}

  NKOJ 1931 电话簿

#include<iostream>  
#include<cstring> 
#include<cstdio>  
using namespace std; 
const int maxn=100005,maxnode=200005;
int n;
bool ok;
struct node {
	int num,next[10];
};
struct Trie{
	int size;
	string s[maxn];
	node trie[maxn];
	void set (){
		size=1;
		memset(trie,0,sizeof(trie));
	}
	int idx(char x){ return (int) x-'0';} 
	bool insert(string a,int id){
		int i,p=1,t,len=a.length();
		bool sub=true;  //是否新建了节点 
		for(i=0;i<len;i++)
			if(isdigit(a[i])){
				if(trie[p].num) return true;  ///走到了别的单词结尾 ,相对于模板其实主要就是多了这里的判断
				t=idx(a[i]);
				if(!trie[p].next[t]){
					sub=false;  ///如果新建了节点说明并没有完整地遍历出树上字符串的的前缀
					p = trie[p].next[t]= ++size;
					trie[p].num=0;
				} 
				else p=trie[p].next[t];
			}
		trie[p].num=id;
		return sub;
	}
} solver;
int main(){
	ios_base::sync_with_stdio(false);
	int t,i;
	string num;
	cin>>t;
	while(t--){
		solver.set();  //初始化 
		cin>>n; 
		ok=false;
		for(i=1;i<=n;i++){
			cin>>num;
			if(!ok&&solver.insert(num,i)){  //出现了前缀 
				cout<<"NO"<<endl;
				ok=true;
			}
		}
		if(!ok)cout<<"YES"<<endl;
	}
}

 

P2292 [HNOI2004]L语言

这道题其实就是相对于模板多出了一个步骤,就是每次将你要查询的字符串查到没有了以后,再把这个字符串没有的地方重新开始遍历查找

#include<bits/stdc++.h>
using namespace std; 
long long n,m,sum;
string a;
int mark[1000005];
struct node
{
    int son[26];
    int mark;
}trie[1000005];
void add(string s)//建树
{
    long long pos=0;
    for(long long i=0;i<=s.size()-1;i++)
    {
        int now=s[i]-'a';
        if(trie[pos].son[now]==0) trie[pos].son[now]=++sum;
        pos=trie[pos].son[now];
    }
    trie[pos].mark=1;//单词结尾标记
}
void find(string s)
{
    int ans,pos=0;
    memset(mark,0,sizeof(mark));//每找一个字符串都要初始化! 
    for(int j=0;j<=s.size()-1;j++) //先跑一遍!不然下面的循环直接一路continue过去了qwq
        {
            int now=s[j]-'a';
            pos=trie[pos].son[now];
            if(pos==0) break;
            if(trie[pos].mark==1) mark[j]=1;
        }
    for(int i=0,k;i<=s.size()-1;i++)
    {
        if(!mark[i])continue; else ans=i+1;
        pos=0;
        for(int j=i+1;j<=s.size()-1;j++)
        {
            int now=s[j]-'a';
            pos=trie[pos].son[now];
            if(pos==0) break;
            if(trie[pos].mark==1) mark[j]=1;
        }
    }
    printf("%d\n",ans);
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        cin>>a;
        add(a);
    }
    for(int i=1;i<=m;i++)
    {
        cin>>a;
        find(a);//查找
    }
    return 0;
}

  

P2536 [AHOI2005]病毒检测

 

思路:

Trie 树+搜索

我用的是dfsdfs

首先对于将所有的RNA片段都建到TrieTrie树里去,之后来匹配那个模板串就好了

如果是匹配的位置是字母,那么我们就继续往下匹配

如果是??,我们必须要略过TrieTrie树上的一位去匹配

如果是*∗,我们可以先考虑直接忽略这一位,也可以直接把这一位当成??来看,或者是在TrieTrie树上略过一位,但是在模板串上的匹配仍为当前位置,这样就能实现在TrieTrie树上忽略多位进行匹配了

一旦某一位匹配成功了,我们如果还有状态到达这一步就没有什么必要了,所以开一个bitsetbitset来进行记忆化就好了

#include<iostream>
#include<cstring>
#include<cstdio>
#include<queue>
#include<bitset>
#define re register
#define maxn 500005
int son[maxn][4],flag[maxn];
int n,m,cnt,L,ans;
char S[1001],T[1001];
std::bitset<1001> f[maxn];
inline int read()
{
    char c=getchar();
    int x=0;
    while(c<'0'||c>'9') c=getchar();
    while(c>='0'&&c<='9')
      x=(x<<3)+(x<<1)+c-48,c=getchar();
    return x;
}
inline int ch(char p)
{
    if(p=='A') return 0;
    if(p=='G') return 1;
    if(p=='T') return 2;
    if(p=='C') return 3;
}
inline void ins()
{
    int len=strlen(S+1);
    int now=0;
    for(re int i=1;i<=len;i++)
    {
        if(!son[now][ch(S[i])]) son[now][ch(S[i])]=++cnt;
        now=son[now][ch(S[i])];
    }
    flag[now]++;
}
void dfs(int now,int t)
{
    if(t==L+1)//匹配成功
    {
        ans+=flag[now];
        flag[now]=0;
        return;
    }
    if(f[now][t]) return;
    f[now][t]=1;//记忆化,因为?和*可以变成任意的字符,所以肯定会有重复的方案,故记忆化排除重复的方案还一直dfs浪费时间
    if(T[t]>='A'&&T[t]<='Z') 
    {
        if(!son[now][ch(T[t])]) return;
        dfs(son[now][ch(T[t])],t+1);
    }//是字母,那么我们就继续往下匹配
    else 
    {
        if(T[t]=='?') 
        {
            for(re int i=0;i<4;i++)
            if(son[now][i]) dfs(son[now][i],t+1);//在Trie上忽略一位,同时模板串匹配位置加1
        }
        if(T[t]=='*')
        {
            dfs(now,t+1);//忽略*,即用0个字符来代替它
            for(re int i=0;i<4;i++)
            if(son[now][i]) dfs(son[now][i],t+1),dfs(son[now][i],t);
            //第一个dfs直接把这一位当成$?$来看,第二个dfs就能实现一个*代替多个字符
        }
    }
}
int main()
{
    scanf("%s",T+1);
    L=strlen(T+1);
    n=read();
    for(re int i=1;i<=n;i++)
        scanf("%s",S+1),ins();
    dfs(0,1);
    printf("%d\n",n-ans);
    return 0;
}

  

猜你喜欢

转载自www.cnblogs.com/hgangang/p/12333970.html