什么是字典树?
字典树使用边来表示字母,从根节点到树上某一结点的路径表示了一个字符串。
字典树要解决什么?
查找字符串,尤其是查找一个字符串是否为另一个字符串的前缀,以及前缀的长度
字典树的特点
1.使用边来表示字母
2.有相同字母的单词公用前缀节点,每个节点最多有N个子节点(N为该字符串包含的字符种类)
3.根节点为空
4.每个单词结束时,在该节点添加标记flag以表示这段路径是一个单词而非前缀
字典树的基本操作
- 插入——insert
对于一个单词,从左往右进行扫描,如果一个字母在其相应根节点1下未出现,则插入该字母,否则,沿着字典树向下,查看单词的下一个字母。
字母该插入在哪个位置呢?
我们设置数组trie[i][j]=k,表示编号为i的节点的第j个孩子的编号为k,节点编号由插入的顺序决定
这里假设先后插入两个单词cat,car
未插入时
插入单词cat,依次插入c,a,t,所有字母在其相应根节点下都未出现
插入单词car,依次插入c,a,r,发现根节点K=0下有c,不重复插入,沿字典树向下,到达节点K=1,发现K=1有a,不重复插入,沿字典树向下,到达节点K=2,K=2下没有r,插入r
注意:因为要判断是前缀还是完整单词,树上节点还需打上标记
例如:如这之后再插入一个单词ca,则进行如下的标记
关于插入操作的时间复杂度:O(n),n为插入的字符串长度
实现代码:
void insert(string s)
{
int len = s.size();//字符串的长度
int p = 0;//根节点
for(int i=0;i<len;++i)
{
int c = s[i]-'a';
if(!trie[p][c])
{
trie[p][c] = k;//k是一个全局变量,用于给节点编号
++k;
}
p = trie[p][c];//向下遍历
}
color[p] = 1;//完成插入操作后,进行标记,表明这是一个完整的单词
}
- 查找——search
查找有多种形式,最常见的就是查找前缀和查找一个单词了。
例如,想要查找一个前缀是否出现过,其遍历顺序与插入时类似,也是从左往右进行遍历,与插入操作不同的是,如果发现某个字母未出现在对应节点下,则直接返回false,表示该前缀未出现;如果遍历至尾则返回true,表示前缀出现。
查找单词,则是在到达末尾后进行一次额外的检查,看该点是否被标记过,标记过则返回true,否则返回false,表示该点只是一个前缀而非单词。
关于查找操作的时间复杂度:O(n),n为查找到字符串的长度
实现代码:
int search(string s)
{
int len = s.size();
int p = 0;
for(int i=0;i<len;++i)
{
int c = s[i]-'a';
if(!trie[p][c])//该节点下没有字母c
{
return false;//返回未找到
}
else
{
p = trie[p][c];
}
}
return color[p];//如果是查找前缀则直接返回true
}
一道例题:洛谷P2580,这道题要查找的是单词的出现次数,所以插入时每出现一次标记一次即可,查找时返回标记次数
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<string>
#include<cmath>
#include<queue>
#include<vector>
#include<map>
#define fast ios::sync_with_stdio(false), cin.tie(0), cout.tie(0)
#define ll long long
#define endl "\n"
#define pii pair<int,int>
#define pb push_back
#define debug(x) cout << "visit:" << x << endl
#define inf 2147483647
using namespace std;
const int N = 5e5+10;
const int char_set = 26;
int trie[N][26]={0};
int color[N]={0};
int k = 1;
void insert(string s)
{
int len = s.size();
int p = 0;
for(int i=0;i<len;++i)
{
int c = s[i]-'a';
if(!trie[p][c])
{
trie[p][c] = k;
++k;
}
p = trie[p][c];
}
color[p] = 1;
}
int search(string s)
{
int len = s.size();
int p = 0;
for(int i=0;i<len;++i)
{
int c = s[i]-'a';
if(!trie[p][c])
{
return false;
}
else
{
p = trie[p][c];
}
}
if(color[p]>=1)
{
++color[p];
}
return color[p];
}
void solve()
{
int n;
cin >> n;
string s;
for(int i=1;i<=n;++i)
{
cin >> s;
insert(s);
}
int m;
cin >> m;
for(int i=1;i<=m;++i)
{
cin >> s;
int flag = search(s);
if(!flag)
{
cout << "WRONG" << endl;;
}
else
if(flag<=2)
{
cout << "OK" << endl;
}
else
{
cout << "REPEAT" << endl;
}
}
}
int main()
{
solve();
return 0;
}
其他做题记录:
1.
HDU1251统计难题
统计某一前缀出现了多少次,标记方式改为每向下遍历则将该编号+1即可,代码略
01字典树
01-trie 是指字符集为 {0.1}的 trie。01-trie 可以用来维护一些数字的异或和,支持修改(删除 + 重新插入),和全局加一(即:让其所维护所有数值递增 1,本质上是一种特殊的修改操作)。
0-1tire通常用于处理有关异或操作的问题
例题:
洛谷P4551最长异或路径
首先,设1为根,计算根到所有点的路径异或值。通过这样一个操作,就可以得到所有点之间路径的异或值(两点到根的路径出现重复的部分,会因为异或操作消除其影响)
考虑到异或操作是针对二进制展开的操作,可将整数转换成对应的二进制数字符串,转换完之后,将所有字符串插入字典树,即生成了一棵0-1字典树,遍历由根到树上所有结点异或值对应的字符串,从最高位开始向下进行搜索,沿树尽量选取异或结果为1的路径搜索,就可在字典树上找到使异或值最大的另一个结点。
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<string>
#include<cmath>
#include<queue>
#include<vector>
#include<map>
#define fast ios::sync_with_stdio(false), cin.tie(0), cout.tie(0)
#define ll long long
#define endl "\n"
#define pii pair<int,int>
#define pb push_back
#define debug(x) cout << "visit:" << x << endl
#define inf 2147483647
using namespace std;
const int N = 1e5+5;
const int M = 4e6+5;
vector<pii> g[N];
int trie[M][2]={0}, k = 0;
int r[N]={0};
string rs[N];
string change(int num)
{
string s="";
for(int i=30;i>=0;--i)
{
if(num&(1<<i))
{
s = s+'1';
}
else
{
s = s+'0';
}
}
return s;
}
void insert(string s)
{
int len = s.size();
int p = 0;
for(int i=0;i<len;++i)
{
int c = s[i]-'0';
if(!trie[p][c])
{
trie[p][c] = ++k;
}
p = trie[p][c];
}
}
int search(string s)
{
int res = 0, num = 1<<30;
int len = s.size();
int p = 0;
for(int i=0;i<len;++i)
{
int c = s[i]-'0';
if(trie[p][(c+1)%2])
{
res += num;
p = trie[p][(c+1)%2];
}
else
{
p = trie[p][c];
}
num /= 2;
}
return res;
}
void dfs(int u,int fa,int val)
{
r[u] = val;
rs[u] = change(val);
insert(rs[u]);
for(int i=0;i<g[u].size();++i)
{
int v = g[u][i].first, w = g[u][i].second;
if(v!=fa)
{
dfs(v,u,val^w);
}
}
}
int main()
{
int n;
scanf("%d",&n);
for(int i=1;i<n;++i)
{
int u, v, w;
scanf("%d %d %d",&u,&v,&w);
g[u].push_back(make_pair(v,w));
g[v].push_back(make_pair(u,w));
}
dfs(1,0,0);
int ans = 0;
for(int i=1;i<=n;++i)
{
ans = max(ans,search(rs[i]));
}
printf("%d\n",ans);
return 0;
}
其他题目:
HDU4825Xor Sum
实际上就是上面题目的简化版本,更适合当例题一点(
关于相应根节点:首字母的根节点是整棵树的根节点,不是首字母则是遍历至上一个字母时对应的节点 ↩︎