Trie树
字典树,又称前缀树,是用于快速处理字符串的问题,能做到快速查找到一些字符串上的信息。
另外,Trie树在实现高效的同时,会损耗更多的空间,所以Trie是一种以空间换时间的算法。
Trie树的思想
我也不明白怎么表达,我们看图吧!
Kur1su the great god
理解了吗?没理解我再想想办法。
我们用字典树干嘛?是不是更快的查询?那么在上面的trie树上是如何体现的呢?比如说我要查"great",那么我的查询过程就是 。从树上可以知道"great"和"god"的公共前缀是'g'。
从树上可以看出,想要插入一个字符串,就顺藤摸瓜一波,如果摸到一半,藤断了,那么我们就另起门户,按照字符串中的顺序新建一些节点连接在之前藤的末尾就好。如果你插入完了整个字符串,结果藤还没断,那么我们就给这个节点打个标记,表明这里是某个字符串的结尾。
这样的话,对于每次插入复杂度就是o(len(s))。
上面是插入的过程,那么你再想想查询呢?是不是一个道理!也是一直向下摸,不同点在于,如果你的藤没断,但是这个位置的没有打标记,这样,即使你在这个树上找到了字符串,但也是没找到的,只能说有一个字符串的前缀是它。对于每次查询复杂度就是o(len(s))。
Trie的实现
看了这么久,你也应该能想到node要怎么写了吧。
节点
struct node
{
int son[26];//我们这里只考虑小写字母,要是想要把全部字符都放进去,你这开大点就好。
bool f;//这个就是我们终点标记。
} trie[maxn];插入操作
void insert(string s)
{
int now=0;//根节点为0
int len=s.length();//需要插入的串的长度
for(int i=0;i<len;i++)
{
if( trie[now].son[s[i]-'a'] )//如果在之前插入过了部分前缀一样的字符串,就不用创建新的点了,也就是顺藤摸瓜的过程
{
now=trie[now].son[s[i]-'a'];
}
else//藤断了,就要自己创建新的节点。
{
trie[now].son[s[i]-'a']=num++;
now=trie[now].son[s[i]-'a'];
}
}
trie[now].f=1;//终点标记的修改
}查询操作
void find(string s)//返回值类型视具体情况
{
int now=0;
int len=s.length();
int now=0;
for(int i=0;i<len;i++)
{
if( trie[now].son[s[i]-'a'] )
{
now=trie[now].son[ s[i]-'a'];
}
else
{
cout<<"查询失败"<<endl;
return ;
}
}
if( trie[now].f)
{
cout<<"查询成功"<<endl;
return ;
}
else
{
cout<<"查询失败"<<endl;
return ;
}
}这么一来字典树的基本操作你就学会了!
来用我练习的题来检测自己吧!
练习题
练习题解答
P2580 于是他错误的点名开始了
思路:就是查询每个串是否存在,若存在再看是不是第一次被查询。
//team yglance+xhwl+TTD
//author: CN.TTDragon
#include<bits/stdc++.h>
typedef long long ll;
const ll mod=1e9+7;
const ll maxn=3e6+7;//这个值大概是node的son数组大小与题目规模的乘积,往大了开就好。
const double pi=acos(-1);
using namespace std;
int t,n,m;
int num=1;//这里也可以写成0,下面insert 的时候改成++num就好
struct node
{
int son[26];//全是小写字母的字符串,数组大小开26,27也可以
bool f;//表示这里是不是某个字符串的最后一个字符
int cnt;//这是为了本题而增加的一个标志,表示这个字符串是否有被查询过。
} trie [maxn];
void insert(string s)
{
int len = s.length();
int now=0;
for(int i=0;i<len;i++)
{
if(trie[now].son[s[i]-'a'])
{
now=trie[now].son[s[i]-'a'];
}
else
{
trie[now].son[s[i]-'a']=num++;
now=trie[now].son[s[i]-'a'];
}
}
//别忘记打上终点标记
trie[now].f = 1;
return ;
}
void find(string s)
{
int len = s.length();
int now = 0;
for(int i=0;i<len;i++)
{
if(trie[now].son[s[i]-'a'])
{
now=trie[now].son[s[i]-'a'];
}
else
{
cout<<"WRONG"<<endl;
return ;
}
}
if(! trie[now].f )
{
cout<<"WRONG"<<endl;
return ;
}
else
{
if(! trie[now].cnt)
{
cout<<"OK"<<endl;
trie[now].cnt++;
return ;
}
else
{
cout<<"REPEAT"<<endl;
return ;
}
}
}
int main()
{
ios::sync_with_stdio(false);
cin>>n;
string s;
for(int i=0;i<n;i++)
{
cin>>s;
insert(s);
}
cin>>n;
for(int i=0;i<n;i++)
{
cin>>s;
find(s);
}
return 0;
}
phone list
思路:跟上面一样,可以先把所有的串放进VECTOR,然后执行插入操作,最后再对每个串进行查询操作,查询到前缀串,就输出NO,否则YES。
//team yglance+xhwl+TTD
//author: CN.TTDragon
#include<bits/stdc++.h>
typedef long long ll;
const ll mod=1e9+7;
const ll maxn=2e6+7;
const double pi=acos(-1);
using namespace std;
struct node
{
int son [10];
int f;
} trie[maxn];
int num=0;//这次用的是0,所以下面是++num
void insert(string s)
{
int len=s.length();
int now=0;
for(int i=0;i<len;i++)
{
if(trie[now].son[s[i]-'0'] !=0 )
{
now=trie[now].son[s[i]-'0'];
}
else
{
trie[now].son[s[i]-'0']=++num;
now=trie[now].son[s[i]-'0'];
}
}
trie[now].f=1;
}
int x=1;//这个就是有无前缀的标志,1代表的是没有,也就是需要输出yes的情况
void find(string s)
{
int len=s.length();
int now=0;
for(int i=0;i<len;i++)
{
if(trie[now].son[s[i]-'0'] !=0 )
{
now=trie[now].son[s[i]-'0'];
if(trie[now].f==1&&i!=len-1) //存在前缀字符串,并且不是自己本身
{
x=0;
return;
}
}
}
return ;
}
vector<string > vec;
string s;
int main()
{
ios::sync_with_stdio(false);
//freopen("in.in","r",stdin);
//freopen("mine.out","w",stdout);
int t;
cin>>t;
while(t--)
{
x=1;
num=0;
vec.clear();
memset(trie,0,sizeof(trie));
int n;
cin>>n;
for(int i=0;i<n;i++)
{
cin>>s;
vec.push_back(s);
insert(s);
}
for(size_t i=0;i<n;i++)
{
find(vec[i]);
if(x==0)
{
cout<<"NO"<<endl;
break;
}
}
if(x)
{
cout<<"YES"<<endl;
}
}
return 0;
}
Immediate Decodability
思路:也是一样的找前缀。不同点在于输入,当输入'9'的时候表示输入结束。进行操作就好。
其实这题我觉得挺烦的,弄得我直接自闭。
//team yglance+xhwl+TTD
//author: CN.TTDragon
#include<bits/stdc++.h>
typedef long long ll;
const ll mod=1e9+7;
const ll maxn=1e5+7;
const double pi=acos(-1);
using namespace std;
int num=0;
struct node
{
int son[2];//01串,数组长度设置为2 就好
int f;
}trie[maxn];
string s[maxn];
void insert(string s)//插入操作没有任何改变
{
int len=s.length();
int now=0;
for(int i=0;i<len;i++)
{
if(trie[now].son[s[i]-'0'])
{
now=trie[now].son[s[i]-'0'];
}
else
{
trie[now].son[s[i]-'0']=++num;
now=trie[now].son[s[i]-'0'];
}
}
trie[now].f=1;
}
int find(string s)
{
int len=s.length();
int now=0;
for(int i=0;i<len;i++)
{
if(trie[now].son[s[i]-'0'])
{
now=trie[now].son[s[i]-'0'];
}
else
{
return 0;
}
}
//这个串已经被遍历完了,如果这时候的son还存在,那么就有一个别的串以它为前缀
if(trie[now].son[0]||trie[now].son[1])
{
return 1;
}
return 0;
}
int main()
{
ios::sync_with_stdio(false);
int n=0;
int bianhao=1;
int f=0;
while(cin>>s[n])
{
if(s[n]=="9")//解决问题
{
for(int i=0;i<n;i++)
{
f=find(s[i]);
if(f)//注意就是不一定需要全部查询的
{
break;
}
}
if(f)
{
printf("Set %d is not immediately decodable\n",bianhao++);
}
else
{
printf("Set %d is immediately decodable\n",bianhao++);
}
//记得重置,因为不重置,我WA了一下午。
n=0;
num=0;
f=0;
memset(trie,0,sizeof(trie));
}
else
insert(s[n++]);
}
return 0;
}The XOR Largest Pair
思路:首先你肯定是知道异或的计算的,就是二进制表示下,对于同一位,如果两个数不同,则该位置的异或值为1。相同为0。那么对于这个题,我们把每个数的二进制表示都放进去,然后去查询有没有不一样的不就行了。注意的是我们要从高位开始,并且如果没有不一样的,也不能退出,而要继续找下去。
//team yglance+xhwl+TTD
//author: CN.TTDragon
#include<bits/stdc++.h>
typedef long long ll;
const ll mod=1e9+7;
const ll maxn=4e6+7;
const double pi=acos(-1);
using namespace std;
int num=1;
struct node
{
int son[2];//二进制数,数组开2就好
ll f;
}trie[maxn];
string fenjie(ll n)//分解为2进制数
{
string s="";
for(int i=0;i<32;i++)
{
s+=char((n&1)+48);
n>>=1;
}
//注意这个s可不是n的二进制表示。顺序反了
string ss="";
for(int i=31;i>=0;i--)
{
ss+=s[i];
}
return ss;
}
void insert(string s)//插入还是没变化
{
int len=32;
int now=0;
for(int i=0;i<len;i++)
{
if(trie[now].son[s[i]-'0'])
{
now=trie[now].son[s[i]-'0'];
}
else
{
trie[now].son[s[i]-'0']=num++;
now=trie[now].son[s[i]-'0'];
}
}
trie[now].f=1;
}
ll find(string s)
{
ll mx=0;//本次
int a[32];//这个就是两个数异或后得到的答案数字的二进制表示形式
int len=32;
int now=0;
for(int i=0;i<len;i++)
{
if(trie[now].son[!(s[i]-'0')])//要取反哦
{
now=trie[now].son[!(s[i]-'0')];
a[31-i]=1; //i越小,对应位置的值是越大的!
}
else
{
now=trie[now].son[s[i]-'0'];
a[31-i]=0;
}
}
ll q=1;
for(int i=0;i<len;i++)
{
mx+=q*a[i];
q<<=1;
}
return mx;
}
int main()
{
ios::sync_with_stdio(false);
//freopen("in.in","r",stdin);
//freopen("mine.out","w",stdout);
int n;
cin>>n;
ll a;
string s;
ll mx=0;
for(int i=0;i<n;i++)
{
cin>>a;
s=fenjie(a);
insert(s);
mx=max(mx,find(s));
//cout<<a<<" "<<s<<endl;
}
cout<<mx<<endl;
return 0;
}
秘密消息Secret Message
思路:容易知道,需要输出的为:密码是多少条信息的前缀。那么这么一来又变成了模板了。这题的关键点就是信息长度和密码长度都未知的,他只搞到了部分,所以可以快乐YY,增加答案大小。
//team yglance+xhwl+TTD
//author: CN.TTDragon
#include<bits/stdc++.h>
typedef long long ll;
const ll mod=1e9+7;
const ll maxn=1e6+7;
const double pi=acos(-1);
using namespace std;
int num=1;
struct node
{
int son[2];//二进制
int f;
int cnt;
}trie[maxn];
void insert(string s,int len)//因为题目给了你长度,多添加一个长度参数
{
int now=0;
for(int i=0;i<len;i++)
{
if(trie[now].son[s[i]-'0'])
{
now=trie[now].son[s[i]-'0'];
}
else
{
trie[now].son[s[i]-'0']=num++;
now=trie[now].son[s[i]-'0'];
}
if(i!=len-1)//值得注意的是这里!
trie[now].cnt++;//有信息经过了这里
}
trie[now].f++;//以这里为终点的信息 数量增加
}
int find(string s,int len)
{
int now=0;
int ans=0;
for(int i=0;i<len;i++)
{
if(trie[now].son[s[i]-'0'])
{
now=trie[now].son[s[i]-'0'];
}
else//后面已经没有匹配的了,退出就好。
{
return ans;
}
if(trie[now].f) //存在以这里为终点的信息 ,这些信息后缀是不知道的。所以可以让他们成为答案中的一员。
{
ans+=trie[now].f;
}
}
return ans+trie[now].cnt;//最后再加上经过这里的信息就好。
}
int main()
{
ios::sync_with_stdio(false);
//freopen("in.in","r",stdin);
//freopen("mine.out","w",stdout);
int n,m;
cin>>m>>n;
int q;
string s="";
char c;
//int mx=0;
for(int i=0;i<m;i++)
{
cin>>q;
s="";
for(int j=0;j<q;j++)
{
cin>>c;
s+=c;
}
insert(s,q);
}
for(int i=0;i<n;i++)
{
cin>>q;
s="";
for(int j=0;j<q;j++)
{
cin>>c;
s+=c;
}
cout<<find(s,q)<<endl;
}
//cout<<mx<<endl;
return 0;
}
还剩几个题,以为自己会了,结果发现自己好像解释不清楚。真是离谱,等我去琢磨一下。
写在最后:
本文是参考该文章 写的(因为我是看这该文学的,讲的很好,如果看完我的辣鸡文章你依然不能完全理解的话,可以去看看)
太菜了~现在看什么都是字典树。结果就是我2020JSCPC的H题T了无数发。从dp到爆搜。

京公网安备 11010502036488号