trie树(二)进阶

一些相对比较难的题。。。

First! G

大意:
给定一些字符串,要求你确定在改变字符优先级的情况下,某些字符串是否能成为字典序最小的字符串

思路:

1.某个字符串能够作为字典序最小的字符串,那么它的首字母一定是最小的。

2.并且对于所有与它有相同前缀的字符串,该字符串的下一个字母的字典序也一定是比另一个字符串的下一个字母的字典序要小。

3.另外,如果如果有一个字符串是它的前缀,那么这个前缀一定会比它有更小的字典序,所以这种情况下该字符串就是失败的。

以上三种情况中,1 3都是很好处理的,我们再仔细看一看情况2

根据情况二下的规则,每次我们确定两个字母的优先关系时,就给它们连一条有向边,从字典序较小的连向较大的。这样最后我们就会得到一张有向图。判断这个图是否合理很简单,只要没有环就是合理的,因为如果有环的话,两个字母的优先关系就会产生矛盾。所以我们只要多加一个判环处理即可,这里可以用拓扑排序

code

#include<bits/stdc++.h>
using namespace std;
#define ll int
#define endl '\n'
const ll N=3e5+10;
ll tr[N][30];
ll idx=0;
ll n,p,ans;
string s[30010];
bool vis[30010];
bool mp[30][30]; 
bool init[30];
ll du[30];//入度 
bool en[N];//字符串结尾 
void insert(string s)
{
	p=0;
	for(int i=0;i<s.size();++i)
	{
		ll id=s[i]-'a'+1;
		if(!tr[p][id]) tr[p][id]=++idx;
		p=tr[p][id];
	}
	en[p]=1;
}
bool find(string ss)
{
	ll tag=ss[0]-'a'+1;
	for(int i=1;i<=26;++i)
	{
		if(i==tag) continue;
		if(!init[i]) continue;
		if(mp[tag][i]) continue;
		mp[tag][i]=1;
		du[i]++;
	}
	p=0;
	for(int i=0;i<ss.size();++i)
	{
		ll id=ss[i]-'a'+1;
		for(int j=1;j<=26;++j)
		{
			if(id==j) continue;
			if(!tr[p][j]) continue;
			if(mp[id][j]) continue;
			mp[id][j]=1;//i<j
			du[j]++;
		}
		p=tr[p][id];
		if(en[p]&&i!=ss.size()-1) return 0;//如果发现有一个字符串是它的前缀,直接返回非法
	}
	return 1;
}
bool topu()//拓扑排序判环
{
	queue<ll> q;
	for(int i=1;i<=26;++i) if(du[i]==0) q.push(i);//取入度为0的加入队列
//	for(int i=1;i<=26;++i) cout<<(char)('a'+i-1)<<du[i]<<" ";
//	cout<<endl;
	while(!q.empty())
	{
		ll ty=q.front();
		q.pop();
		for(int i=1;i<=26;++i)
		{
			if(i==ty) continue;
			if(!mp[ty][i]) continue;
			mp[ty][i]=0;
			du[i]--;
			//cout<<"ID="<<id<<' '<<(char)('a'+ty-1)<<" "<<(char)('a'+i-1)<<endl;
			if(du[i]==0) q.push(i);
		}
	}
	for(int i=1;i<=26;++i) if(du[i]) return 0;
	return 1;
}
int main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;++i)
	{
		cin>>s[i];
		init[s[i][0]-'a'+1]=1;
		insert(s[i]);
	}
	for(int i=1;i<=n;++i)
	{
		for(int j=1;j<=26;++j) for(int k=1;k<=26;++k) mp[j][k]=0;
		for(int j=1;j<=26;++j) du[j]=0;
		if(find(s[i])==0) continue;
		if(topu()) vis[i]=1,ans++;
	}
	cout<<ans<<endl;
	for(int i=1;i<=n;++i) if(vis[i]) cout<<s[i]<<endl;
	return 0;
}

病毒检测

大意:
给定一个模式串和一系列匹配串,求无法与其匹配的字符串数

模式中 * 的意思是可以匹配上0个或任意多个字符,而 ? 的意思是匹配上任意一个字母。

思路:

思路是trie树上跑dfs

如果没有两个通配符的话,我们要做字符串匹配,只要建好trie树,按模式串的字符顺序遍历一下trie树就可以了。现在有了两个通配符,我们就有了多个可能性来匹配更多的字符串,所以会考虑到dfs。

现在讨论一下要如何做dfs

dfs(a,p)a:模式串的位置 p:trie树的位置

1.如果在模式串上遍历到的是一个字母(ATCG中的一个),我们只要按普通规则继续往下递归就可以了 dfs(a+1,tr[p][i])

2.如果遍历到的是一个?,那么就是取ATCG中的任意字符往下递归(只要树上有到它们的路径)

for(int i=1;i<=4;++i)
		{
			if(tr[p][i]) dfs(a+1,tr[p][i]);
		}
		return;

3.如果是遍历到了一个“*”,我们还有两个情况要讨论:
           一:让它作为空串,也就是跳过它,那么a+1,p不变,也就是dfs(a+1,p);

           二:它会去匹配一个字符串,我们可以表示成“ ?+* ”的形式,这样就保证了*可以一直往下匹配,同时我们也可以一个字符一个字符去匹配。

for(int i=1;i<=4;++i)
		{
			if(!tr[p][i]) continue;
			//?+*
			dfs(a+1,tr[p][i]);
			dfs(a,tr[p][i]);
		}

另外,这里六个字符我们可以用一个函数来映射成数字

code

#include<bits/stdc++.h>
using namespace std;
#define ll int
#define endl '\n'
const ll N=5e5+10;
ll tr[N][10];
ll cnt[N];
ll idx=0;
ll n;
ll len=0;
ll ans=0;
bitset<500007> vis[1010];
string vir,s;
ll get_id(char x)
{
	if(x=='A') return 1;
	if(x=='G') return 2;
	if(x=='T') return 3;
	if(x=='C') return 4;
	if(x=='*') return 5;
	if(x=='?') return 6;
}
void insert(string s)
{
	ll p=0;
	for(int i=0;i<s.size();++i)
	{
		int id=get_id(s[i]);
		if(!tr[p][id]) tr[p][id]=++idx;
		p=tr[p][id];
	}
	cnt[p]++;
}
void dfs(ll a,ll p)
{
	if(a>=len)
	{
		ans+=cnt[p];
		cnt[p]=0;//清空防止多加 
		return;
	}
	if(vis[a][p]) return;
	vis[a][p]=1;//标记 
	int id=get_id(vir[a]);
	if(id>=1&&id<=4)
	{
		if(tr[p][id])
	    dfs(a+1,tr[p][id]);	
	    return;
	} 
	if(id==6)
	{
		for(int i=1;i<=4;++i)
		{
			if(tr[p][i]) dfs(a+1,tr[p][i]);
		}
		return;
	}
	if(id==5)
	{
		dfs(a+1,p);
		for(int i=1;i<=4;++i)
		{
			if(!tr[p][i]) continue;
			//?+*
			dfs(a+1,tr[p][i]);
			dfs(a,tr[p][i]);
		}
	}
}
int main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
	cin>>vir;
	len=vir.size();
	cin>>n;
	for(int i=1;i<=n;++i) cin>>s,insert(s);
	//for(int i=1;i<=idx;++i) cout<<i<<' '<<cnt[i]<<endl;
	dfs(0,0);
	cout<<n-ans<<endl;
}

Type Printer

大意:
有一个打印队列,每次可以执行以下操作:

往末尾添加一个字母/删除一个末尾字母/打印(输出所有字母,但不会删除它们)

现在要求打印所有字符串(顺序任意),求最小操作数

ps:打印完最后一个字符串后不要求清空打印队列

思路:
不难想到,最长的字符串肯定是最后打印,因为它就不用清空了,就可以省下很多操作次数

那么我们的打印规则就是:尽可能利用好每一个前缀,也就是把每一个前缀的对应的所有字符串都打印完再打印别的字符串,同时要最后打印那个最长的字符串

那么自然就还是会想到dfs

最后打印最长的字符串,可以在建trie树时给这个字符串经过的链打上标记

至于普通的打印过程,就是深度优先遍历以下就好了

具体看代码吧

有一个点要注意,就是当你发现当前已经到达某一个字符串的结尾时,ans++,但是不能直接return,因为有可能其它字符串的起点刚好就是这个字符串的结尾,所以这时我们应该继续dfs下去,而不是返回。这一点根其它题目不太一样,也让我wa了好多发。。。

#include<bits/stdc++.h>
using namespace std;
#define ll int
const ll N=25010;
ll tr[N*20][30];
ll idx=0;
ll n;
string s[N];
ll ma=0;
ll tag=0; 
bool ne[N*20];//对应最长序列,要最后访问
string ans="";
bool en[N*20];//单词结尾的地方 
char mp[N*20];
ll sd=0;
void insert(ll k)
{
	ll p=0;
	string ss=s[k];
	for(int i=0;i<ss.size();++i)
	{
		ll id=ss[i]-'a'+1;
		if(!tr[p][id]) tr[p][id]=++idx;
		p=tr[p][id];
		//num[p]++;//有多少单词经过这里 
		mp[p]=ss[i];//反向映射 
		if(k==tag) ne[p]=1;//长度最长的单词经过这里 
	}
	en[p]=1;//结尾标记 
}
void dfs(ll p)
{
	if(en[p])
	{
		ans+='P';
		sd++;
        //这里不能直接返回。。。
	}
	if(sd==n)
	{
		cout<<ans.size()<<endl;
		for(int i=0;i<ans.size();++i) cout<<ans[i]<<endl;
		return;
	}
	for(int i=1;i<=26;++i)
	{
		if(ne[tr[p][i]]) continue;//先跳过最长的单词 
		if(!tr[p][i]) continue;
		ans+=mp[tr[p][i]];
		dfs(tr[p][i]);
		ans+='-';//该前缀后面的东西已经打印好了,就可以把这个字符删掉了
	}
	for(int i=1;i<=26;++i)//现在来处理最长的字符串
	{
		if(!tr[p][i]) continue;
		if(!ne[tr[p][i]]) continue;
		ans+=mp[tr[p][i]];
		dfs(tr[p][i]);
		ans+='-';
		//break;
	}
}
int main()
{ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;++i)
	{
		cin>>s[i];
		ll len=s[i].size();
		if(len>ma)
		{
			ma=len;
			tag=i;/标记最长
		}
	}
	for(int i=1;i<=n;++i)
	{
		insert(i);
	}
	dfs(0);
	return 0;
}

 11.9 来更新啦

电子词典

大意:
给定字符串的一个操作定义:

1.删除某一个字母

2.在任意位置添加一个字母

3.替换某一个字母

然后定义两个字符串A到B的距离为A至少要经过几次操作才能与B相同

现在给出一个字符串集,然后对于每一次询问,对一个字符串,在字符串集中找到与其距离为1的字符串的个数

思路:

不难发现,这个问题其实跟上一题差不多,都是trie树上进行dfs。

接下来考虑dfs的具体实现:

不妨设dfs(int p,int id,int fl) 参数分别代表:节点,字符串匹配到的位置,字符串是否进行过操作

1.删除字母:直接跳过该字母 if(id<s.size()) dfs(p,id+1,1);

2.替换和增加字母可以放在一起做:

for(int i=0;i<26;++i)
        {
            if(tr[p][i]) 
            {
                dfs(tr[p][i],id,1);
                if(i!=ide) dfs(tr[p][i],id+1,1);
            }
        } 

注意替换后的字母不能跟原字母相同,所以要判一下

然后就差不多了

#include<bits/stdc++.h>
using namespace std;
#define ll int 
#define endl '\n'
const int N=1e4+10;
int n,m;
string s;
ll tr[10010*22][30];
ll vis[N*22];
ll loc[N*22],used[N*22];//位置 被标记过 
ll idx=0;
void add(string s)
{
	ll p=0;
	for(int i=0;i<s.size();++i)
	{
		int id=s[i]-'a';
		if(!tr[p][id]) tr[p][id]=++idx;
		p=tr[p][id];
	}
	vis[p]=1;
}
int flag=0;
ll vit=0;
void dfs(ll p,ll id,ll fl)
{
	//节点,字符串位置,是否进行过操作
	if(vis[p]&&id==s.size()&&fl==0)
	{
		flag=1;
		return;
	} 
	if(vis[p]&&id==s.size()&&fl)
	{
		if(used[p]) return;
		vit++;
		loc[vit]=p;
		used[p]=1;//这个字符串之前已经被匹配过了 
		return;
	}
	int ide=s[id]-'a';//哪一个字符 
	if(fl==0)//可以进行修改操作 
	{
		//跳过
		if(id<s.size()) dfs(p,id+1,1);
		for(int i=0;i<26;++i)
		{
			if(tr[p][i]) 
			{
				dfs(tr[p][i],id,1);
			    if(i!=ide) dfs(tr[p][i],id+1,1);
			}
		} 
	}
	if(id>=s.size()) return;
	//不修改:
	if(tr[p][ide]) dfs(tr[p][ide],id+1,fl); 
}
void solve()
{
	cin>>n>>m;
	for(int i=1;i<=n;++i)
	{
		cin>>s;
		add(s);
	}
	for(int i=1;i<=m;++i)
	{
		vit=0;
		cin>>s;
		
		dfs(0,0,0);
		
		if(flag) cout<<-1<<endl;
		else cout<<vit<<endl;
		while(vit)
		{
			used[loc[vit]]=0;
			vit--;
		}
		vit=0;flag=0;
	}
}
int main()
{
	ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
	solve();
	return 0;
}

未完待续...

<think>嗯,用户想了解Trie的高级用法和进阶使用方法,需要结合数据结构和算法示例。首先,我得回忆一下Trie的基本概念,然后思考有哪些高级应用场景。可能包括压缩Trie、双数组Trie、AC自动机,或者是结合其他数据结构的优化方法。 用户提到了高级用法,所以不能只讲基础。比如,压缩Trie可以节省空间,比如Radix Tree或者Patricia Trie。双数组Trie(Double-Array Trie)也是一种优化,用于提高查询效率和减少内存占用。AC自动机是基于Trie的结构,用于多模式匹配,比如敏感词过滤。 然后,可能需要举一些具体的例子,比如如何用双数组Trie实现高效存储,或者用AC自动机处理多个关键词的匹配。此外,应用场景方面,比如搜索引擎的自动补全、拼写检查、路由表的最长前缀匹配,这些都是Trie的高级应用。 还要注意用户提到的引用,比如引用[3]提到关于next数组和base数组的关系,这可能和双数组Trie的实现有关,需要正确引用。同时,用户可能希望看到代码示例,所以可能需要给出Python或伪代码的实现片段,比如AC自动机的节点结构或者双数组的构建步骤。 另外,用户的问题生成部分需要相关的问题,比如如何优化Trie的空间效率,或者Trie在实时系统中的应用,这些都需要在回答后列出相关问题。 需要确保回答结构清晰,分点说明每个高级用法,包括原理、应用和示例。同时,引用相关的文献,比如引用[3]关于双数组Trie的部分。还要检查LaTeX格式是否正确,所有数学表达式用$...$,独立公式用$$...$$,比如在讲解路径压缩时可能有公式。 最后,要验证每个例子的正确性,比如AC自动机的构建步骤是否正确,双数组Trie的base和check数组如何工作,确保示例代码没有语法错误,并且符合用户的要求。</think>### Trie高级用法解析 Trie(前缀)通过共享前缀的特性实现高效字符串检索。以下是其进阶应用方法和优化策略: #### 一、空间优化技术 1. **压缩Trie(Radix Tree)** - 合并单分支路径,将连续单子节点合并为边标签 - 存储结构示例: $$节点结构:(边标签, 子节点指针)$$ - 应用场景:路由表最长前缀匹配[^3] 2. **双数组Trie(Double-Array Trie)** - 使用`base`和`check`数组代替指针 ```python base = [0]*SIZE # 存储状态转移基数 check = [-1]*SIZE # 验证前驱状态 # 插入字符c时:t = base[s] + c; check[t] = s ``` - 内存效率提升50%以上[^3] #### 、功能扩展应用 1. **AC自动机(Aho-Corasick算法)** ```python class ACNode: def __init__(self): self.children = {} self.fail = None self.output = [] # 构建失败指针实现多模式匹配 ``` - 应用实例:敏感词过滤系统(每秒处理百万级文本) 2. **动态权值存储** - 节点附加权重信息,实现带权字符串排序 $$节点数据结构:\{字符→子节点指针, 权重值, 热词标记\}$$ #### 三、混合结构优化 1. **Trie+哈希表复合结构** - 高层使用哈希快速定位 - 深层使用Trie存储长后缀 - 查询时间复杂度:$O(1)$(首字符哈希)+ $O(n)$(后缀匹配) 2. **Trie+倒排索引** ```python trie_node = { 'children': {}, 'doc_ids': set() # 存储包含该前缀的文档ID } ``` - 应用场景:搜索引擎自动补全建议 #### 四、高级应用场景 1. **生物信息学中的基因序列比对** - 使用4进制Trie存储DNA序列(A/T/C/G) - 实现$O(n)$时间复杂度的模式匹配 2. **实时日志分析系统** - 滑动窗口+Trie结构实现流数据关键词统计 - 内存占用降低约40%[^1]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值