字典树粗学

什么是字典树?
字典树使用边来表示字母,从根节点到树上某一结点的路径表示了一个字符串。

字典树要解决什么?
查找字符串,尤其是查找一个字符串是否为另一个字符串的前缀,以及前缀的长度

字典树的特点
1.使用来表示字母
2.有相同字母的单词公用前缀节点,每个节点最多有N个子节点(N为该字符串包含的字符种类
3.根节点为空
4.每个单词结束时,在该节点添加标记flag以表示这段路径是一个单词而非前缀

字典树的基本操作

  1. 插入——insert
    对于一个单词,从左往右进行扫描,如果一个字母在其相应根节点1下未出现,则插入该字母,否则,沿着字典树向下,查看单词的下一个字母。
    字母该插入在哪个位置呢?
    我们设置数组trie[i][j]=k,表示编号为i的节点的第j个孩子编号为k,节点编号由插入的顺序决定
    这里假设先后插入两个单词cat,car
    未插入时
    在这里插入图片描述
    插入单词cat,依次插入c,a,t,所有字母在其相应根节点下都未出现

![在这里插入图片描述](https://img-blog.csdnimg.cn/bed9463865e64f72aa617da1edd67e86.png

插入单词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;//完成插入操作后,进行标记,表明这是一个完整的单词
}
  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
实际上就是上面题目的简化版本,更适合当例题一点(


  1. 关于相应根节点:首字母的根节点是整棵树的根节点,不是首字母则是遍历至上一个字母时对应的节点 ↩︎

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值