trie树
又称单词查找树,字典树,前缀树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。
个人觉得关键是搞明白数组tree[root][id]
的意思。(假设用于存仅含小写字母的单词)
若tree[root][id] == 0
,表示root这个点没有与表示字符id + 'a'
的点相连.
若tree[root][id] == k
,表示root这个点与表示字符id + 'a'
的点相连了,且表示id + 'a'
的这个点的编号是k.
(好像是这样)
除了tree数组,我们通常还会用到另外几个数组
1.bool数组boo[i]
,表示是否有一个单词结束在点i
2.int数组sum[i]
,表示结束在点i的这一坨字符是多少个字符串的前缀
…
添加单词
void insert_(char *str)
{
len = strlen(str);
root = 0;
for(int i = 0; i < len; ++i)
{
id = str[i] - 'a';
if(!tree[root][id]) tree[root][id] = ++tot;
root = tree[root][id];
}
boo[root] = true;
}
传一个字符指针,传指针比传整个字符串效率要高(应该是),但这意味着要用字符数组。
另外,可以考虑直接不传参。
int insert_()
{
len = s.size();
root = 0;
for(int i = 0; i < len; ++i)
{
id = s[i] - 'a';
if(!tree[root][id]) tree[root][id] = ++tot;
root = tree[root][id];
}
boo[root] = true;
return root;
}
对于这种不传参写法,首先是习惯不传参,另外是习惯用string,但不少题目最好还是用char数组,以防被卡。
一点前置知识
(下面几道题可能会用到)
欧拉图
(应该放在图论里整理的。。。当时没整理现在就成前置知识惹)
判断是否为欧拉图
• 如果图G中的一个路径包括每个边恰好一次,则该路径称为欧拉路径(Euler path)。
• 如果一个回路是欧拉路径,则称为欧拉回路(Euler circuit)。
• 具有欧拉回路的图称为欧拉图(简称E图)。具有欧拉路径但不具有欧拉回路的图称为半欧拉图。
• 无向图存在欧拉回路的充要条件
• 一个无向图存在欧拉回路,当且仅当该图所有顶点度数都为偶数,且该图是连通图。
• 有向图存在欧拉回路的充要条件
• 一个有向图存在欧拉回路,所有顶点的入度等于出度且该图是连通图。
无向图是半欧拉图的充要条件
有且仅有两个点的度数为奇数,或者全部点度数为偶数(直接是欧拉图了)
有向图是半欧拉图的充要条件
只有一个点入度大于出度,只有一个点出度大于入度,其余点入度等于出度。
c_str() 函数
string s = "1234";
printf("%s\n", s.c_str());
c_str()函数返回一个指向正规C字符串的指针, 内容与本string串相同.
这是为了与c语言兼容,在c语言中没有string类型,故必须通过string类对象的成员函数c_str()把string 对象转换成c中的字符串样式。
注意以下几点:
1.不能将该函数返回的指针赋给另一个字符指针
2.正确写法是利用strcpy等函数进行复制后再使用
char c[20];
string s="1234";
strcpy(c,s.c_str());
3.如果一个函数要求传char*参数,可以使用c_str()方法:
string s = "Hello World!";
printf("%s", s.c_str()); //输出 "Hello World!"
strrev函数
strrev(ch);
把字符串ch的所有字符的顺序颠倒过来(不包括空字符NULL)。
需要传参类型是char *ch
几道模板题
第一题HDU2072
分析:
模板题,对于每个单词,判断trie树里有没有,没有ans++,并把这个单词放到trie树即可。
本题中,trie树就是用来存着一些单词,然后用来查找,感觉很像map
AC代码:
#include <bits/stdc++.h>
using namespace std;
const int maxn = (int)2e6 + 5;
int tree[maxn][30];
int boo[maxn];
string s;
string temp;
stringstream ss;
int len, root, id, tot;
int ans;
void insert_()
{
len = temp.size();
root = 0;
for(int i = 0; i < len; ++i)
{
id = temp[i] - 'a';
if(!tree[root][id]) tree[root][id] = ++tot;
root = tree[root][id];
}
boo[root] = true;
}
bool find_()
{
len = temp.size();
root = 0;
for(int i = 0; i < len; ++i)
{
id = temp[i] - 'a';
if(!tree[root][id]) return true;
root = tree[root][id];
}
if(boo[root]) return false;
else return true;
}
int main()
{
ios::sync_with_stdio(false);
while(getline(cin, s))
{
if(s[0] == '#') break;
ss.clear();
ss << s;
while(ss >> temp)
{
if(find_())
{
ans++;
insert_();
}
}
cout << ans << '\n';
for(int i = 0; i <= tot; ++i)
{
boo[i] = 0;
for(int j = 0; j <= 26; ++j)
{
tree[i][j] = 0;
}
}
tot = ans = 0;
}
return 0;
}
第二题HUD1251
题意:
先输入一坨字符串
再输入一坨字符串
问你第二坨字符串中每个字符串在第一坨中,它是几个字符串的前缀。
分析:
比较模板的题(甚至好像可以完全不用trie树做),开个sum数组记录一下即可
但是有个地方需要注意一下,就是他说第一坨的输入以一个换行为结束,如果while(cin>>s)
的话,好像并不会换行输到s里去,while(~scanf("%s", ch)
好像也是,所以这里用gets
,另外,注意gets好像只对char数组生效。
本题中trie树用于存那一坨单词,以及单词前缀的判断
AC代码:
#include <bits/stdc++.h>
using namespace std;
const int maxn = (int)2e6 + 5;
int tree[maxn][30];
int sum[maxn];
char ch[15];
int len, root, id, tot;
void insert_()
{
len = strlen(ch);
root = 0;
for(int i = 0; i < len; ++i)
{
id = ch[i] - 'a';
if(!tree[root][id]) tree[root][id] = ++tot;
root = tree[root][id];
++sum[root];
}
}
int find_()
{
len = strlen(ch);
root = 0;
for(int i = 0; i < len; ++i)
{
id = ch[i] - 'a';
if(!tree[root][id]) return 0;
root = tree[root][id];
}
return sum[root];
}
int main()
{
while(gets(ch))
{
if(ch[0] == '\0') break;
insert_();
}
while(gets(ch)) printf("%d\n", find_());
}
第三题POJ2001
题意:
求一个能代表这个字符串的最短前缀。
即符合三个条件:
1.是该字符的前缀
2.不是其他任何一个字符的前缀
3.最短
分析:
先建trie树,需要维护sum[i]数组
,表示结束在点i的字符串是多少个字符串的前缀。
之后对于每个字符串,在树上找到第一个sum[]值为1的点,所经路径即答案
本题中trie树作用感觉与第二题差不多
AC代码:
#include <iostream>
#include <cstring>
using namespace std;
const int maxn = (int)2e6 + 5;
int tree[maxn][30];
int sum[maxn];
int len, id, root, tot;
string s[1005];
string re;
int cnt;
void insert_(int k)
{
len = s[k].size();
root = 0;
for(int i = 0; i < len; ++i)
{
id = s[k][i] - 'a';
if(!tree[root][id]) tree[root][id] = ++tot;
root = tree[root][id];
++sum[root];
}
}
void find_(int k)
{
len = s[k].size();
root = 0;
re = "";
for(int i = 0; i < len; ++i)
{
id = s[k][i] - 'a';
root = tree[root][id];
re += s[k][i];
if(sum[root] == 1) return ;
}
}
int main()
{
ios::sync_with_stdio(false);
while(cin >> s[++cnt]) insert_(cnt);
for(int i = 1; i <= cnt; ++i)
{
find_(i);
cout << s[i] << ' ' << re << '\n';
}
return 0;
}
第四题 POJ3630
题意:
给你一坨字符串,判断是否所有的字符串都不是其他字符串的前缀。
分析:
建trie树,然后跑每个字符串,只需要跑到len - 1
即可,判断是否有字符串结束在路径上的点,不需要跑到len,因为len是它自己。
另外,这个题用string和cin会超时,但是关同步可以过(188ms),当然用char数组肯定也不会被卡。
本题trie树作用,前缀。。。
AC代码:
#include <cstdio>
#include <iostream>
#include <cstring>
using namespace std;
const int maxn = (int)2e6 + 5;
int tree[maxn][15];
bool boo[maxn];
int t, n;
int tot, root, id, len;
string s[10050];
bool flag;
void insert_(int k)
{
len = s[k].size();
root = 0;
for(int i = 0; i < len; ++i)
{
id = s[k][i] - '0';
if(!tree[root][id]) tree[root][id] = ++tot;
root = tree[root][id];
}
boo[root] = true;
}
bool find_(int k)
{
len = s[k].size();
root = 0;
for(int i = 0; i < len - 1; ++i)
{
id = s[k][i] - '0';
root = tree[root][id];
if(boo[root]) return false;
}
return true;
}
int main()
{
std::ios::sync_with_stdio(false);
cin >> t;
while(t--)
{
cin >> n;
for(int i = 1; i <= n; ++i)
{
cin >> s[i];
insert_(i);
}
for(int i = 1; i <= n; ++i)
{
if(!find_(i))
{
flag = 1;
cout << "NO\n";
break;
}
}
if(!flag) cout << "YES\n";
for(int i = 0; i <= tot; ++i)
{
boo[i] = false;
for(int j = 0; j <= 10; ++j) tree[i][j] = 0;
}
flag = false;
tot = 0;
}
return 0;
}
第五题POJ2513
题意:
有几根棍子,没根棍子两端都有两种颜色,如果两根棍子的某一端颜色相同,那么这两根棍子可以接起来,问你最后能不能把所有的棍子接起来。
分析:
把每种颜色看成一个点,给同一根棍子上的两点连边,可以得到一张无向图,判断张图存不存在欧拉路径即可。
trie树在这个题的作用主要是把字符串映射成一个数字,用这个数字代表点,这个工作显然也可以用map实现,但可能时间复杂度就比较大惹。然后欧拉路径有关内容放在了前置知识里惹。
另外,由该题可以得出,在一些情况下我们可以用trie树来代替map
AC代码:
#include <cstdio>
#include <iostream>
#include <cstring>
using namespace std;
const int maxn = (int)2e6 + 5;
int tree[maxn][30];
int sum[maxn];
int fa[maxn];
int vis[maxn];
int pos1, pos2;
string s1, s2, s;
int len, tot, id, root;
int cnt;
int flag1;
bool flag;
int findfa(int x)
{
return x == fa[x] ? x : fa[x] = findfa(fa[x]);
}
void merge_(int x, int y)
{
fa[findfa(y)] = findfa(x);
}
int insert_(int k)
{
if(k == 1) s = s1;
else s = s2;
len = s.size();
root = 0;
for(int i = 0; i < len; ++i)
{
id = s[i] - 'a';
if(!tree[root][id]) tree[root][id] = ++tot;
root = tree[root][id];
}
if(!vis[root]) vis[root] = ++cnt;
return vis[root];
}
int main()
{
std::ios::sync_with_stdio(false);
for(int i = 0; i <= maxn; ++i) fa[i] = i;
while(cin >> s1 >> s2)
{
pos1 = insert_(1);
++sum[pos1];
pos2 = insert_(2);
++sum[pos2];
merge_(pos1, pos2);
}
for(int i = 1; i <= cnt; ++i)
{
if(findfa(1) != findfa(i))
{
flag = 1;
cout << "Impossible\n";
break;
}
if(sum[i] & 1) ++flag1;
}
if(!flag)
{
if(flag1 == 2 || flag1 == 0) cout << "Possible\n";
else cout << "Impossible\n";
}
return 0;
}
第六题POJ1451
题意:
本题背景是老年机的输入法。
告诉你每个单词出现的频率(次数),然后用户输入一个数字串,让你根据用户单词的使用频率对应输出这个数字串在不同长度是对应的最可能的单词
分析:
由于trie树可以统计出每个前缀出现的次数,每个前缀(每个单词)都对应一个数字串,在trie树添加元素时,一些前缀出现的次数会改变,然后我们可以更新每个数字串对应的出现次数最多的单词串,以及当前数字串对应单词串出现概率(次数),最后查询时,直接输出即可。由此,我们需要维护一下数据结构。
1.tree[maxn][30] trie树
2.sum[maxn] 前缀出现的次数
3.map<string,string> 存数字串对应的出现次数最多的单词串
4.map<string, int> 存数字串当前对应的单词串出现的次数
5.belong[i] 存第i个字母在九键上对应那个数字
另外,本题用了char数组,妹用string,对于trie树的题,最好还是使用char数组,因为比较快。然后char数组输出时用到一个叫做.c_str()的函数,见前置知识
AC代码:
#include <cstdio>
#include <cstring>
#include <map>
#include <iostream>
using namespace std;
const int maxn = (int)2e6 + 5;
int tree[maxn][30];
int sum[maxn];
int tot, root, id, len;
int belong[30];
map<string, int> mp1;
map<string, string> mp2;
int t, n, m, num;
string s, s1, s2;
string temp;
char ch[maxn];
void insert_(char *str, int num)
{
len = strlen(str);
root = 0;
s1 = "";
s2 = "";
for(int i = 0; i < len; ++i)
{
id = str[i] - 'a';
s1 += (belong[id] + '0');
s2 += str[i];
if(!tree[root][id]) tree[root][id] = ++tot;
root = tree[root][id];
sum[root] += num;
if(mp1[s1] < sum[root])
{
mp1[s1] = sum[root];
mp2[s1] = s2;
}
}
}
void build_belong()
{
for(int i = 0; i < 25; ++i)
{
if(i < 18) belong[i] = 2 + i / 3;
else belong[i] = 2 + (i - 1) / 3;
}
belong[25] = 9;
}
void Init()
{
mp1.clear();
mp2.clear();
for(int i = 0; i <= tot; ++i)
{
sum[i] = 0;
for(int j = 0; j < 26; ++j) tree[i][j] = 0;
}
tot = 0;
}
int main()
{
build_belong();
scanf("%d", &t);
for(int p = 1; p <= t; ++p)
{
Init();
scanf("%d", &n);
while(n--)
{
scanf("%s%d", ch, &num);
insert_(ch, num);
}
scanf("%d", &m);
printf("Scenario #%d:\n", p);
while(m--)
{
scanf("%s", ch);
temp = "";
len = strlen(ch);
for(int i = 0; i < len - 1; ++i)
{
temp += ch[i];
if(!mp2.count(temp)) printf("MANUALLY\n");
else printf("%s\n", mp2[temp].c_str());
}
putchar('\n');
}
putchar('\n');
}
return 0;
}
第七题HDU1247
题意:
给你一坨单词,找出所有的可以拆成另外两个单词的单词,按字典序输出。
分析:
建两颗trie树,一颗正序插入单词,另一颗逆序,把所有的单词插进去后开始遍历每一个单词,在正序trie树上查找这个单词,如果路径上某点是一个单词的结束点,标记该字符串的这一字符的位置i(++sum[i]),之后在逆序trie树上查找这个单词,如果路径上某点是一个单词的结束点,需要标记字符串的这一字符对应的正序字符串的前一个位置,这样的话,如果一个单词可以分为两个单词,那么该字符串某一位置值必定为2.
本题trie树,判断前缀和后缀
另外,对字符串逆序用到函数strrev,见前置知识
AC代码:
#include <bits/stdc++.h>
using namespace std;
const int maxn = (int)2e6 + 5;
int tree1[maxn][30];
int tree2[maxn][30];
set<string> st;
set<string>::iterator it;
string temp;
bool boo1[maxn], boo2[maxn];
int sum[maxn];
int tot1, tot2;
int len, id, root;
char ch[50050][60];
int cnt;
void insert1_(char *str)
{
len = strlen(str);
root = 0;
for(int i = 0; i < len; ++i)
{
id = str[i] - 'a';
if(!tree1[root][id]) tree1[root][id] = ++tot1;
root = tree1[root][id];
}
boo1[root] = true;
}
void insert2_(char *str)
{
len = strlen(str);
root = 0;
for(int i = 0; i < len; ++i)
{
id = str[i] - 'a';
if(!tree2[root][id]) tree2[root][id] = ++tot2;
root = tree2[root][id];
}
boo2[root] = true;
}
void find1_(char *str)
{
len = strlen(str);
root = 0;
for(int i = 0; i < len - 1; ++i)
{
id = str[i] - 'a';
root = tree1[root][id];
if(boo1[root]) ++sum[i];
}
}
void find2_(char *str)
{
len = strlen(str);
root = 0;
for(int i = 0; i < len - 1; ++i)
{
id = str[i] - 'a';
root = tree2[root][id];
if(boo2[root]) ++sum[len - i - 2];
}
}
int main()
{
while(~scanf("%s", ch[++cnt]))
{
insert1_(ch[cnt]);
strrev(ch[cnt]);
insert2_(ch[cnt]);
strrev(ch[cnt]);
}
for(int i = 1; i <= cnt; ++i)
{
len = strlen(ch[i]);
for(int j = 0; j <= len; ++j) sum[j] = 0;
find1_(ch[i]);
strrev(ch[i]);
find2_(ch[i]);
strrev(ch[i]);
for(int j = 0; j < len; ++j)
{
if(sum[j] == 2)
{
temp = ch[i];
st.insert(temp);
break;
}
}
}
for(it = st.begin(); it != st.end(); ++it) printf("%s\n", (*it).c_str());
return 0;
}
第八题POJ2408
题意:
给你一坨单词,如果某些单词经过重新排列后完全相同,那么它们是一个集合,让你输出元素最多的5个集合中的单词,如果集合元素数相同,则按照字典序从小到大。
分析:
本题中trie树的主要作用有点类似于map,map同样可以实现,不过比trie树多跑了200ms
#include <cstdio>
#include <iostream>
#include <cstring>
#include <set>
#include <algorithm>
using namespace std;
const int maxn = (int)2e6 + 5;
int tree[maxn][30];
int vis[maxn];
char ch[maxn];
int num[30];
struct node
{
int num;
set<string> st;
}ar[maxn];
string s, temp;
set<string>::iterator it;
int len, root, id, tot, cnt;
bool cmp(const node &a, const node &b)
{
if(a.num != b.num) return a.num > b.num;
else return *(a.st.begin()) < *(b.st.begin());
}
void insert_(char *str)
{
root = 0;
len = strlen(str);
s = "";
for(int i = 0; i < len; ++i) ++num[str[i] - 'a'];
for(int i = 0; i < 26; ++i)
{
while(num[i] != 0)
{
s += (i + 'a');
--num[i];
}
}
for(int i = 0; i < len; ++i)
{
id = s[i] - 'a';
if(!tree[root][id]) tree[root][id] = ++tot;
root = tree[root][id];
}
if(!vis[root]) vis[root] = ++cnt;
++ar[vis[root]].num;
temp = str;
ar[vis[root]].st.insert(temp);
}
int main()
{
while(~scanf("%s", ch)) insert_(ch);
sort(ar + 1, ar + cnt + 1,cmp);
for(int i = 1; i <= 5; ++i)
{
printf("Group of size %d: ",ar[i].num);
for(it = ar[i].st.begin(); it != ar[i].st.end(); ++it) printf("%s ", (*it).c_str());
printf(".\n");
}
return 0;
}
第九题
题意:
给n个数,从里面选两个求异或,最大是多少
分析:
把这n个数化成2进制,不够位的补零,然后倒着插到trie树中,然后对于每个数,遍历一遍tire树,即可找到在两个数其中一个数是当前数的情况下,最大的异或值。
AC代码:
#include <bits/stdc++.h>
using namespace std;
int n, t;
struct node
{
int en;
int vis[3];
}tree[3200050];
int ch[100005][35];
int tot, id, u, len;
int a, cnt, ans;
bool flag;
int bas[35];
void dio()
{
bas[0] = 1;
for(int i = 1; i <= 30; ++i) bas[i] = bas[i - 1] * 2;
}
void insert_(int *str)
{
u = 0;
for(int i = 32; i >= 0; --i)
{
id = str[i] - 0;
if(!tree[u].vis[id]) tree[u].vis[id] = ++tot;
u = tree[u].vis[id];
}
tree[u].en = 1;
}
int find_(int *str)
{
u = 0;
int res = 0;
for(int i = 32; i >= 0; --i)
{
id = str[i] - 0;;
if(tree[u].vis[1 - id])
{
u = tree[u].vis[1 - id];
res += bas[i];
}
else u = tree[u].vis[id];
}
return res;
}
int main()
{
dio();
scanf("%d", &n);
for(int i = 1; i <= n; ++i)
{
scanf("%d", &a);
cnt = 0;
while(a)
{
ch[i][cnt++] = a % 2;
a /= 2;
}
insert_(ch[i]);
}
for(int i = 1; i <= n; ++i) ans = max(ans, find_(ch[i]));
printf("%d\n", ans);
return 0;
}
第十题Libre10051
题意:
给你一个长度为n的数组,让你选两个没有覆盖的区间,使得这两个区间中数的异或和的和最大
分析:
异或有一个性质:就是据有可减性,也就是说我们记前缀异或sum数组,如果求[l,r]的异或和,那么答案就是sum[r]^sum[l-1];后缀异或也是一样的
所以我们可以对于每一个位置i都求一遍以i为结尾的区间中,区间异或和最大。方法是将sum[1…i]都插入到Trie中,然后find(sum[i])(第九题),答案计为l[i]数组。
同时,对于每一个位置i都求一遍以i为开头的区间中,区间异或和最大。方法是将sum[r…i](异或后缀和)都插入到Trie中,然后find(sum[i])(第九题),答案计为r[i]数组。
则ans = max(l[i] + r[i + 1])
AC代码:
#include <bits/stdc++.h>
using namespace std;
int tree[15000000][3];
int n, tmp, cnt;
int ar[400050];
int l[400050], r[400050], s[35], sum;
int bas[35];
int tot, id, len, u;
int ans;
void dio()
{
bas[0] = 1;
for(int i = 1; i <= 30; ++i) bas[i] = bas[i - 1] * 2;
}
void insert_()
{
u = 0;
for(int i = 31; i >= 0; --i)
{
id = s[i];
if(!tree[u][id]) tree[u][id] = ++tot;
u = tree[u][id];
}
}
int find_()
{
int res = 0;
for(int i = 31; i >= 0; --i)
{
id = 1 - s[i];
if(!tree[u][id]) id = 1 - id;
else res += bas[i];
u = tree[u][id];
}
return res;
}
int main()
{
dio();
scanf("%d", &n);
for(int i = 1; i <= n; ++i)
{
scanf("%d", &ar[i]);
sum = sum ^ ar[i];
tmp = sum;
cnt = 0;
memset(s, 0, sizeof(s));
while(tmp)
{
s[cnt++] = tmp & 1;
tmp >>= 1;
}
insert_();
l[i] = max(l[i - 1], find_());
//cout << sum << ' ' << tot << '\n';
}
//cout << '\n';
for(int i = 0; i <= tot; ++i)
{
tree[i][0] = 0;
tree[i][1] = 0;
}
tot = 0;
sum = 0;
for(int i = n; i >= 1; --i)
{
sum = sum ^ ar[i];
tmp = sum;
cnt = 0;
memset(s, 0, sizeof(s));
while(tmp)
{
s[cnt++] = tmp & 1;
tmp >>= 1;
}
insert_();
r[i] = max(r[i + 1], find_());
}
for(int i = 1; i < n; ++i) ans = max(ans, l[i] + r[i + 1]);
printf("%d\n", ans);
return 0;
}
一点点对trie树的理解?(其实妹李姐)
经过这几道题,现在对tire树第一感觉是有点像map(比如都能用于字符串计数,字符串映射成数字),但不同于map的有两点,一是trie树大多数情况比map快, 二是trie树对字符串前缀的处理上,确实,trie树在处理字符串前缀这方面有很大作用,所以trie树也叫前缀树。
另外就是在处理异或问题时,有奇效。
参考博客:https://blog.csdn.net/qq_38891827/article/details/80532462