字典树 - trie树

trie树,也称字典树,大家有没有想过你们在字典里查找一个单词是怎么样的。

首先,我们会查找第一个字符。

然后在第一个字符的范围内,查找第二个字符…以此类推

我们的字典树,就是这样的。

前置知识

字符串(都会?)

算法用途

快速搜索字符串

算法复杂度

时间

插入,删除,查询长度为 l e n \tt len len 的字符串都是 O ( l e n ) \tt O(len) O(len)

空间

n次插入,最大长度len,有x个不同字符: O ( m i n ( x × l e n × n , x l e n ) ) \tt O(min(x \times len \times n, x^{len})) O(min(x×len×n,xlen))

算法实现

插入

插入字符串 s t r \tt str str

我们从根开始,往下走,根的深度为 0 \tt 0 0,当前深度为 i \tt i i,我们看看当前节点有没有子节点 s t r [ i ] \tt str[i] str[i] s t r \tt str str 的第 i \tt i i 个字符的编号),有,继续往下走,没有,那就增加一个,然后继续走。直到执行完 s t r . s i z e ( ) \tt str.size() str.size()

查询

查询字符串str。

同理,我们从根开始,往下走,根的深度为 0 \tt 0 0,当前深度为 i \tt i i,我们看看当前节点有没有子节点 s t r [ i ] \tt str[i] str[i] s t r \tt str str 的第 i \tt i i个字符的编号),有,继续往下走,没有,就是没有这个字符串了

删除

删除字符串str。

同上,只是查找到后把只属于这个字符串的东西删了就行了。

算法优化

我们可以发现这算法时间复杂度很好,但是空间太大了。

所以我们考虑用一下奇怪的优化

正常的话要储存基本的字符串我们要每个节点 128 \tt 128 128 个儿子,这样空间复杂度很大。

但是,我们可以考虑把一个字符拆分成 2 \tt 2 2 个一次插入,这样一个字符串长度就为 2 × l e n \tt 2 \times len 2×len,然后每个节点儿子个数就为 16 \tt 16 16

因为一般情况下都是 128 × l e n × n \tt 128 \times len \times n 128×len×n 12 8 l e n \tt 128 ^ {len} 128len

那原来空间为 128 × l e n × n \tt 128 \times len \times n 128×len×n,现在变为 16 × 2 × l e n × n = 32 × l e n × n \tt 16 \times 2 \times len \times n = 32 \times len \times n 16×2×len×n=32×len×n

减少了 4 \tt 4 4 倍!当然你也可以考虑拆成4份 O ( 16 × l e n × n ) \tt O(16 \times len \times n) O(16×len×n)

代码

void add(string str)
{
	int p = 0;
	tree[p].cnt++;
	for(int i = 0; i < str.size(); i++)
	{
		int a = str[i] >> 4;
		if(tree[p].son[a])
		{
			p = tree[p].son[a];
		}
		else
		{
			if(sta.empty())
			{
				tree[p].son[a] = ++siz;
				p = siz;
			}
			else
			{
				tree[p].son[a] = sta.top();
				p = tree[p].son[a];
				sta.pop();
			}
		}
		tree[p].cnt++;
		a = str[i] & 0x0f;
		if(tree[p].son[a])
		{
			p = tree[p].son[a];
		}
		else
		{
			if(sta.empty()) 
			{
				tree[p].son[a] = ++siz;
				p = siz;
			}
			else
			{
				tree[p].son[a] = sta.top();
				p = tree[p].son[a];
				sta.pop();
			}
		}
		tree[p].cnt++;
	}
}
void del(string str)
{
	int p = 0;
	tree[p].cnt--;
	for(int i = 0; i < str.size(); i++)
	{
		int a = str[i] >> 4;
		if(tree[p].son[a])
		{
			int nowp = p;
			p = tree[p].son[a];
			tree[p].cnt--;
			if(!tree[p].cnt)
			{
					tree[nowp].son[a] = 0;
				sta.push(p);
			}
		}
		else
		{
			break;
		}
		a = str[i] & 0x0f;
		if(tree[p].son[a]) 
		{
			int nowp = p;
			p = tree[p].son[a];
			tree[p].cnt--;
			if(!tree[p].cnt)
			{
				tree[nowp].son[a] = 0;
				sta.push(p);
			}
		}
		else
		{
			break;
		}
	}
}

例题

P2580   于是他错误的点名开始了 \color{3498DB}{\texttt{P2580 于是他错误的点名开始了}} P2580 于是他错误的点名开始了

P4551   最长异或路径 \color{9D3DCF}{\texttt{P4551 最长异或路径}} P4551 最长异或路径

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值