A Horrible Poem(信息学奥赛一本通- P1460) [POI 2012] OKR-A Horrible Poem(洛谷-P3538)

【题目描述】

原题来自:POI 2012

给出一个由小写英文字母组成的字符串 S,再给出 q 个询问,要求回答 S 某个子串的最短循环节。

如果字符串 B 是字符串 A 的循环节,那么 A 可以由 B 重复若干次得到。

【输入】

第一行一个正整数 n,表示 S 的长度。

第二行 n 个小写英文字母,表示字符串 S 。

第三行一个正整数 q ,表示询问个数。

下面 q 行每行两个正整数 a,b,表示询问字符串 S[a..b] 的最短循环节长度。

【输出】

依次输出 q 行正整数,第 i 行的正整数对应第 i 个询问的答案。

【输入样例】

复制代码
8
aaabcabc
3
1 3
3 8
4 8

【输出样例】

复制代码
1
3
5

【提示】

1≤a≤b≤n≤5×10^5 , q≤2×10^6。

一、 题目分析

核心诉求 :给定长度为N的字符串,进行Q次查询,每次查询要求找出一个给定子串S[a..b]的最短循环节长度。

数据范围,

这是一个极其庞大的查询量。如果单次查询的时间复杂度达到O(N)甚至,总时间都会飙升到级别,必然超时。这要求我们在预处理后,单次查询的时间复杂度必须被压制在O(1)或 级别。

二、 思考过程与解题思路

面对这种题,我们需要拆解两个核心问题:如何快速验证循环节? 以及如何快速找到最短的那个?

1. 验证循环节(错位重叠法)

判断一个长度为的前缀是否是总长为的字符串的循环节,有一个非常经典的结论:

如果是循环节,必然满足:原串掐掉结尾个字符的子串,完全等于掐掉开头个字符的子串。

映射到区间 上就是:

S[a...b-len]==S[a+len...b]

要实现O(1)的字符串相等比较,字符串前缀哈希是唯一解。

2. 避免暴力枚举(引入质因数分解)

已知最短循环节长度一定能整除总长度L。

最朴素的想法是枚举L的所有约数,但时间吃不消。

逆向思考:假设初始循环节就是L本身,如果它能被压缩,必然是除以了L的某个质因数p。

因此,我们只需要找出L的所有质因数。对于每一个质因数p,只要当前循环节长度能被p整除,且尝试除以p后的长度能通过"错位重叠哈希校验",我们就把循环节长度缩小为原先的。一直榨干这个质因数,再换下一个质因数继续测试。

3. O(1)获取质因数(欧拉筛改造)

每次查询如果现场做的质因数分解,依然会超时。

我们可以利用欧拉筛(线性筛)的特性:欧拉筛保证了每个合数只会被它的最小质因数筛掉。我们在筛的过程中,顺手记录下每一个数字的"最小质因数"。

这样分解质因数时,只需不断查表p=minprim[L],然后 L/=p,就能在极其严格的 时间内剥离出所有质因数。

三、 算法设计

  1. 预处理哈希时间预处理字符串的前缀哈希数组ans和进制的次幂数组p

  2. 预处理最小质因数时间跑一遍改造后的欧拉筛,利用minprim数组记录 每一个数的最小质因数。

  3. 处理查询

    • 计算区间长度L=b-a+1。

    • 设定初始答案shortlen=L,复制一份用于提供质因数的len2=L

    • len2>1时,取p=minprim[len2]

    • 不断尝试testlen=shortlen/p。若哈希校验通过,则shortlen=testlen,继续死磕这个p;若不通过则break

    • len2中关于p的因子全部除尽,准备获取下一个全新的质因数。

    • 最终留下的shortlen即为绝对最短循环节。

四、 时空复杂度分析

  • 时间复杂度 :预处理哈希O(N),欧拉筛O(N)。单次查询中,分解质因数最多执行 次除法和哈希验证(哈希验证为)。总时间复杂度为。完美通过。

  • 空间复杂度:需要维护长度为N的次幂数组、哈希数组、素数表、埃氏表、最小质因数表。全部使用连续内存的静态数组,空间复杂度O(N)。

五、 易错点总结

  1. 哈希区间提取边界 :提取闭区间[l, r]的哈希值,公式必须是ans[r]-ans[l-1]*p[r-l+1],千万不能写成减去ans[l],否则会把第l个字符给删掉。

  2. 素数的最小质因数 :在欧拉筛中,不要只给合数标记minprim。当扫描到一个素数时,必须明确记录它的最小质因数就是它自己 (minprim[i]=i),否则后续查表会遇到0导致死循环或运行错误。

  3. 海量I/O卡常,输出量极大,必须关闭流同步 ios::sync_with_stdio(false); cin.tie(0);,否则算法再优也可能会超时。

  4. 自然溢出被出题人针对 :正常情况下unsigned long long的自然溢出足够安全,但如果有出题人恶意构造冲突数据,可将ull改为带模数的大质数单哈希或双哈希机制。

六、 完整代码

cpp 复制代码
#include <iostream>
using namespace std;
typedef unsigned long long ull;
int n,q;
string s;
ull p[500010];//存储131的i次方
//如果ull过不了 就是出题人专门构造数据来卡我们
//我们改用大质数取模单哈希或者双哈希即可
ull ans[500010];//存储字符串s的前缀哈希
int es[500010];//存埃氏表(es[i]==0代表i为素数否则为合数)
int prim[500010];//从小到大存n以内的素数
int minprim[500010];//记录每个数的最小质因数
int k;//用来记录n以内素数的个数

void oula(){
    //这里的i代表两层意思:1.作为倍数因子
    // 2.作为被检查者 看是否为素数
    for(int i=2;i<=n;i++){
        //如果i没有被标记 代表为素数
        if(es[i]==0){
            prim[++k]=i;
            //素数的最小质因数是自身
            minprim[i]=i;
        }
        //用当前的i去乘已知的素数,筛掉合数
        //j遍历的是素数表,prime[j]是我们选定的"最小质因子"
        //遍历素数表
        for(int j=1;j<=k&&prim[j]*i<=n;j++){
            es[prim[j]*i]=1;
            //记录该合数单最小质因数
            minprim[prim[j]*i]=prim[j];
        //为了保证"每个合数只被它的最小质因子筛一次",必须立刻停止。
            if(i%prim[j]==0) break;
        }
    }
}

//返回闭区间[l,r]的哈希值
ull gethash(int l,int r){
    return ans[r]-ans[l-1]*p[r-l+1];
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n;
    cin>>s;
    //存储131的i次方
    p[0]=1;
    for(int i=1;i<=n;i++) p[i]=131*p[i-1];
    //存储s的前缀哈希
    for(int i=1;i<=n;i++) ans[i]=131*ans[i-1]+s[i-1];
    //欧拉筛预处理找出1-n内每个数的素因数
    oula();
    cin>>q;
    while(q--){
        int a,b;
        cin>>a>>b;
        //区间长度len
        int len=b-a+1;
        //最短循环节长度 初始化为自身长度
        int shortlen=len;
        //len2作为工具变量
        //用len2来掉落素因数 每判断完一个素因数 就从中取出
        int len2=len;
        while(len2>1){
            //获取一个质因数p
            int p=minprim[len2];
            //只要当前的最短循环节还能被p整除 就尝试压缩
            while(shortlen%p==0){
                //用testlen来测试当前长度是否可以满足为循环节
                int testlen=shortlen/p;
                //错位重叠哈希校验
                //比对左半部[a,b-testlen]和右半部[a+testlen,b]
                if(gethash(a,b-testlen)==gethash(a+testlen,b)){
                    shortlen=testlen;
                }
                //校验失败,说明该质因数不能再使用了,立刻停止
                else break;
            }
            //将len2中包含的质因数p全部除尽,以便下一次拿全新的质因数
            while(len2%p==0){
                len2/=p;
            }
        }
        cout<<shortlen<<"\n"; 
    }
    return 0;
}
相关推荐
程序员爱德华2 小时前
LeetCode刷题
算法·leetcode
memcpy02 小时前
LeetCode 1202. 交换字符串中的元素【无向图连通分量】中等
算法·leetcode·职场和发展
fengfuyao9852 小时前
基于遗传算法的分布式电源选址定容优化(考虑环境因素)
算法·matlab·平面
睡觉就不困鸭2 小时前
第10天 删除有序数组中的重复项
数据结构·算法
Chase_______2 小时前
LeetCode 643:子数组最大平均数 I
算法·leetcode
笨笨饿2 小时前
#65_反激电源
stm32·单片机·嵌入式硬件·算法·硬件工程·个人开发
wengqidaifeng2 小时前
数据结构:排序(下)---进阶排序算法详解
数据结构·算法·排序算法
MicroTech20252 小时前
突破单机量子计算限制:MLGO微算法科技的新型分布式量子算法模拟平台实现高效验证
科技·算法·量子计算
没有天赋那就反复2 小时前
C++里面引用参数和实参的区别
开发语言·c++·算法