Trie树及其应用

Nick_zheng blog

$Trie$树及其应用

特别声明: 以下仅为个人直观理解, 并不都正确严谨


$Trie$, 也叫字典树, 是一种按照前缀整理字符串(或其他广义上的串)的数据结构, 可以在 $O(l)$ 内往$Trie$树中加入一个字符串或查询qaq. 当然, $Trie$树也可以结合$dp$, $dfs$等算法实现高级功能, 也可以有$hash$“类似"的为字符串标号的功能qaq.

下面我将给出Trie树的模板.很容易可以直观理解, 详细参考注释qaq. (此题为P5149 会议座位, 结合了一个逆序对的求解)

#include<bits/stdc++.h>
//#define int long long
using namespace std;
const int N=5e5+10;
int trie[N][60];//第一位为当前节点编号, 第二维为字符集(可能包含的字符)
int mp(char c)//将字符转化为数字
{
    if(c>='a')//大小写分类讨论
    {
        return c-'a'+26;
    }
    return c-'A';
}
int id[N];//类似hash, 为字符串标号(通常如果不标号, 则需要用一个vis数组来记录当前节点是否为字符串的结尾)
int cnt=0;//记录字符串个数(在这个题里等于n)
int tot=0;//记录Trie树节点总数
void insert(string s)//插入
{
	int len=s.size();
	int p=0;
	for(int i=0;i<len;i++)//O(len)实现
	{
		int c=mp(s[i]);//当前字符(转化后)
		if(!trie[p][c])//如果不存在节点
		{
			trie[p][c]=++tot;//则直接插入一个
		}
		p=trie[p][c];//跳到下一个节点
	}
	if(!id[p])//如果没有编号过, 则编号
	{
		id[p]=++cnt;
	}
}
int tr[N];//树状数组, 在这里不解释了
int find(string s)//查询函数
{
	int len=s.size();
	int p=0;
	for(int i=0;i<len;i++)//O(len)
	{
		int c=mp(s[i]);//同理
		if(!trie[p][c])//如果不存在, 则认为当前串在trie中不存在
		{
			return 0;
		}
		p=trie[p][c];//跳到下一个节点
	}
	return id[p];//返回编号
}
int lowbit(int x)//一下三个函数为树状数组实现, 在这里不介绍
{
	return x&(-x);
}
int n;
void modify(int p,int v)
{
	for(int i=p;i<=cnt;i+=lowbit(i))
	{
		tr[i]+=v;
	}
}
int query(int p)
{
	int res=0;
	for(int i=p;i;i-=lowbit(i))
	{
		res+=tr[i];
	}
	return res;
}
signed main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		string t;
		cin>>t;
		insert(t);//插入t到Trie中
	}
	long long ans=0;//求逆序对的和
	for(int i=1;i<=n;i++)
	{
		string t;
		cin>>t;
		int tmp=find(t);//查询t的编号
		ans+=i-query(tmp)-1;//逆序对处理
		modify(tmp,1);//逆序对处理
	}
	printf("%lld\n",ans);
    return 0;
}

以上就是$Trie$树的基本模板.(其实本质来看就是记录了一下孩子节点的编号qaq)


接下来分享几个$Trie$的综合运用.


1. $Trie$结合$dfs$: P2536 [AHOI2005] 病毒检测

  • 难点: 如何处理串中的 *

    考虑使用$dfs$, 记$l$为当前病毒模板片段的下标, $now$为当前在$Trie$树上的编号 其实我们可以考虑把 *号拆分为两种情况:

    1. 看作空串, 直接$l+1$即可
    2. 看作 $?+*$, 此时$l$不变 (分割成子问题的化归思想)

    这样我们就可以用$dfs$实现比较暴力的搜索了qaq.

    但是这样会$TLE$, 我们可以使用记忆化来优化搜索.

    代码如下qaq:

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=500+10;
int trie[N*N][6];//数组别开小了
map<char,int> mp;//转化关系
int tot=0;
bool isend[N*N];//是否为字符串结尾
int ans=0;//病毒数量
void insert(string t)//标准的Trie模板
{
	int p=0;
	for(int i=0;i<(int)t.size();i++)
	{
		int c=mp[t[i]];
		if(!trie[p][c])
		{
			trie[p][c]=++tot;
		}
		p=trie[p][c];
	}
	isend[p]=1;
}
bool find(string t)//标准模板
{
	int p=0;
	for(int i=0;i<(int)t.size();i++)
	{
		int c=mp[t[i]];
		if(!trie[p][c])
		{
			return 0;
		}
		p=trie[p][c];
	}
	return isend[p];
}
string s;
bool vis[N][N*N];//记忆化: 如果搜过当前状态则直接返回
void dfs(int l,int now)//l, now解释同上
{
	if(vis[l][now])//记忆化
	{
		return;
	}
	vis[l][now]=1;
	if(l==(int)s.size())//如果搜完整个串了(当前串即为一个病毒)
	{
		if(isend[now])//如果当前的串是我们需要判定的
		{
			ans++;//病毒数量+1
		}
		return;
	}
	if(s[l]=='A'||s[l]=='C'||s[l]=='T'||s[l]=='G')
	{
		if(trie[now][mp[s[l]]])//如果有这条路在Trie上
		{
			dfs(l+1,trie[now][mp[s[l]]]);
		}
	}
	else if(s[l]=='?')//问号的情况, 直接转化成四种可能
	{
		if(trie[now][mp['A']])
		{
			dfs(l+1,trie[now][mp['A']]);
		}
		if(trie[now][mp['C']])
		{
			dfs(l+1,trie[now][mp['C']]);
		}
		if(trie[now][mp['G']])
		{
			dfs(l+1,trie[now][mp['G']]);
		}
		if(trie[now][mp['T']])
		{
			dfs(l+1,trie[now][mp['T']]);
		}
	}
	else//星号的情况, 考虑问号的四种+空串的一种情况
	{
		if(trie[now][mp['A']])
		{
			dfs(l,trie[now][mp['A']]);//问号, 注意当前l不加一(仍然是有一个星号)
		}
		if(trie[now][mp['C']])
		{
			dfs(l,trie[now][mp['C']]);
		}
		if(trie[now][mp['G']])
		{
			dfs(l,trie[now][mp['G']]);
		}
		if(trie[now][mp['T']])
		{
			dfs(l,trie[now][mp['T']]);
		}
		dfs(l+1,now);//空串
	}
}
signed main()
{
	cin>>s;
	mp['A']=0;//记录一下转化关系
	mp['C']=1;
	mp['T']=2;
	mp['G']=3;
	mp['*']=4;
	mp['?']=5;
	int n;
	scanf("%lld",&n);
	for(int i=1;i<=n;i++)
	{
		string t;
		cin>>t;
		insert(t);//正常插入
	}
	dfs(0,0);//别忘了调用qaq
	printf("%lld\n",n-ans);//输入不是病毒的
    return 0;
}

以上就是$Trie$上$dfs$的一个简单实现qaq, 其实主要还是$dfs$的处理, $Trie$看做作为一个优化.


2. $Trie$树结合$DP$