洛谷P4407 电子词典

发布时间 2023-07-28 22:21:24作者: 最爱丁珰

读完这题我马上就想到了题解trie+dfs的爆搜解法,这种解法思维难度很低,算个模拟,很容易想到
但是我们稍微计算一下复杂度,就可以发现达到了\(1e8\)级别(\(26*20*20*1e4\),即对于每一个待查字符串(\(1e4\)),枚举每一个位置(\(20\)),每一个位置枚举26个字母(\(26\)),然后再在trie树上匹配(\(20\)))
害怕T(但从实际上来看,并不会T,因为远远达不了上界),加上此题是个紫题,于是我只能想别的解法
那我们一个一个操作来思考
对于第一个操作,我们枚举每一个待查字符串的删除的字母,然后再在trie树上匹配,复杂度为\(1e4*20*20\)
这里注意一个小细节,就是对于一个待查字符串,挨在一起的相同的字母只能删除一次(比如样例中的\(abcdd\)的最后两个\(dd\),在枚举时只用枚举一次就可以了,不然会重复计数;可以证明,只有这种情况才会导致重复计数)
对于第二个操作,我们反过来想,待查字符串添加了一个字母与已知字符串中的某一个匹配上了,是不是等价于这个已知字符串删除某一个字母然后与待查字符串匹配?于是我们就可以转换成第一个操作了,但是同样也需要注意第一个操作的那个小细节,只不过现在是对于已知字符串的注意了
本来我最初的想法是对于每一个已知字符串删除某一个字母后所能产生的不同的字符串都建立在同一颗trie树上,但这样会爆空间(一共有\(1e4\)个原始已知字符串,每一个已知字符串最多有\(20\)个字母,故能产生\(20\)个新字符串,所有新字符串一共有\(20*1e4\)个,即trie树上有\(20*1e4*20\)个节点,每个节点开\(26\)个子节点,故一共有\(20*1e4*26*20\)个int类型,大约400MB)
于是我转换了一下,我考虑一次只删除所有已知字符串的同一位置,然后建立一个trie树,然后在进行查询(有点不好描述,麻烦看下代码,注释详细)

    for(int i=1;i<=20;i++)//枚举每次删除的位置 
	{
	    init();//每次删除前记得将trie树清空 
		for(int j=1;j<=n;j++)
		if(l[j]<i) continue;//l[j]即第j个已知字符串的长度 
		else if(i==1||w[j][i-1]!=w[j][i-2])//小细节 
		newinsert(w[j],i);//建立trie树 
		for(int j=1;j<=m;j++)
		if(ans[j]==-1||L[j]<i) continue;//ans[j]==-1代表第j个待查字符串是已知字符串中的一个 
		else ans[j]+=check(q[j]);
	}

这样不重不漏,复杂度也不会超
对于第三个操作,我们想一下,对于一个已知字符串和一个待查字符串,如果待查字符串可以通过第三个操作来等同于已知字符串,我们设我们是通过改变待查字符串的第\(i\)个字母来达到已知字符串的,那么同时删除已知字符串和待查字符串的第\(i\)个字母是不是剩余的字符串就要相等了?所以这就是我们第三个操作的等价转换
我们还需要证明一个小结论,对于通过第三个操作完成的匹配,这一组匹配的已知字符串和待查字符串一定只有\(i\)这一个位置是不同的,这里建立在这个待查字符串不是已知字符串中的一个的前提下。这个结论比较简单,反证法即可
有了这个结论,我们也不需要再去注意第一个和第二个操作的细节了,因为一定不会重复的
综上所述,按照以上的解法去做这道题,我们就不会超时超空间

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=1e4+10;
int n,m;
int tot=1;
int trie[N*20][30],cnt[N*20],L[N],ans[N],l[N];
char w[N][30],q[N][30];
void insert(char *str)
{
	int p=1,len=strlen(str);
	for(int i=0;i<len;i++)
	{
		if(!trie[p][str[i]-'a']) trie[p][str[i]-'a']=++tot;
		p=trie[p][str[i]-'a'];
	}
	cnt[p]++;
}
int check(char *str)
{
	int p=1,len=strlen(str);
	for(int i=0;i<len;i++)
	if(!trie[p][str[i]-'a']) return 0;
	else p=trie[p][str[i]-'a'];
	return cnt[p];
}
void newinsert(char *str,int k)
{
	int p=1,len=strlen(str);
	for(int i=0;i<len;i++)
	{
		if(i==k-1) continue;
		if(!trie[p][str[i]-'a']) trie[p][str[i]-'a']=++tot;
		p=trie[p][str[i]-'a'];
	}
	cnt[p]++;
}
int solve(char *str,int k)
{
	int p=1,len=strlen(str);
	for(int i=0;i<len;i++)
	{
		if(i==k-1) continue;
		if(!trie[p][str[i]-'a']) return 0;
		p=trie[p][str[i]-'a'];
	}
	return cnt[p];
}
void init()
{
    tot=1;
	memset(trie,0,sizeof(trie));
	memset(cnt,0,sizeof(cnt));
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
	{
		scanf("%s",w[i]);
		l[i]=strlen(w[i]);
		insert(w[i]);
	}
	for(int i=1;i<=m;i++)
	{
		scanf("%s",q[i]);
		L[i]=strlen(q[i]);
		if(check(q[i])) ans[i]=-1;
	}
	for(int i=1;i<=20;i++)
	for(int j=1;j<=m;j++)
	if(ans[j]==-1||L[j]<i) continue;
	else if((i==1||q[j][i-1]!=q[j][i-2])) ans[j]+=solve(q[j],i);
	for(int i=1;i<=20;i++)//枚举每次删除的位置 
	{
	    init();//每次删除前记得将trie树清空 
		for(int j=1;j<=n;j++)
		if(l[j]<i) continue;//l[j]即第j个已知字符串的长度 
		else if(i==1||w[j][i-1]!=w[j][i-2])//小细节 
		newinsert(w[j],i);//建立trie树 
		for(int j=1;j<=m;j++)
		if(ans[j]==-1||L[j]<i) continue;//ans[j]==-1代表第j个待查字符串是已知字符串中的一个 
		else ans[j]+=check(q[j]);
	}
	for(int i=1;i<=20;i++)
	{
	    init();
		for(int j=1;j<=n;j++)
		if(l[j]<i) continue;
		else newinsert(w[j],i);
		for(int j=1;j<=m;j++)
		if(ans[j]==-1||L[j]<i) continue;
		else ans[j]+=solve(q[j],i);
	}
	for(int i=1;i<=m;i++) printf("%d\n",ans[i]);
    return 0;
}