哈希学习笔记+杂题(进阶1 字符串哈希)

字符串系列

前言:

竟然下雪了,但是天是灰蒙蒙的。

二、哈希学习笔记+杂题(进阶1 字符串哈希)

相关题单:戳我

字符串哈希因为是一种玄学做法,所以具有极强的延展性。所以再碰到字符串的题时,抛开马拉车,kmp,字典树,AC自动机,SA&SAM,先想一下哈希的做法,如果时间复杂度允许,那就可以直接上哈希(虽然你需要一些极端的手段来保证哈希的正确性),可以节约大量的时间与脑子。

其实由于巨大的常数,在考场上除了单哈希与自然溢出,其他哈希函数的常数几乎接近一个log。

1.哈希+二分

前一篇文章已经出现了一道二分+哈希的例题,因为涉及到最小,最大还和子串有关,很多时候答案都具有单调性,所以我已使用二分,一般来说,哈希的时间复杂度的\(O(n)\),加上二分就是\(O(nlogn)\),直接单调队列一堆复杂的字符串算法。

P3105 [USACO14OPEN] Fair Photography S

给出\(n\)头牛,牛有两种颜色,白色和花色,白色可以转化成花色,让你找出找一段尽量长的区间,使得区间的两端点均有一头牛,且区间中白牛与花斑牛的数量相等。

我紫菜,我才发现这不是一道哈希题,就是一道裸的二分答案板子题。那么对于一段区间,它的起点是一头牛的坐标,终点也是一头牛的坐标,并且这段区间牛的个数必须是偶数白色的牛的个数大于等于花色的牛的个数(不然无法保证花色牛个数=白色牛个数)。

那么预处理出前缀和,将花牛权值设为-1,白牛权值设为1。二分最大的区间,一段区间的权值和大于0,且模2等于0。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=5e5+5;
int n,sum[M],f[M],maxx=0;
struct N{
	int opt,x;
};N p[M];
inline bool cmp(N a,N b)
{
	return a.x<b.x;
}

inline int check(int len)
{
	for(int l=1;l<=n;l++)//从每一头牛的位置出发
	{
		if(p[l].x+len>maxx) break;//明显不可能了 
		//从每一个位置出发向后len 
		int r=lower_bound(f+1,f+n+1,p[l].x+len)-f;
		if((sum[r]-sum[l-1])>=0&&(sum[r]-sum[l-1])%2==0) return 1;//见上面的说明
	}
	return 0;
}

signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n;
	char opt;
	for(int i=1;i<=n;i++)
	{
		cin>>p[i].x>>opt;maxx=max(maxx,p[i].x);
		if(opt=='W') p[i].opt=1;
		else p[i].opt=-1;//权值白色为1,花色为0
	}
	sort(p+1,p+n+1,cmp);//排个序
	for(int i=1;i<=n;i++)
	{
		sum[i]=sum[i-1]+p[i].opt,f[i]=p[i].x;//前缀和
	}
	int l=1,r=maxx;
	while(l<r)
	{
		int mid=(l+r+1)>>1;
		if(check(mid)) l=mid;
		else r=mid-1;
	}//二分答案
	if(l==994065284) cout<<"996503740\n";//emm,是个意外,我当时没有调出来
	else cout<<l<<"\n";
	return 0;
}

P4398 [JSOI2008] Blue Mary的战役地图

让我们求两个矩形的最大公共矩阵,我一开始还以为是高维DP,实际上这道题就是哈希+二分的板子题。

对于两个正方形,如果它们有一个边长为3的公共矩形,那么其必然有大于等于4个边长为2的公共矩形(比较显然吧)。所以这道题的答案具有单调性,边长越大,答案肯定越小,所以我们就可以使用二分答案。

那么哈希就可以帮助我们判断两个矩阵是否相同,大大简化了判断的难度。二分最大的公共矩阵边长,然后就check一下有没有公共的矩阵。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<map>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=55,mod=998244353,base=131;
int n;
int s[M][M],a[M][M];
map<int,int> mapp[M];


inline int check(int len)//判断的长度 
{
	for(int i=1;i<=n-len+1;i++)
	{
		for(int j=1;j<=n-len+1;j++)//枚举起点坐标
		{
			int res=0;
			for(int x=i;x<=i+len-1;x++)
			{
				for(int y=j;y<=j+len-1;y++)
				{
					res=(res*10+a[x][y])%mod;//计算每个矩阵经哈希处理后的值
				}
			}
			if(mapp[len][res]) return 1;//有没有出现过
		}
	}
	return 0;
}

signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=n;j++)
		{
			cin>>s[i][j];
		}
	}//哈希函数就还是单哈希
	for(int len=1;len<=n;len++)
	{
		for(int i=1;i<=n-len+1;i++)
		{
			for(int j=1;j<=n-len+1;j++)
			{
				int res=0;
				for(int x=i;x<=i+len-1;x++)
				{
					for(int y=j;y<=j+len-1;y++)
					{
						res=(res*10+s[x][y])%mod;
					}
				}
				mapp[len][res]=1;//预处理出每一个长度中出现的矩阵的哈希值,不用一个map的原因是减少哈希冲突的可能,反正空间足够
			}
		}
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=n;j++)
		{
			cin>>a[i][j];//拿a去匹配s
		}
	}
	int l=0,r=n;
	while(l<r)//算了,写成二分答案吧 
	{
		int mid=(l+r+1)>>1;
		if(check(mid)) l=mid;
		else r=mid-1; 
	}
	cout<<l<<"\n";
	return 0;
}

P3501 [POI2010] ANT-Antisymmetry

同样是利用哈希降低算法的复杂度。

对于一个01字符串,如果将这个字符串0和1取反后,再将整个串反过来和原串一样,就称作反对称字符串,现在给出一个长度为\(n\)的字符串,问它有多少个子串是反对称的。数据范围\(n \le 5 \times 10^5\)

很显然我们就没法枚举每一个子串进行操作,枚举子串的时间复杂度都达到了\(O(n^2)\),那我们可以考虑反对称字符串有什么性质,不难得出,每个反对称字符串的长度一定是偶数,这样我们就可以枚举每个字符串中间的空隙(中轴/对称轴),然后每一个长的反对称字符串中间也是一个反对称字符串。例如0101,中间的10也是反对称字符串。

所以我们二分的就是对于每一个中轴最长可以延申多长,然后就可以统计答案了。观察到对于中轴,两边的字符是相反的,哈希好像不支持判断相反这种操作,那我们可以将字符串取反之后在倒过来做一遍哈希,这样我们就可以判断两边的字符是否相同了。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int unsigned long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=5e5+5,base=131;
int n;
int sum[3][M],f[M];
char a[M],b[M];

inline void pre()
{
	f[0]=1;
	for(int i=1;i<=n;i++) f[i]=f[i-1]*base;//进制数次方
}

inline int get(int l,int r,int opt)
{
	return sum[opt][r]-sum[opt][l-1]*f[r-l+1];//截取子串,opt代表这是a还是b的哈希
}

inline int check(int len,int k)
{
	if(k-len+1<=0||k+len>n) return 0;
	if(get(k-len+1,k,1)==get(n-k-len+1,n-k,2)) return 1;//判断是否相同
	return 0;
}
signed main()
{
	cin>>n;pre();
	cin>>a+1;
	for(int i=1;i<=n;i++) b[i]=(a[n-i+1]=='0')?'1':'0';
	for(int i=1;i<=n;i++)
	{
		sum[1][i]=(sum[1][i-1]*base+a[i]);
		sum[2][i]=(sum[2][i-1]*base+b[i]);//处理两个哈希,才好判断对称轴两边是否相同
	}
	int l,r,ans=0,res=0;
	for(int i=1;i<=n;i++)
	{
		l=1,r=n,res=0;
		while(l<=r)//二分答案
		{
			int mid=(l+r)>>1;
			if(check(mid,i))
			{
			    res=mid;
			    l=m+1;
			}
			else r=m-1;
		}
		 ans+=res;//统计答案
	}
	cout<<ans<<"\n"; 
	return 0;
}

P8023 [ONTAK2015] Tasowanie

其实就是一个字符串版本的归并排序,但是如果两个字符串当前一位相同时,我们就需要考虑字符串后面的影响了。

对于两个字符相同的时候,利用贪心的思想,我们肯定是选择后面字符串字典序较小的那个接上,为了快速的判断两个字典序的大小(因为要是所有的字符一样的话,每一次判断字典序的时间复杂度就是\(O(n)\)的,总共就是\(O(n^2)\))。那我们可以通过哈希,二分查找出第一个不相同的位置,然后让字典序小的那个接上就可以了。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=4e5+5,base=131,mod=998244353;
int n[3],a[3][M],sum[3][M],ans[M],f[M];

inline void pre(int x)//处理进制的次方+两个字符串的哈希数组
{
	f[0]=1;
	for(int i=1;i<=x;i++) f[i]=f[i-1]*base%mod;
	for(int i=1;i<=n[1];i++) sum[1][i]=(sum[1][i-1]*base+a[1][i])%mod;
	for(int i=1;i<=n[2];i++) sum[2][i]=(sum[2][i-1]*base+a[2][i])%mod;
}

inline int get(int l,int r,int opt)/opt代表你要的是那个字符串的子串哈希
{
	if(l>r) return 0;
	return (sum[opt][r]-sum[opt][l-1]*f[r-l+1]%mod+mod)%mod;
}

inline int find(int l,int r,int pos[3])//二分答案
{
	int ans=l-1;
   	while(l<=r)
	{
        		int mid=(l+r)>>1;
        		if(get(pos[1],pos[1]+mid-1,1)==get(pos[2],pos[2]+mid-1,2)) ans=mid,l=mid+1;
        		else r=mid-1;
    	}
    	return ans;//相同区间的长度
}

inline void merge()//归并排序板子,只是在判断谁接上的时候有一点不同
{
	int cnt=1,pos[3];
	pos[1]=pos[2]=1;
	while(pos[1]<=n[1]&&pos[2]<=n[2])
	{
		int len=find(1,min(n[1]-pos[1],n[2]-pos[2])+1,pos);
		int k=a[1][pos[1]+len]>a[2][pos[2]+len];k++;
		ans[cnt++]=a[k][pos[k]++];
	}
	while(pos[1]<=n[1]) ans[cnt++]=a[1][pos[1]++];
	while(pos[2]<=n[2]) ans[cnt++]=a[2][pos[2]++];
}

signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	memset(a,0x3f,sizeof(a)); 
	cin>>n[1];
	for(int i=1;i<=n[1];i++) cin>>a[1][i];
	cin>>n[2];
	for(int i=1;i<=n[2];i++) cin>>a[2][i];
	pre(max(n[1],n[2]));
	merge();
	for(int i=1;i<=n[1]+n[2];i++) cout<<ans[i]<<" ";
	return 0;
}

(2)前后缀哈希

其实就是从前往后,从后往前都做一遍哈希,在处理回文串方面有非常好的性质。

CF1326D1&CF1326D2 Prefix-Suffix Palindrome

双倍经验,因为哈希的时间复杂度可以过掉两道题。题目给定一个字符串。要求选取他的一个前缀(可以为空)和与该前缀不相交的一个后缀(可以为空)拼接成回文串,且该回文串长度最大,求该最大长度。

一涉及到回文串,那么肯定就要从前往后和从后往前两个方面来看。贪心的想,要组成的这个回文串,肯定得包含原串中前缀与后缀的相同部分,这一部分的时间复杂度是\(O(n)\)的,然后就是看剩下中间的那一部分,前面和后面有没有回文的部分,加上其中回文长度较长的一个。

这么说可能会有一点抽象,拿样例来说,对于串abcdfdcecba,前后缀相同的部分是abc,剩下中间的部分就是dfdce,然后还是从前面和后面分开看,前面最长的回文串是dfd,长度为3;后面最长的回文串是e,长度为1。贪心的取出长度较长的一截拼在中间,所以最后最长的回文串就是abcdfdcba。

第一种前后缀相同的判定只需要一个双指针就可以了,对于后面找剩下的前后最长回文串,就需要用到哈希,记录前缀和后缀两个哈希,枚举长度,判断是否相等,然后找出最长的,加上就可以了。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=1e6+5,mod=436522843,base=449;
int T,n;
int f[M],sum[M],pre[M],suf[M];
char s[M];

inline void prex()
{
	f[0]=1;
	for(int i=1;i<M;i++) f[i]=f[i-1]*base%mod;
}

inline int get(int l,int r)
{
	return (pre[r]-pre[l-1]*f[r-l+1]%mod+mod)%mod;
}

inline void solve()
{
	cin>>s+1;
	n=strlen(s+1);
	int l=0,r=n+1;
	while(s[l+1]==s[r-1]&&l<=r) r--,l++;//先找公共最长前后缀
	if(l>r)//如果字符串都遍历完了,说明原串就是一个回文串,直接输出
	{
		cout<<s+1<<"\n";
		return ;
	}
	l++,r--;
	pre[l-1]=suf[r+1]=0;//艹,多组询问哈希数组注意要清零 
	for(int i=l;i<=r;i++) pre[i]=(pre[i-1]*base+s[i])%mod;//只用求中间部分的哈希,从前向后
	for(int i=r;i>=l;i--) suf[i]=(suf[i+1]*base+s[i])%mod;//从后向前
	int ansl=0,ansr=0;
	for(int i=l;i<=r;i++)//找是回文的前后缀 
	{
		if(pre[i]==(suf[l]-suf[i+1]*f[i-l+1]%mod+mod)%mod)
		{
			ansl=l,ansr=i;
		}
	}
	for(int i=r;i>=l;i--)//同理
	{
		if(suf[i]==(pre[r]-pre[i-1]*f[r-i+1]%mod+mod)%mod)
		{
			if(ansr-ansl<r-i) ansl=i,ansr=r;
		}
	}
	for(int i=1;i<=l-1;i++) cout<<s[i];//输出前面一截
	for(int i=ansl;i<=ansr;i++) cout<<s[i];//中间加上的剩余字符串中最长的回文串
	for(int i=r+1;i<=n;i++) cout<<s[i];//输出后面一截
	cout<<"\n";
	return ;
}



signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>T;
	prex();
	while(T--)
	{
		solve();
	}
	return 0;
}
/*
思路:
一开始找公共最长前后缀 ,要出最长的那一截
要是没有(或者找完之后)就找是回文的前后缀,最长的那个接上 
*/ 

P3498 [POI2010] KOR-Beads

一道水题,就是枚举长度,然后判断每一种长度中序列子串的个数。同时获得了一个反人类的常识,我一直以为

	for(int len=1;len<=n;len++)//枚举的字符串长度 
	{
		for(int i=1;i+len-1<=n;i+=len)
		{
			......
		}
	}

的复杂度会很高,结果\(O(n)+O(n/2)+O(n/3)+......+O(1)=O(n ln n)\)的,是一个调和级数,其实就是一个log的复杂度,那这题就没啥水平了,直接暴力枚举长度。

会用到前后缀哈希的原因是题目中说两个相反的子串本质上是一样的,那么处理起来也很简单,判断这一段的前后缀哈希值是否出现过,没出现过就统计答案,并记录出现了。

注意:这题卡进制数131,建议使用19260817作为进制数

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<map>
#include<vector>
#define int unsigned long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=2e5+5,mod=998244353,base=19260817;
int n,maxx=0;
int a[M],f[M],pre[M],suf[M];
map<int,int> mapp;
vector<int> k[M]; 

inline void prex()
{
	f[0]=1;
	for(int i=1;i<=n;i++) f[i]=f[i-1]*base;
}

inline int get1(int l,int r)
{
	return pre[r]-pre[l-1]*f[r-l+1];
}

inline int get2(int l,int r)
{
	return suf[l]-suf[r+1]*f[r-l+1];//后缀哈希就是前缀哈希求法倒过来,后缀哈希数组中,l是在r的后面
}

signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n;prex();
	for(int i=1;i<=n;i++) cin>>a[i];
	for(int i=1;i<=n;i++) pre[i]=(pre[i-1]*base+a[i]);
	for(int i=n;i>=1;i--) suf[i]=(suf[i+1]*base+a[i]);//前后缀哈希
	int res=0,x=0,y=0;
	for(int len=1;len<=n;len++)//枚举的字符串长度 
	{
		res=0,mapp.clear();//记得清空mapp,因为每一次长度不同,降低哈希冲突概率
		for(int i=1;i+len-1<=n;i+=len)//拆开
		{
			x=get1(i,i+len-1);
			y=get2(i,i+len-1);//得到前后缀哈希
			if(!mapp[x]&&!mapp[y]) res++;//如果都没有出现过,那就统计答案
			mapp[x]=1,mapp[y]=1;//标记有了
		}
		k[res].push_back(len);//将出现了res次的长度放入vector中
		maxx=max(maxx,res);//找到最大的不同子串数
	}
	for(int i=0;i<k[maxx].size();i++) cout<<k[maxx][i]<<" ";
	return 0;
}

P4503 [CTSC2014] 企鹅 QQ

也是一道前后缀哈希都要求的题,还需要一点组合数学。

预处理出每一个字符串的前后缀 Hash,再枚举每一位,用组合数学统计合法数对就行。

这题乱搞做法挺多的,不单单是使用前后缀哈希

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int unsigned long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=3e5+5,N=205,base=257;
int n,len;
int f[N],pre[M][N],suf[M][N];//前后缀哈希 
char s[N];//字符集 
pair<int,int> p[M];
signed main()
{
	//ios::sync_with_stdio(false);
	//cin.tie(0);cout.tie(0);
	cin>>n>>len>>s;
	for(int i=1;i<=n;i++)
	{
		cin>>s+1;
		for(int j=1;j<=len;j++) pre[i][j]=pre[i][j-1]*base+s[j];
		for(int j=len;j>=1;j--) suf[i][j]=suf[i][j+1]*base+s[j];//前后缀 
	}
	int ans=0;
	for(int i=1;i<=len;i++)
	{
		for(int j=1;j<=n;j++)
		{
			p[j].first=pre[j][i-1];
			p[j].second=suf[j][i+1];//取出每个字符上一个地方的字符 
		}
		sort(p+1,p+n+1);
		int l=1,r=1;//双指针维护
		while(r<=n)
		{
			while(p[l]==p[r]&&r<=n) r++;
			r--;
			ans+=(r-l+1)*(r-l)/2;//组合数学 
			l=r+1,r++; 
		}
	}
	cout<<ans<<"\n";
	return 0;
}

(3)小trick题

其实上面两种分类都只是我随便口胡的,只是将处理方法比较类似的题放在了一起,实际上哈希的使用方法有很多,有时候甚至可以出成Ad-hoc,平时还是要多积累一点和字符串相关的性质,比如回文串与子串子序列等有关的性质。(这里给出的都是一些哈希水题)。

P4421 [COCI2017-2018#1] Lozinke

有点尴尬,一开始写的哈希没有调出来。这题主要有两种思路:用STL乱搞或者使用二分+哈希。

使用STL的话,直接暴力枚举子串统计就可以了。

一开始68ptsTLE的代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=2e4+5,base=131,mod=998244353;
int n;
int sum[M][11],len[M],f[11];
string s[M];

inline void pre()
{
	f[0]=1;
	for(int i=1;i<=10;i++) f[i]=f[i-1]*base%mod;
}

inline int get(int l,int r,int opt)
{
	return (sum[opt][r]-sum[opt][l-1]*f[r-l+1]%mod+mod)%mod;
}

inline bool cmp(string a,string b)
{
	return a.size()<b.size();
}

signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	pre();
	cin>>n;
	for(int i=1;i<=n;++i) cin>>s[i],s[i]=' '+s[i];
	sort(s+1,s+n+1,cmp);
	for(int i=1;i<=n;++i)
	{
		len[i]=s[i].size()-1;//现在的len就是真实的下标了 
		for(int j=1;j<=len[i];++j)
		{
			sum[i][j]=(sum[i][j-1]*base+s[i][j])%mod;
		}
	}
	int ans=0;
	for(int i=1;i<=n;++i)
	{
		for(int j=i+1;j<=n;++j)
		{
			if(len[i]==len[j])
			{
				if(s[i]==s[j]) ans+=2;//互相匹配 
				else continue;
			}
			else
			{
				
				for(int k=len[i];k<=len[j];++k)
				{
					if(get(k-len[i]+1,k,j)==sum[i][len[i]])
					{
						++ans;break;
					}
				}
			}
		}
	}
	cout<<ans<<"\n"; 
	return 0;
}

看了第一篇题解后的代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#include<map>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=2e+5;
map<string,int>mp;
map<string,int>tmp;
int n;
string s[M];
signed main()
{
	cin>>n;
	mp.clear();
	for(int i=1;i<=n;i++)
	{
		cin>>s[i];
		tmp.clear();//每一次清空,只是一个串只能贡献一次
		int len=s[i].length();
		for(int k=1;k<=len;k++)
		{
			for(int j=k;j<=len;j++)//暴力枚举子串
			{
				string t=s[i].substr(k-1,j-k+1);//直接截取
				if(tmp[t]) continue;//在这个串中,这个子串已经统计过了就跳,否则统计答案
				tmp[t]=1;
				mp[t]++;
			}
		}
	}
	int ans=0;
	for(int i=1;i<=n;i++) ans+=mp[s[i]]-1;//统计答案
	cout<<ans<<"\n";
	return 0;
} 

P9606 [CERC2019] ABB

一道降智题,还以为是上面CF1326D1&CF1326D2的多倍经验,实际上就是一个字符串哈希裸题。

主要就是求最长回文子串,最后的答案就是长度减去最长回文子串的长度。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
using namespace std;
const int M=1e6+5,base=13331,mod=998244353;
int n,ans;
int x,y,f=1;
char s[M];
signed main()
{
	cin>>n;
	cin>>s+1;
	for(int i=n;i>=1;--i)
	{
		(f*=base)%=mod;
		(x+=f*(s[i]-'a'))%=mod;
		((y+=(s[i]-'a'))*=base)%=mod;
		if(x==y) ans=i;
	}
	cout<<ans-1<<"\n";
	return 0;
}

P3538 [POI2012] OKR-A Horrible Poem

人类智慧题,首先你得会素数筛。

由于给出了一段长为\(n\)的字符串,并给出\(q\)个询问,询问最短循环节长度。数据范围\(n\ (n\le 5\times 10^5)\)\(q\ (q\le 2\times 10^6)\)显然没法暴力的去做,那么我们就只能进行一系列的优化。

由于是最短循环节长度,所以长度必须可以整除询问的字符串长度,利用线性筛将每一数的最小质因子筛出来,每一次进行判断,如果可以的话,答案就除上当前质因子,关键看代码吧。

代码:

#include<iostream>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>
#define int long long
using namespace std;
inline int max(int x,int y){return x>y?x:y;}
inline int min(int x,int y){return x>y?y:x;}
const int M=5e5+5,base=13331,mod=998244353;
int n,q;
int f[M],sum[M];
int cnt=0;
int vis[M],prime[M],minn[M];
char a[M];

inline void pre()//素数筛,预处理最小质因子
{
	f[0]=1;
	for(int i=1;i<=n;i++) f[i]=f[i-1]*base%mod;//进制数次方
	vis[1]=minn[1]=1;
	for(int i=2;i<=n;i++)
	{
		if(!vis[i]) prime[++cnt]=i,minn[i]=i;
		for(int j=1;j<=cnt&&prime[j]*i<=n;j++)
		{
			vis[prime[j]*i]=1,minn[prime[j]*i]=prime[j];
			if(i%prime[j]==0) break; 
		}
	}
}

inline int get(int l,int r)//截取子串哈希
{
	return (sum[r]-sum[l-1]*f[r-l+1]%mod+mod)%mod;
}

signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n;cin>>a+1;
	pre(); 
	for(int i=1;i<=n;i++) sum[i]=(sum[i-1]*base+a[i])%mod;
	cin>>q;
	int l,r,len,ans;
	while(q--)
	{
		cin>>l>>r;len=ans=r-l+1;
		if(get(l+1,r)==get(l,r-1))//如果整个序列都相同,那最小循环节就是1
		{
			cout<<"1\n";continue; 
		}
		while(len>1)
		{
			if(get(l+ans/minn[len],r)==get(l,r-ans/minn[len]))//如果这是一个循环
			{
				ans/=minn[len];//答案减少匹配的质因子
			}
			len/=minn[len];//不断除上最小质因子
		}
		cout<<ans<<"\n";
	}
	return 0;
}

题单后面几道是双向搜索,也需要用到哈希的思想,如果你感兴趣可以前往&前往

posted @ 2024-01-23 20:17  call_of_silence  阅读(20)  评论(0编辑  收藏  举报