字典树模板详解附加例题

一、字典树

1.创建字典树

儿子数组 ch[p][j]:从节点p沿着j(例如:a 0 b 1)这条边走到子节点 
计数数组 cnt[p]:记录以节点p结尾的单词的插入次数
节点编号 idx:用来给节点编号

   char s[N];
   int ch[N][26],cnt[N],idx;
   void insert(char *s)
   {
   	    int p=0;
   	    for(int i=0;s[i];i++)
   	    {
   		    int j=s[i]-'a';//映射 
		    if(!ch[p][j])
		        ch[p][j]=++idx;
	  	    p=ch[p][j]; 
	    }
	        cnt[p]++;//插入次数 
   } 

PS:

1.空的字典树只有一个编号为0的根节点
2.从根开始插入,枚举字符串的每个字符。有儿子,p指针走到儿子,没有儿子,则先创建,再    走

3.单词结束点,记录插入次数

2.查询某个单词是否出现,或者出现的次数

int query(char *s)
{
	int p=0;
	for(i=0;s[i];i++)
	{
		int j=s[i]-'a';
		if(!ch[p][j])
		    return 0;
		p=ch[p][j];
	}
	return cnt[p];
} 

 ps:1.从根开始,先扫描字符串
    2.有字母s[i]并能走到词尾,则返回插入次数
    3.没有字母s[i],返回0;

3.时间复杂度及作用

时间复杂度: O(n)

作用:快速插入字符串、查询字符串

二、相关例题


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

一、题目要求

题目背景

XS中学化学竞赛组教练是一个酷爱炉石的人。

他会一边搓炉石一边点名以至于有一天他连续点到了某个同学两次,然后正好被路过的校长发现了然后就是一顿欧拉欧拉欧拉(详情请见已结束比赛 CON900)。

题目描述

这之后校长任命你为特派探员,每天记录他的点名。校长会提供化学竞赛学生的人数和名单,而你需要告诉校长他有没有点错名。(为什么不直接不让他玩炉石。)

输入格式

第一行一个整数 n,表示班上人数。

接下来 n 行,每行一个字符串表示其名字(互不相同,且只含小写字母,长度不超过 50)。

第 n+2 行一个整数 m,表示教练报的名字个数。

接下来 m 行,每行一个字符串表示教练报的名字(只含小写字母,且长度不超过 50)。

输出格式

对于每个教练报的名字,输出一行。

如果该名字正确且是第一次出现,输出 OK,如果该名字错误,输出 WRONG,如果该名字正确但不是第一次出现,输出 REPEAT

输入输出样例

输入

5  
a
b
c
ad
acd
3
a
a
e

输出 

OK
REPEAT
WRONG

说明/提示

  • 对于 40% 的数据,n≤1000,m≤2000。
  • 对于 70% 的数据,n≤10^4,m≤2×10^4。
  • 对于 100% 的数据,n≤10^4,m≤10^5。

upd 2022.7.30upd 2022.7.30:新增加一组 Hack 数据。

二、思路

1.在插入建树后,询问该单词的时候,按普通方法,最好解决的是名字错误,和名字正确且第一次出现。

2.面对名字正确但不是第一次出现,可以在建树过程中令cnt[p]=1;

3.当每次询问时,就比如样例中,第一个出现的a,输出OK,面对第二次出现的a,则需要在第一次出现过cnt[p]=1,变成cnt[p]=2,便可以进行标记,以此输出REPEAT;

三、代码

1.第一种方法:字典树

#include<bits/stdc++.h>
#define endl '\n'
#define int long long
#define IOS ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
using namespace std;
const int N=1e5+10;
const int inf=0x3f3f3f3f;
int n,m;
int ch[N*32][26],cnt[N*32],idx;
void insert(string s)//建树 
{
	int p=0;
	for(int i=0;i<s.size();i++)
	{
		int j=s[i]-'a';
		if(!ch[p][j])
		    ch[p][j]=++idx;
		p=ch[p][j]; 
	} 
	//cnt[p]++; 存储以节点p结尾的单词的插入次数
	cnt[p]=1;
} 
void query(string s)
{
	int p=0;
	for(int i=0;i<s.size();i++)
	{
		int j=s[i]-'a';
		if(!ch[p][j])
		    break;//return 0
		p=ch[p][j];
	}
	//return cnt[p]
	if(cnt[p]==1)
	{
		cout<<"OK"<<endl;
		cnt[p]=2;
	}
	else if(cnt[p]==2)
	    cout<<"REPEAT"<<endl;
	else
	    cout<<"WRONG"<<endl;
}
void solve()
{
	cin>>n;
	int i,j;
	for(i=1;i<=n;i++)
	{
		string ss; 
		cin>>ss;
		insert(ss); 
	}
	cin>>m;
	for(i=1;i<=m;i++)
	{
		string s;
		cin>>s;
		query(s);
	}
}
signed main()
{
    int t=1;
    while(t--)
    {
       solve();
    }
    return 0;
}

2. 第二种方法:STL容器(利用map<string,int>s)

先简单介绍下,map<string,int>s

map<类型名,类型名>变量名
第一个类型名:key值,key值只在关键字中出现一次
第二个类型名:value值,是key对应的数值 

#include<bits/stdc++.h>
#define endl '\n'
#define int long long
#define IOS ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
using namespace std;
const int N=2e5+10;
const int inf=0x3f3f3f3f;
int n,m;
map<string,int>s;
void solve()
{
	cin>>n;
	int i,j;
	for(i=1;i<=n;i++)
	{
		string x;
		cin>>x;
		s[x]=1;
	}
	cin>>m;
	for(i=1;i<=m;i++)
	{
		string y;
		cin>>y;
		if(s[y]==1)
		{
			cout<<"OK"<<endl;
			s[y]=2;
		}
		else if(s[y]==2)
		    cout<<"REPEAT"<<endl;
		else
		    cout<<"WRONG"<<endl;
	} 
}
signed main()
{
    int t=1;
    while(t--)
    {
       solve();
    }
    return 0;
}
/*
map<类型名,类型名>变量名
第一个类型名:key值,key值只在关键字中出现一次
第二个类型名:value值,是key对应的数值 
*/ 

P3879 [TJOI2010] 阅读理解

一、题目要求

题目描述

英语老师留了 N 篇阅读理解作业,但是每篇英文短文都有很多生词需要查字典,为了节约时间,现在要做个统计,算一算某些生词都在哪几篇短文中出现过。

输入格式

第一行为整数 N ,表示短文篇数,其中每篇短文只含空格和小写字母。

按下来的 N 行,每行描述一篇短文。每行的开头是一个整数 L ,表示这篇短文由 L 个单词组成。接下来是 L 个单词,单词之间用一个空格分隔。

然后为一个整数 M ,表示要做几次询问。后面有 M 行,每行表示一个要统计的生词。

输出格式

对于每个生词输出一行,统计其在哪几篇短文中出现过,并按从小到大输出短文的序号,序号不应有重复,序号之间用一个空格隔开(注意第一个序号的前面和最后一个序号的后面不应有空格)。如果该单词一直没出现过,则输出一个空行。

输入输出样例

输入 

3
9 you are a good boy ha ha o yeah
13 o my god you like bleach naruto one piece and so do i
11 but i do not think you will get all the points
5
you
i
o
all
naruto

输出 

1 2 3
2 3
1 2
3
2

说明/提示

对于 30% 的数据, 1≤M≤10^3 。

对于 100% 的数据,1≤M≤10^4,1≤N≤10^3 。

每篇短文长度(含相邻单词之间的空格)5×10^3 字符,每个单词长度 ≤20 字符。

每个测试点时限 22 秒。

感谢@钟梓俊添加的一组数据。

二、思路

唯一麻烦的一点是记录需要查询的单词出现在哪几篇短文中,即题目要求的从小到大输出其对应的序号,再次需要用bitset容器,若用bool数组,则会MLE

简单介绍下bitset容器的优点

bitset容器其实是个01串,可以被看作一个bool数组,
但比bool数组更优的点:节约空间,节约时间,支持基本的位运算。
在bitset容器中,8位占一个字节,而bool数组4位占一个字节。
n位的bitset在执行一次位运算的复杂度可以被看作n/32 

三、代码

#include<bits/stdc++.h>
#define endl '\n'
#define int long long
#define IOS ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
using namespace std;
const int N=5e6+10;
const int inf=0x3f3f3f3f;
int n,m,q;
int ch[N][26],cnt[N],idx;
bitset<1005>b[N];//声明一个1005位的bitset,利用bool会MLE  
void insert(string s,int x)
{
	int p=0;
	for(int i=0; i<s.size(); i++)
	{
		int j=s[i]-'a';
		if(!ch[p][j])
			ch[p][j]=++idx;
		p=ch[p][j];
	}
	b[p][x]=1;//记录序号
}
void query(string s)
{
	int p=0,f=0;
	for(int i=0;i<s.size(); i++)
	{
		int j=s[i]-'a';
		if(!ch[p][j])
		{
			f=1;
			break;
		}
		p=ch[p][j];
	}
	if(!f)
	{
		for(int i=1; i<=n; i++)//输出
		{
			if(b[p][i]==1)
				cout<<i<<' ';
		}
	}
	cout<<endl;
}
void solve()
{
	cin>>n;
	int i,j;
	for(i=1; i<=n; i++)
	{
		cin>>m;
		for(j=1; j<=m; j++)
		{
			string s;
			cin>>s;
			insert(s,i);//i代表短文编号
		}
	}
	cin>>q;
	while(q--)
	{
		string s;
		cin>>s;
		query(s);
	}
}
signed main()
{
	int t=1;
	while(t--)
	{
		solve();
	}
	return 0;
}

 P1481 魔族密码

一、题目要求

题目背景

风之子刚走进他的考场,就……

花花:当当当当~~偶是魅力女皇——花花!!^^(华丽出场,礼炮,鲜花)

风之子:我呕……(杀死人的眼神)快说题目!否则……-_-###

题目描述

花花:……咦好冷我们现在要解决的是魔族的密码问题(自我陶醉:搞不好魔族里面还会有人用密码给我和菜虫写情书咧,哦活活,当然是给我的比较多拉*^_^*)。

魔族现在使用一种新型的密码系统。每一个密码都是一个给定的仅包含小写字母的英文单词表,每个单词至少包含 1个字母,至多 75个字母。如果在一个由一个词或多个词组成的表中,除了最后一个以外,每个单词都被其后的一个单词所包含,即前一个单词是后一个单词的前缀,则称词表为一个词链。例如下面单词组成了一个词链:

  • ii;
  • intint;
  • integerinteger。

但下面的单词不组成词链:

  • integerinteger;
  • internintern。

现在你要做的就是在一个给定的单词表中取出一些词,组成最长的词链,就是包含单词数最多的词链。将它的单词数统计出来,就得到密码了。

风之子:密码就是最长词链所包括的单词数阿……

输入格式

这些文件的格式是,第一行为单词表中的单词数 N(1≤N≤2000),下面每一行有一个单词,按字典顺序排列,中间也没有重复的单词。

输出格式

输出共一行,一个整数,表示密码。

输入输出样例

输入 

5
i
int
integer
intern
internet

输出 

4

二、思路

第一种方法:利用string里面的find()函数,从后往前查,看有几个单词是它的子串,最后不要忘了加1

第二种方法:利用字典树加dfs

三、代码

第一种:find()函数

#include<bits/stdc++.h>
#define endl '\n'
#define int long long
#define IOS ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
using namespace std;
const int N=2100;
const int inf=0x3f3f3f3f;
int n;
string s[N];
void solve()
{
	int i,j;
	cin>>n;
	for(i=1;i<=n;i++)
	{
		cin>>s[i];
	}
	int ans=0; 
	for(i=1;i<=n;i++)
	{
		int cnt=0;
		for(j=1;j<i;j++)
		{
			if(s[i].find(s[j])==0)//s[j]要找的元素 
			{
				cout<<s[j]<<' '<<s[i]<<endl;
				cnt++;//从后往前找,看s[i]有几个不同的子串s[j]; 
			}
		}
		cout<<endl;
		ans=max(ans,cnt);
	}
	cout<<ans+1<<endl;
}
signed main()
{
    int t=1;
    while(t--)
    {
       solve();
    }
    return 0;
}
/*
string里面find()函数用法:
find():查找指定字符,从某个位置向后开始查找,若查找不到就返回-1 
返回值为目标字符的位置(第一个字符位置为0) 
*/

 第二种:字典树+dfs

#include<bits/stdc++.h>
#define endl '\n'
#define int long long
#define IOS ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
using namespace std;
const int N=2e5+10;
const int inf=0x3f3f3f3f;
int n,m,q,ans=0;
int ch[N][26],cnt[N],idx;
void insert(string s)
{
	int p=0;
	for(int i=0; i<s.size(); i++)
	{
		int j=s[i]-'a'+1;//a 1
		if(!ch[p][j])
			ch[p][j]=++idx;
		p=ch[p][j];
	}
	cnt[p]++; //记录以节点p结尾的单词的插入次数
}
void dfs(int x,int step)
{
	step+=cnt[x];
	bool f=0;
	for(int i=1;i<=26;i++)
	{
		if(ch[x][i])//如果这个字符存在 
		{
			dfs(ch[x][i],step);//接着走 
			f=1;
		} 
	}
	if(f==0)
	{
		ans=max(ans,step);
	}
}
void solve()
{
	cin>>n;
	int i,j;
	for(i=1; i<=n; i++)
	{
		string s;
		cin>>s;
		insert(s);
	}
	dfs(0,0);
	cout<<ans<<endl;
}
signed main()
{
	int t=1;
	while(t--)
	{
		solve();
	}
	return 0;
}

 P4551 最长异或路径

一、题目要求

题目描述

给定一棵 n 个点的带权树,结点下标从 11 开始到 n。寻找树中找两个结点,求最长的异或路径。

异或路径指的是指两个结点之间唯一路径上的所有边权的异或。

输入格式

第一行一个整数 n,表示点数。

接下来 n−1 行,给出 u,v,w ,分别表示树上的 u 点和 v 点有连边,边的权值是 w。

输出格式

一行,一个整数表示答案。

输入输出样例

输入 

4
1 2 3
2 3 4
2 4 6

输出 

7

说明/提示

最长异或序列是 1,2,3,答案是 7=3⊕4。

数据范围

1≤n≤100000;0<u,v≤n;0≤w<2^31。

二、思路

1.创建邻接链表

2.计算每个点到root(根)异或值(用dfs)

3.建立01字典树

4.查询

三、代码

#include<bits/stdc++.h>
#define endl '\n'
#define int long long
#define IOS ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
using namespace std;
const int N=1e6+10;
const int inf=0x3f3f3f3f;
int n;
int he[N],tot;
int to[N*2],nex[N*2],w[N*2]; 
int dis[N];
int ch[N*32][2],idx;
void add(int u,int v,int z)//创建邻接表+加边 
{
	
	to[tot]=v;
	w[tot]=z;
	nex[tot]=he[u];
	he[u]=tot++;
}
void dfs(int u,int father,int sum)
{
	dis[u]=sum;
	for(int i=he[u];~i;i=nex[i])
	{
		int j=to[i],ww=w[i];//ps:to[i]!!! 
		if(j!=father)//如果这条边的初边不是往上走的 
		{
			dfs(j,u,sum^ww);
		}
	}
}
void insert(int x)
{
	int p=0;
	for(int i=31;i>=0;i--)
	{
		int j=x>>i&1;
		if(!ch[p][j])
		    ch[p][j]=++idx;//不变 
		p=ch[p][j]; 
	}
} 
int query(int x)
{
	int p=0,res=0;
	for(int i=31;i>=0;i--)
	{
		int j=x>>i&1;
		if(ch[p][!j]) 
		{
			res+=1<<i;
			p=ch[p][!j]; 
		}
		else
		    p=ch[p][j];
	}
	return res;
}
void solve()
{
	cin>>n;
	int i,j;
	memset(he,-1,sizeof(he));
	for(int i=0;i<n-1;i++)//无向图 
	{
		int u,v,w;
		cin>>u>>v>>w;
		add(u,v,w);
		add(v,u,w);
	}
	dfs(0,-1,0);//挑0为根节点 
	/*for(i=0;i<n;i++)
	{
		cout<<dis[i]<<' ';
	}
	cout<<endl;*/
	for(i=0;i<n;i++)
	{
		insert(dis[i]);
	}
	int ans=0;
	for(i=0;i<n;i++)
	{
		ans=max(ans,query(dis[i]));
	}
	cout<<ans<<endl;
}
signed main()
{
    int t=1;
    while(t--)
    {
       solve();
    }
    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值