NOIP 2020 T2字符串匹配

前言

好的现在离下课还有17分钟,蒟蒻拼手速的时间到了!!

正题

首先看到这一道题,我人都是傻掉的,字符串没学好啊!!!

首先是一种暴力的算法,但是水一下可以卡过去
本地2.3s,洛谷386ms,洛谷神机万岁!!

思路:
首先枚举循环节的长度,然后在枚举指数,直到两个的前缀不符合或匹配完了,依旧是枚举每一种情况。

对于前缀使用哈希表,哈希自然溢出很快,不用去摸。

考虑有一个数组来记录每到一个位置之前出现奇数次的字母个数所对应的前缀和(A中的字母个数),然后每次枚举循环节时暴力更新。

定义f(1,j),f(1,j)表示s[1,j]中出现奇数次的字符的个数。

每当找到一种情况,ans就加上比后缀的C中的奇数次的字母次数小的个数(不同的A的情况)。

O(nlogn+26n)

点击查看代码
#include<cstdio>
#include<cstring>
#define ull unsigned long long
#define re register
using namespace std;
const ull base=131;
char s[1050000];
int n;
short sum[1050000][2],a[1050000],b[1050000],c[1050000],r[100];
ull v[1050000],l[1050010];
long long vis[30],ans=0;
signed main()
{
	//freopen("string25.in","r",stdin);
	//freopen("string.out","w",stdout);
	int T;
	scanf("%d",&T);
	l[0]=1;
	for(re int i=1;i<=1050000;i++)//哈希预处理
		l[i]=l[i-1]*base;
	while(T--){
		ans=0;
		scanf("%s",s+1);
		n=strlen(s+1);
		memset(sum,0,sizeof sum);//存字母出先次数为奇数的字母个数
		memset(a,0,sizeof a);//前缀的字母个数
		memset(b,0,sizeof b);//后缀,同上
		memset(r,0,sizeof r);
		memset(vis,0,sizeof vis);
		for(re int i=1;i<=n;i++){
			c[i]=s[i]-'a'+1;
			r[c[i]]^=1;//异或判断次数
			if(r[c[i]]) sum[i][0]=sum[i-1][0]+1;
			else sum[i][0]=sum[i-1][0]-1;
			a[i]=sum[i][0];
		}
		memset(r,0,sizeof r);
		for(re int i=n;i>=1;i--){
			r[c[i]]^=1;
			if(r[c[i]]) sum[i][1]=sum[i+1][1]+1;
			else sum[i][1]=sum[i+1][1]-1;
			b[i]=sum[i][1];
		}
		for(re int i=1;i<=n;i++){
			v[i]=base*v[i-1]+1ull*(c[i]);
		}
		for(re int i=2;i<=n;i++){
			for(re int j=a[i-1];j<=26;j++) vis[j]++;//暴力维护前缀和
			for(re int j=i;j<n&&(v[i]==v[j]-v[j-i]*l[i]);j+=i) ans+=vis[b[j+1]];//统计答案,和树状数组差不多
		}
		printf("%lld\n",ans);
	}

	
	return 0;
}

算法二:

正统的O(nlog26)

和上面的思路差不多,但是用树状数组维护前缀和,也不用预处理前后缀。

还有Z算法计算循环节个数,所以只用枚举循环节的长度即可。

注意,他把循环节的个数分为奇偶两种情况讨论,每种情况都不同。

在枚举到一个点时,同时O(1)处理前后的字母个数。

又使用了树状数组来维护字母个数的前缀和。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=(1<<20)+5;
char s[maxn];
int n,z[maxn];
int before[30],after[30];
int pre,suf,all;

int c[30];

inline int lbt(int x){
	return x&(-x);
}

inline void update(int x){
	while(x<=27){
		c[x]++;
		x+=lbt(x);
	}
}

inline int query(int x){
	int r=0;
	while(x>=1){
		r+=c[x];
		x-=lbt(x);
	}
	return r;
}

void Z(){//具体的Z算法
	z[0]=n;
	int now=0;
	while(now+1<n&&s[now]==s[now+1]){
		now++;
	}
	z[1]=now;
	int p0=1;
	for(int i=2;i<n;i++){
		if(i+z[i-p0]<p0+z[p0]){
			z[i]=z[i-p0];
		}else{
			now=p0+z[p0]-i;
			now=max(now,0);
			while(now+i<n&&s[now]==s[now+i]){
				now++;
			}
			z[i]=now;
			p0=i;
		}
	}
	
}
int main()
{
	int T;
	cin>>T;
	while(T--){
		cin>>s;
		n=strlen(s);
		memset(before,0,sizeof before);
		memset(after,0,sizeof after);
		memset(c,0,sizeof c);
		all=pre=suf=0;
		Z();
		for(int i=0;i<n;i++){//留一个位置给C,C不能为空
			if(i+z[i]==n){
				z[i]--;
			}
		}
		for(int i=0;i<n;i++){//后缀的个数
			after[s[i]-'a']++;
		}
		for(int i=0;i<26;i++){//所有区间内出现奇数次的字母个数
			if(after[i]&1){
				all++;
			}
		}
		suf=all;
		long long ans=0;
		
		for(int i=0;i<n;i++){
			if(after[s[i]-'a']&1){//O(1)修改
				suf--;
			}else{
				suf++;
			}
			after[s[i]-'a']--;
			if(before[s[i]-'a']&1){
				pre--;
			}else{
				pre++;
			}
			before[s[i]-'a']++;
			if(i!=0&&i!=n-1){//合法的情况
				int t=z[i+1]/(i+1)+1;//循环节个数
				ans+=1ll*(t/2)*query(all+1)+1ll*(t-t/2)*query(suf+1);//奇偶两种情况,当k为偶数时,前面的所有的字母都出现偶数次,C中奇次的字母个数就是all+1,当k为奇数时,是后缀个数
			}
			update(pre+1);
		}
		cout<<ans<<endl;
	}
	
	return 0;
}
posted @   SSZX_loser_lcy  阅读(51)  评论(0编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示