NOIP2020 T2 字符串匹配题解

首先考虑O(n^3)的暴力怎么写。

显然,可以枚举字符串 A A A+ B B B的右端点,左端点显然是1,暴力判断是否能与后面的字符构成循环节,对于满足

k ∗ ( A + B ) + C = k*(A+B)+C= k(A+B)+C= 整个字符串 ( k ∈ Z ) (k \in Z) (kZ)

的情况暴力枚举 A A A, B B B分界点,对于 L ( x ) ≤ R ( x ) L(x)\le R(x) L(x)R(x)的情况统计答案即可, L ( x ) L(x) L(x) 1 1 1 n n n出现奇数次的字符数量, R ( x ) R(x) R(x) x x x n n n出现奇数次的字符数量。

其实这样由于 A + B A+B A+B很难找到循环节,均摊下来时间复杂度在随机数据下远小于 O ( n 3 ) O(n^{3}) O(n3),可以拿到 48 p t s 48pts 48pts了,美滋滋

把暴力贴上来吧:
#include<bits/stdc++.h>
#define LL long long 
#define N (1<<20)+50
using namespace std;

LL ans;
int t,len;
char a[N];
int toa[30],toc[30],jia[N],jic[N];

inline int qr()
{
	char a=0;int x=0,w=1;
	while(a<'0'||a>'9'){if(a=='-')w=-1;a=getchar();}
	while(a<='9'&&a>='0'){x=(x<<3)+(x<<1)+(a^48);a=getchar();}
	return x*w;
}

int main()
{
	t=qr();
	while(t--)
	{
		scanf("%s",(a+1));
		len=strlen(a+1);
		memset(toa,0,sizeof(toa));
		memset(toc,0,sizeof(toc));
		memset(jic,0,sizeof(jic));
		memset(jia,0,sizeof(jia));
		ans=0;
		for(register int i=1;i<=len;i++)//计算L(i)
		{
			toa[a[i]-'a']++;
			toa[a[i]-'a']&1 ? jia[i]=jia[i-1]+1 :jia[i]=jia[i-1]-1;
		}
		for(register int i=len;i>=1;i--)//计算R(i)
		{
			toc[a[i]-'a']++;
			toc[a[i]-'a']&1 ? jic[i]=jic[i+1]+1 :jic[i]=jic[i+1]-1;
		}
		for(register int i=2;i<len;i++)
		{
			int flag=1;
			for(register int k=1;k<i;k++)//特判k=1的情况
				if(jia[k]<=jic[i+1])
					ans++;
			for(register int j=2;j*i<len;j++)//枚举循环次数
			{
				int le=(j-1)*i+1;
				int re=j*i;
				for(register int k=le;k<=re;k++)//判断循环节
					if(a[k]!=a[k-i])
					{
						flag=0;
						break;
					}
				if(flag)//循环成立枚举A,B分界点统计次数
				{
					for(register int k=1;k<i;k++)
						if(jia[k]<=jic[re+1])
							ans++;
				}
				else
					break;//循环不成立退出
			}
		}
		printf("%lld\n",ans);
		continue;
	}
	return 0;
}

下面考虑优化时间复杂度。

前置知识
  • 字符串hash

没了

我们可以想到,时间主要浪费在以下两点:
  • 在查找循环节时暴力匹配显然不是一个明智之举
  • 查找 A A A, B B B分界时单一操作重复多次。
那么考虑优化:

显然,一个字符串出现奇数次的字符的数量 ≤ \le 26 26 26 废话,那么可以
维护一个类似前缀和的东西 R R R( i i i, j j j)表示从 1 − i 1-i 1i奇数次的字符的数量满足 ≤ \le j j j的位置,用 L L L( i i i)表示从 i − n i-n in奇数次的字符的数量。

于是,对可成立的A+B字符串判断复杂度变成了预处理 O O O( 27 n 27n 27n),查询 O O O( 1 1 1)。

其次,可以用字符串哈希或KMP判循环节,将判循环节复杂度优化为调和级数级即 O O O( n n n ln ⁡ \ln ln n n n)。
加上这两个优化后复杂度大概在 O ( T ( 27 n + n ln ⁡ n ) ) O( T(27n+n \ln n) ) O(T(27n+nlnn))

喜闻乐见的 Code :
#include<bits/stdc++.h>
#define LL long long 
#define ULL unsigned long long
#define N (1<<20)+50
using namespace std;

LL ans;
int t,len;
char a[N];
int sm[N][27],sum[N];//分别对应题解中的L(i,j),R(i)
ULL Hash[N],jc[N],p=13331;//hash采取p进制,不过最好用双模数
//因为我太懒了,用了单模数哈希QAQ
inline int qr()//平平无奇的快读
{
	char a=0;int x=0,w=1;
	while(a<'0'||a>'9'){if(a=='-')w=-1;a=getchar();}
	while(a<='9'&&a>='0'){x=(x<<3)+(x<<1)+(a^48);a=getchar();}
	return x*w;
}

inline bool check(int l,int r,int L,int R)//字符串哈希判断相等
{
	ULL hash1=(Hash[r]-Hash[l-1]*jc[r-l+1]);
	ULL hash2=(Hash[R]-Hash[L-1]*jc[R-L+1]);
	if(hash1==hash2)
		return 1;
	return 0;
}

int main()
{
	t=qr();
	jc[0]=1;
	for(register int i=1;i<=(1<<20)+5;i++)
		jc[i]=jc[i-1]*p;//处理p^i
	while(t--)
	{
		scanf("%s",(a+1));
		len=strlen(a+1);
		ans=0;
		int opk[27]={},val=0;
		for(register int i=1;i<=len;i++)
			Hash[i]=(Hash[i-1]*p+a[i]);//计算hash值
		for(register int i=1;i<=len;i++)//预处理L(i,j)
		{
			for(register int j=0;j<=26;j++)
				sm[i][j]=sm[i-1][j];//首先继承i-1时的前缀和
			opk[(int)(a[i]-'a')+1]++;
			opk[(int)(a[i]-'a')+1]&1?val++:val--;
			sm[i][val]++;//加上统计的最新答案
		}//这时sm(i,j)表示的仅为 1~i 	中出现奇数次的字符数量=j的数量,于是需要再做一下前缀和
		for(register int i=1;i<=len;i++)
			for(register int j=1;j<=26;j++)
				sm[i][j]+=sm[i][j-1];//现在sm(i,j)表示的就是 1~i 中出现奇数次的字符数量<=j的数量了
		val=0;
		memset(opk,0,sizeof(opk));
		for(register int i=len;i>=1;i--)//预处理R(i,j)
		{
			opk[(int)(a[i]-'a')+1]++;
			opk[(int)(a[i]-'a')+1]&1?val++:val--;
			sum[i]=val;
		}//没啥可说的
		for(register int i=2;i<len;i++)
			for(register int j=1;j*i<len;j++)//开始判循环节
			{
				int re=j*i+1;
				if(check(1,i,i*(j-1)+1,j*i))//如果循环节成立
					ans+=sm[i-1][sum[re]];//统计答案
				else
					break;//不成立直接退出即可
			}
		printf("%lld\n",ans);
		continue;
	}
	return 0;
}

Update:

发现 R R R( i i i, j j j)可以边进行计算边求,优化了一些常数(不开O2有96分,C++11就过了),代码如下:

#include<bits/stdc++.h>
#define LL long long 
#define ULL unsigned long long
#define N (1<<20)+50
using namespace std;

LL ans;
int t,len;
char a[N];
int sm[27],sum[N];//分别对应题解中的L(i,j),R(i)
ULL Hash[N],jc[N],p=13331;//hash采取p进制,不过最好用双模数
//因为我太懒了,用了单模数哈希QAQ
inline int qr()//平平无奇的快读
{
	char a=0;int x=0,w=1;
	while(a<'0'||a>'9'){if(a=='-')w=-1;a=getchar();}
	while(a<='9'&&a>='0'){x=(x<<3)+(x<<1)+(a^48);a=getchar();}
	return x*w;
}

inline bool check(int l,int r,int L,int R)//字符串哈希判断相等
{
	ULL hash1=(Hash[r]-Hash[l-1]*jc[r-l+1]);
	ULL hash2=(Hash[R]-Hash[L-1]*jc[R-L+1]);
	if(hash1==hash2)
		return 1;
	return 0;
}

int main()
{
	t=qr();
	jc[0]=1;
	for(register int i=1;i<=(1<<20)+5;i++)
		jc[i]=jc[i-1]*p;//处理p^i
	while(t--)
	{
		scanf("%s",(a+1));
		len=strlen(a+1);
		ans=0;
		int opk[27]={},val=0;
		for(register int i=1;i<=len;i++)
			Hash[i]=(Hash[i-1]*p+a[i]);//计算hash值
		val=0;
		for(register int i=len;i>=1;i--)//预处理R(i,j)
		{
			opk[(int)(a[i]-'a')+1]++;
			opk[(int)(a[i]-'a')+1]&1?val++:val--;
			sum[i]=val;
		}//没啥可说的
		val=0;
		memset(sm,0,sizeof(sm));
		memset(opk,0,sizeof(opk));
		for(register int i=2;i<len;i++)
		{
			opk[(int)(a[i-1]-'a')+1]++;
			opk[(int)(a[i-1]-'a')+1]&1?val++:val--;
			for(register int j=val;j<=26;j++)
				sm[j]++;//求R(i,j)
			for(register int j=1;j*i<len;j++)//开始判循环节
			{
				int re=j*i+1;
				if(check(1,i,i*(j-1)+1,j*i))//如果循环节成立
					ans+=sm[sum[re]];//统计答案
				else
					break;//不成立直接退出即可
			}
		}
			
		printf("%lld\n",ans);
		continue;
	}
	return 0;
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值