Trie 字典树

一、字典树

什么是字典树

字典树又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。(摘自百度百科)

字典树的应用

给你 n   ( n ≤ 1 0 7 ) n \ (n \le 10 ^ 7) n (n107)个长度为10的字符串。对于每一个单词,我们要判断他出没出现过,如果出现了,求出现在出现了几次。
暴力的话是 O ( n 2 ) O(n ^ 2) O(n2),大概要个30多个小时(按每秒 1 0 9 10^9 109次运算来算),神威·太湖之光巅峰大概能一秒算完,不多不多
现在我们换个思路想。假设我要查询的单词是abcd,那么在他前面的单词中,以b,c,d,f之类开头的我显然不必考虑。而只要找以a开头的中是否存在abcd就可以了。同样的,在以a开头中的单词中,我们只要考虑以b作为第二个字母是否存在,以此类推,一次次缩小范围,这样一个树的模型就出来了。
假设 n = 4 n=4 n=4,字符串为abc,cdb,cde,eed,我们构建的树就是下图这样的:

root
a
b
c
c
d
b
e
e
e
d

我们可以看到,trie树每一层的节点数是26^i级别的。所以为了节省空间。我们用动态链表,或者用数组来模拟动态。空间的花费,不会超过单词数×单词长度。

字典树的优缺点

  • 优点:字典树中实际上记录了多个字符串的公共前缀,因此用于查询公共前缀时是十分高效的,他减少了无意义的字符串匹配,其查询效率要优于哈希树。

  • 缺点:字典树的内存消耗非常大。

我们不难发现,字典树的核心思想是用空间来换时间(利用公共前缀来降低查询时间的开销以达到提高效率的目的)。

字典树的操作

基本思路:从左到右扫描这个单词,如果扫描指针指向的字母在相应的根节点下没有出现过,就插入这个字母;否则沿着字典树往下走,扫描指针后移。

  • 首先我们需要记录根结点的编号,初始化为0
  • 从左到右扫描这个单词,每次计算关系编号id(在字符集{a~z}中,关系编号- 为’当前字符’ - ‘a’)
  • 如果之前没有从u到id的前缀,则插入该编号(需要预先在声明计数变量,对一种编号进行记录)
  • 执行u = tree[root][id],表示顺着字典树继续向下走。

二、字典树练习

洛谷 P8306

板子题,没啥好讲的。

/*
*/
#include<bits/stdc++.h>
#define rep(i,s1,s2,s3) for(i = s1;i <= s2;i += s3)
#define r(i,s1,s2,s3) for(i = s1;i >= s2;i -= s3)
#define ull unsigned long long
#define sort stable_sort
#define INF 0x7f7f7f7f
#define ll long long 
using namespace std;
int n,q;
int get(char ch){
	if(ch >= '0' && ch <= '9') return ch - '0';
	else if(ch >= 'A' && ch <= 'Z') return ch - 'A' + 10;
	else return ch - 'a' + 36;
} 
struct Trie{
	int id,cnt[3000010],trie[3000010][65];
	void insert(string str){
		int i,u = 0,s;
		rep(i,0,str.size() - 1,1){
			s = get(str[i]);
			if(!trie[u][s]) trie[u][s] = ++id;
			u = trie[u][s];
			cnt[u]++;
		}
	}
	int query(string str){
		int i,u = 0,s;
		rep(i,0,str.size() - 1,1){
			s = get(str[i]);
			if(!trie[u][s]) return 0;
			u = trie[u][s];
		}
		return cnt[u];
	}
}tr; 
int main(){
	ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
	//freopen(".in","r",stdin);
	//freopen(".out","w",stdout);
	int i,j,t;
	string str;
	cin>>t;
	while(t--){
		cin>>n>>q;
		rep(i,0,tr.id,1) rep(j,0,122,1) tr.trie[i][j] = 0;
		rep(i,0,tr.id,1) tr.cnt[i] = 0;
		tr.id = 0;
		rep(i,1,n,1){
			cin>>str;
			tr.insert(str);
		}
		rep(i,1,q,1){
			cin>>str;
			cout<<tr.query(str)<<'\n';
		}
	} 
	return 0;
}

洛谷 P8306

首先我们要知道一个异或的性质:(u ⊕ f) ⊕ (v ⊕ f) = u ⊕ v
对于这题,我可以用dfs跑一遍树,计算出自己到根节点的异或值,i~j的路径上的异或和,就可以表示成根到i的异或和异或上根到j的异或和。那思路就很明确了:对于每一位进行贪心,如果这一位有一个与它不同的,即异或 后是1,那我们就顺着这条路往下走;否则就顺着原路往下走。这样贪心为什么是对的呢?因为当前这一位的权值比后面所有位数加起来还要高,所以不管下面怎么样,当前这一位能取就取。

#include<bits/stdc++.h>
#define PQG priority_queue < ll , vector < ll > , greater < ll > >
#define PQL priority_queue < ll , vector < ll > , less < ll > >
#define rep(i,s1,s2,s3) for(i = s1;i <= s2;i += s3)
#define r(i,s1,s2,s3) for(i = s1;i >= s2;i -= s3)
#define ULL_MAX numeric_limits < ull > :: max()
#define LL_MAX numeric_limits < ll > :: max()
#define rand_ srand(int(time(NULL)))
#define log(a,b) log(a) / log(b)
#define ull unsigned long long
#define NP next_pemurtation
#define sort stable_sort
#define INF 0x7f7f7f7f
#define ll long long 
using namespace std;
int id,n,ans,head[100001],s[100001];
struct node{
	int u,v,w,next; 
}e[100001 * 2];
void add(int u,int v,int w){
	e[++id].v = v;
	e[id].u = u;
	e[id].w = w;
	e[id].next = head[u];
	head[u] = id;
}
inline ll re(){
    ll x = 0,f = 1;
    char ch = getchar();
    while(ch < '0' || ch > '9'){
        if(ch == '-') f = -1; ch = getchar();
    }
    while(ch >= '0' && ch <= '9'){
        x = (x << 1) + (x << 3) + (ch ^ 48);
        ch = getchar();
    }
    x *= f;
    return x;
}
inline void write(ll x){
	if (x < 0){putchar('-'); x = -x;}
	ull y = 10, len = 1;
	while (y <= x){y *= 10; len++;}
	while (len--){y /= 10; putchar(x / y + 48); x %= y;}
}
void dfs(int u,int father){
	for(int i = head[u];i;i = e[i].next) if(e[i].v != father){
        s[e[i].v] = s[u] ^ e[i].w;
        dfs(e[i].v,u);
    }
}
struct Trie{
	int id = 0,trie[100001 * 31][2];
	void insert(int x){
		int i,u = 0;
		r(i,30,0,1){
			int s = x >> i & 1;
			if(!trie[u][s]) trie[u][s] = ++id;
			u = trie[u][s];
		}
	}
	int find(int x){
		int i,u = 0,ans = 0;
		r(i,30,0,1){
			int s = x >> i & 1;
			if(trie[u][!s]){
				ans += 1ull << i;
				u = trie[u][!s];
			}else u = trie[u][s];
		}
		return ans;
	}
}a; 
int main(){
	ios::sync_with_stdio(false); cin.tie(0); cout.tie(0);
	//freopen(".in","r",stdin);
	//freopen(".out","w",stdout);
	cin>>n;
	int i,u,v,w;
	rep(i,1,n - 1,1){
		cin>>u>>v>>w;
		add(u,v,w); add(v,u,w);
	}
	dfs(1,-1);
	rep(i,1,n,1) a.insert(s[i]);
	rep(i,1,n,1) ans = max(ans,a.find(s[i]));
	cout<<ans; 
	return 0;
} 
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值