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;
}

未完待续...

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值