Trie树(字典树)
目录
介绍:
Trie树
数据结构:可以高效的存储和查找字符串的
题目中需要用到Trie树时的关键词:全是大写字母,全是小写字母,全是数字,全是0/1
两个应用:
1.是否存在一个串,是当前串的前缀:当前串遍历路径中,是否存在串结尾标志,如果存在,成立
2.当前串,是否是某一个串的前缀:用当前串遍历路径中,是否创建过新节点,如果没有,成立
根节点:在Trie树中,下标为0的点,既是根节点,又是空节点,所以节点下表从1开始,++idx
空间大小:一般trie树的节点数要多开,具体根据题目分析
例题:
例题一 :
AC代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 5e5 + 7, M = 1e6 + 7;
char s[M];
int trie[N][26], cut[N], idx; //trie保存前缀树的信息, cut保存以某个节点作结的前缀的数量, index记录节点号,注意index是关键字
int m, n;
void insert()
{
int p = 0;
for (int i = 0; s[i]; i ++ ) //一种巧妙的读取字符的方法
{
int t = s[i] - 'a';
if(!trie[p][t])
trie[p][t] = ++ idx;
p = trie[p][t];
}
cut[p] ++;
}
bool exist_query() //判断一个串是否存在
{
int p = 0;
for(int i = 0; s[i]; i ++ )
{
int t = s[i] - 'a';
if(!trie[p][t])
return false;
p = trie[p][t];
}
return true;
}
int num_query() //查找一个串的前缀数量
{
int res = 0, p = 0;
for(int i = 0; s[i]; i ++ )
{
int t = s[i] - 'a';
if(!trie[p][t])
break; //不能用return 0,这里是结束查找而不是没找到的意思,没找到的话 res = 0
p = trie[p][t];
res += cut[p];
}
return res;
}
int main()
{
scanf("%d%d", &n, &m);
while (n -- )
{
scanf("%s", s);
insert(); //因为 s 是全局变量,所以这里可以不用传递 s 的值就可以直接调用
}
while (m -- )
{
scanf("%s", s);
printf("%d\n", num_query());
}
return 0;
}
tip:1.一种遍历字符串的方法
2.为什么 insert()和 query()不需要传递实参
参考:字典树(前缀树)
例题二 :
这道题不仅仅需要插入和查找,还用到了删除操作,但其实删除和插入大同小异
因为本题目数的大小<=1e5,所以我们可以规定每一个数都是6为的,不足6位前面补0
那么这个trie树的就是一棵“满二叉树”,所有叶子节点都在同一层,这样就方便计算了
不同与上一题,这一题的cnt在循环的里面++或者--,这是因为这里的cnt含义是不同的!千万不要思维定式了,上一题的cnt是某个字符串的数量,而这里的cnt是某个该节点代表的值的数量,例如插入两个111111,111111,那么cnt[1]~cnt[6]=2,表示该节点插入了两次,之所以这样,是方便下面的删除操作,我们只需要直接让cnt[k]--,就代表这个点代表的值少了一个,从而实现了删除的效果
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010, M = 2000010;
int n, a[N], cnt[N];
int tr[M][20], idx;
void Insert(int x)
{
string str = to_string(x);
while(str.size() < 6) str = "0" + str;
int p = 0;
for(int i = 0; str[i]; i ++ )
{
int u = str[i] - '0';
if(!tr[p][u]) tr[p][u] = ++ idx;
p = tr[p][u];
cnt[p] ++ ;//??
}
}
void erase(int x)
{
string str = to_string(x);
while(str.size() < 6) str = '0' + str;
int p = 0;
for(int i = 0; str[i]; i ++ )
{
int u = str[i] - '0';
p = tr[p][u];
cnt[p] -- ;
}
}
int query(int x)
{
string str = to_string(x);
while(str.size() < 6) str = "0" + str;
int p = 0, ans = 0;
for(int i = 0; str[i]; i ++ )
{
int u = 9 - (str[i] - '0');
while(1)
{
if(cnt[tr[p][u]])
{
p = tr[p][u];
ans = ans * 10 + (u + str[i] - '0') % 10;
break;
}
u -- ;
if(u < 0) u = 9;
}
}
return ans;
}
int main()
{
ios::sync_with_stdio(false);
cin >> n;
for(int i = 0; i < n; i ++ )
{
cin >> a[i];
Insert(a[i]);
}
for(int i = 0; i < n; i ++ )
{
erase(a[i]);
int res = query(a[i]);
cout << res << " ";
Insert(a[i]);
}
cout << endl;
return 0;
}
例题三 :
Trie + 前缀和的模板题
题目要求我们找到一个异或最大的区间,我们可以前缀和预处理,这样每次只需要计算两个区间,然后枚举每一个右端点,这样就确保了异或和最大的情况下,右端点最小
题目还要求在右端点最小的情况下,区间长度最小,即左端点最大,那么对于预处理前缀和之后的左端点,我们只需要让后出现的左端点覆盖前面相同的左端点就可以了,这样就实现了我们每次取得都是最靠右的左端点
其次,本题不能使用#define int long long,因为在trie题目中,数组范围本身就比较大,我们在开long long,又大了一倍,很容易Memory Limit
另外我们采取的是先查询在插入的方法,所以在查询之前要插入一个0,表示L-1可以为0
#include <iostream>
#include <cstring>
#include <algorithm>
#define endl '\n'
#define debug(x) cout << #x << " = " << endl
using namespace std;
const int N = 100010;
int n, w[N], id[N * 21];
int tr[N * 21][21], idx;
void Insert(int x, int y)
{
int p = 0;
for(int i = 20; i >= 0; i -- )
{
int u = (x >> i & 1);
if(!tr[p][u]) tr[p][u] = ++ idx;
p = tr[p][u];
}
id[p] = y;
}
int query(int x)
{
int p = 0;
for(int i = 20; i >= 0; i -- )
{
int u = (x >> i & 1);
if(tr[p][!u]) p = tr[p][!u];
else p = tr[p][u];
}
return id[p];
}
signed main()
{
cin >> n;
for(int i = 1; i <= n; i ++ )
{
int x; cin >> x;
w[i] = w[i - 1] ^ x;
}
int res = -1, l = 0, r = 0;
Insert(w[0], 0);
for(int i = 1; i <= n; i ++ )
{
int j = query(w[i]);
if((w[j] ^ w[i]) > res)
{
res = (w[j] ^ w[i]);
r = i;
l = j + 1;
}
Insert(w[i], i);
}
cout << res << " " << l << " " << r << endl;
return 0;
}