题意:
询问次数有200万次,每次询问的长度有50万,需要求出询问的区间的最小循环节。
给出性质1:对于一个区间的循环节来说,设区间为[l,r],循环节长度为len,那么可得:[l,r-len]=[l+len,r] (我们用哈希可以做到O(1)实现)
现在我们已经有了快速判断是否是循环节的方法了,然后我们需要找出一个快速枚举循环节的方法。
不难想到,枚举一个区间的全部因数,这样的复杂度为O(sqrt(n)q),粗略计算一下发现貌似需要14秒的时间(恰巧可以出一档询问次数为10万次的部分分)
现在我们需要一个更快的做法枚举全部因数,比sqrt(n)快的且符合此题的貌似只有log(n)了。
给出性质2:如果[l,l+len-1]为[l,r]的循环节,即循环长度为len,且r-l+1为2len的倍数,则[l,l+len*2-1]也为[l,r]的循环节。
所以,我们可以转变一个思路:先想办法判断2len是否是一个循环节,如果2len不是一个循环节,那么len则肯定也不是了;如果2len是一个循环节,接下来再判断len是否是循环节。
考虑到循环节长度肯定是区间长度的因数,所以我们可以用区间长度不断地分解质因数。
记r-l+1=LEN
设LEN=prime[p1]prime[p2]…prime[pn],不保证prime均不同。
首先判断循环节是否可能是LEN/prime[p1],如果是的话,那么很高兴,现在的LEN就变成了LEN/prime[p1],接下来我们需要试试LEN/prime[p1]/prime[p2]了;如果不是,那么很不高兴,我们只能试试LEN/prime[p2]了。
对于一个区间长度为LEN的字符串,即使prime均为2,也只需要log(LEN)的复杂度,符合我们的期望。所以,感觉做好了?!?
其实还没有。
我们怎么知道LEN分解质因数会分解成什么样子呢?
如果不考虑pollard rho或者miller-rabin这种东西,我们正常的分解质因数是要sqrt(n)的,这样,不是又没变嘛…
考虑在get_prime中预处理出每个数的最小质因子,设LEN的最小质因子为num[LEN],这样,LEN变为LEN/num[LEN],LEN/num[LEN]变为(LEN/num[LEN])/num[LEN/num[LEN]],并且同时保留num[LEN],num[LEN/num[LEN]],就可以log(n)内分解完质因数了。
而预处理的复杂度则是线性筛素数复杂度O(n)。
#include <bits/stdc++.h>
#define int unsigned long long
using namespace std;
const int N=5e5+5,base=131;
int tot,prime[N],num[N];
bool mark[N];
int n,q,l,r,len,cnt;
int bin[N],hash[N],b[N];
char str[N];
inline void get_prime()
{
mark[1]=true;
for (register int i=2; i<=n; ++i)
{
if (!mark[i]) prime[++tot]=i,num[i]=i;
for (register int j=1; j<=tot; ++j)
{
if (prime[j]*i>n) break;
mark[prime[j]*i]=true;
num[prime[j]*i]=prime[j];
if (i%prime[j]==0) break;
}
}
}
inline int get(int x,int y){return hash[y]-hash[x-1]*bin[y-x+1];}
signed main(){
scanf("%lld",&n);
get_prime();
scanf("%s",str+1);
bin[0]=1;
for (register int i=1; i<=n; ++i) bin[i]=bin[i-1]*base;
for (register int i=1; i<=n; ++i) hash[i]=hash[i-1]*base+(int)str[i];
scanf("%d",&q);
while (q--)
{
scanf("%lld%lld",&l,&r);
len=r-l+1;
cnt=0;
while (len>1) b[++cnt]=num[len],len/=num[len];
len=r-l+1;
for (register int i=1; i<=cnt; ++i)
if (get(l,r-len/b[i])==get(l+len/b[i],r)) len/=b[i];
printf("%lld\n",len);
}
return 0;
}