CSP-S集训刷题记录

$ CSP.S $ 集训刷题记录:

$ By~wcwcwch $





一、字符串专题:



1. 【模板】$ manacher $ 算法

模型:求出字符串 $ S $ 中所有回文串的位置及长度。

个人理解:解决这类问题,回文串的对称性质最重要。

于复杂度最关键的一句话: $ f[i]=min~(~r-i~,~f[~mid\times2-i~]~)~ $ (实现不同,边界可能不一样)

这个 $ min $ 函数左边 $ r-i $ 是当前位置到它所属于的回文串边界的距离,右边 $ mid\times 2 -1 $ 是它在所属于的回文串的另一边的对应位置。如果取左边,那么我用当前位置扩展(会超出当前回文串边界)于是必然能够使 $ r $ 向右扩展;如果取右边,我们发现由于回文串的对称性,这个位置必然不能继续向外扩展,因为他最开始向外扩展必然还是在当前回文串边界内(否则 $ min $ 函数肯定取的左边),而如果它能在当前回文串内扩展,它的对称位置 $ mid\times 2 -1 $ 也能,而 $ f[~mid\times2-i~] $ 已是扩展最大值,与之矛盾。

总结一下,如果 $ min $ 函数取左边,那么 $ r $ 一定能向右扩展;如果取右边,那么复杂度为 $ 1 $ (常数)。于是复杂度为 $ r $ 的值域和 $ 1 $ 的次数,两者都不超过 $ n $ ,于是复杂度为 $ O(2\times n) $ ,就算再加上与处理时加“#”号,也是常数而已。



2. 【模板】扩展 $ KMP $

模型:有一个字符串 $ a $ ,要求输出【 $ a $ 本身】和【 $ a $ 的每一个后缀】的最长公共前缀 。

$ KMP $ 和 $ Manacher $ 思想的结合产物。不过个人更倾向于 $ Manacher $ 。因为他的计算方法和 $ Manacher $ 神似,都是在某一个位置先根据之前的结果得到一个初始的公共前缀长度,就是“回文串”变成了最长公共前缀,回文串的中心 $ mid $ 变成了最长公共前缀的左端点 $ l $ ,最后 $ min $ 函数长的不一样了。

我们对于 $ a $ 处理出它【每一位开始的后缀】和【 $ a $ 本身】的最长公共前缀 $ f[i] $ 。注意我们要从第二位开始 ,因为 $ f[1]=|a| $ 很特殊。我们用 $ l $ 记录已经遍历过的 $ i $ 中 $ i+f[i] $ 最大的 $ i $ ,同时用 $ r $ 记录 $ i+f[i] $ ,这两者初始均为 $ 0 $ 。然后: $ f[i]=min(~r-i~,~f[i-l+1]~) $ ,这个计算初始值的式子和 $ Manacher $ 很像, $ i $ 处于一个公共前缀里,那么 $ i $ 后面的字母一定和 $ f[i-l+1] $ 后面的字母在 $ r-i $ 范围内保持一致,于是直接拿来作为初始值即可。复杂度证明和上面 $ Manacher $ 一样!

$ code $ :

int n,m;
int f[2000005];
string s,a;

int main(){
    cin>>a; n=a.size();
    cin>>s; m=s.size();
    s=' '+s+'#'+a; n+=m+2; //加到一起
    rg l=0,r=0; f[1]=m; //第一个不管
    for(rg i=2;i<=n;++i){
        if(i<=r) f[i]=min(r-i,f[i-l+1]); //确定初始值
        while(s[f[i]+1]==s[i+f[i]]) ++f[i]; //向后扩展
        if(i+f[i]-1>r) l=i,r=i+f[i]-1; //更新l和r的值
    }
    for(rg i=1;i<=m;++i) printf("%d%c",f[i],i==m?'\n':' ');
    for(rg i=m+2;i<n;++i) printf("%d%c",f[i],i==n-1?'\n':' ');
    return 0;
}


3. $ LOJ~3095 $ : $ Snoi~2019 $ 字符串

题目概括:给一个长度为 $ n $ 的字符串 $ S $ ,记 $ S′_i $ 表示 $ S $ 删去第 $ i $ 个字符后得到的字符串,输出:\(( S′_1 , 1)...(S′_n , n)\) 的字典序排序结果。 $ n\leq 10^6 $

我们仔细观察题目,手算分析样例,可以发现一个性质:

  1. 设从 $ i $ 号位置开始,往后第一个与 $ i $ 号位不同的位置为 $ j $ ,则 $ [i,j-1] $ 的字符串相等。
  2. 若 $ a[i]<a[j] $ ,则可以发现后面的字符串因为多包含一个 $ a[i] $ 而小于 $ [i,j-1] $ 。
  3. 若 $ a[i]>a[j] $ ,则可以发现后面的字符串因为多包含一个 $ a[i] $ 而大于 $ [i,j-1] $ 。

于是我们开一个双端加入的数组,记录最后的答案。期望复杂度: $ O(n) $

扫描二维码关注公众号,回复: 7378227 查看本文章

$ code $ :

    n=qr(); cin>>a; a=' '+a; a[n+1]=')'; //防出界
    rg l=0,r=n+1; //记录双端指针
    for(rg i=1;i<=n;++i){ rg j=i;
        while(a[i]==a[i+1])++i; //找到连续相同串的右端点
        if(a[i+1]<a[i]) for(rg k=j;k<=i;++k) s[++l]=k; //这一段都必然比后面的串小
        else for(rg k=i;k>=j;--k) s[--r]=k; //这一段都必然比后面的串大
    }
    for(rg i=1;i<=n;++i) printf("%d ",s[i]);


4. 洛谷 $ P5446 $ : $ THUPC~2018 $ 绿绿和串串

题意概括:对于字符串 $ S $ ,定义运算 $ f(S) $ 为将 $ S $ 的前 $ |S|-1 $ 个字符倒序接在 $ S $ 后面形成的长为 $ 2|S|-1 $ 的新字符串。现给出字符串 $ T $ ,询问有哪些串长度不超过 $ |T| $ 的串 $ S $ 经过若干次 $ f $ 运算后得到的串包含 $ T $ 作为前缀。$ 1 \le |T| \le 5 \times 10^6. $

很纠结的一道题,调了好一会才发现思维不够严谨,判断出错了。

首先我们不难发现这是一道与回文串有关的题目,我们可以发现如果 $ S $ 串中存在一个位置 $ i $ 使得它的最长回文串能到达 $ S $ 串的末尾,那么对 $ [1,i] $ 这一段字符进行 $ f(1,i) $ 的操作一定可以得到一个合法新字符串使 $ S $ 为其前缀。然后我们还可以发现如果将这些位置标记,如果 $ S $ 串中还有一些位置 $ i $ 使得从 $ i $ 扩展的回文串可以到达 $ S $ 串的第一个字符且这个回文串的末尾字符是被标记了的,那么这个位置也是合法的!因为只要进行多次 $ f $ 操作即可。

$ code $ :

    t=qr();
    while(t--){
        cin>>s; n=s.size(); s=' '+s; //读入
        rg mid=0,r=0; s[0]='('; s[n+1]=')'; //设边界
        for(rg i=1;i<=n;++i){ f[i]=0;
            if(i<r) f[i]=min(r-i,f[(mid<<1)-i]); //manacher寻找初值
            while(s[i-f[i]-1]==s[i+f[i]+1]) ++f[i]; //扩展
            if(i+f[i]>r) mid=i,r=i+f[i]; //更新
        } k[n]=1;
        for(rg i=n;i>=1;--i) //如果转一次就合法,或者能够连转多次
            if(i+f[i]==n||(i-f[i]==1&&k[i+f[i]])) k[i]=1;
        for(rg i=1;i<=n;++i)
            if(k[i])printf("%d ",i),k[i]=0; //输出+清零
        puts("");
    }


5. 洛谷 $ P4503 $ :$ CTSC~2014~$ 企鹅 $~QQ $

题意概括: 给出一个带通配符的字符串 $ S $ ,通配符分两种,一种可以匹配恰好一个字符,另一种可以匹配任意个(包括 $ 0 $ 个)字符。再给出 $ n $ 个串 $ T_i $ ,询问 $ T_i $ 能否与 $ S $ 匹配。 $ 1 \le n \le 100, 1 \le |S|,|T_i| \le 10^5, 0 \le $ 通配符个数 $ \le 10. $

总算搞了一道 $ Hash $ 题了,应该算是第一道仔细调了细节的代码。对于 $ Hash $ 我们有单哈希和多哈希,根据元素信息的维数来看。但是个人还是喜欢单哈希里的 $ long~long $ ,效果和双 $ int $ 哈希差不多。 $ long~long $ 好写,跑的飞快,但是模数太大容易溢出;双 $ int $ 哈希(一般用 $ STL:pair $ ),跑的不快,但是正确性高很多。

这道题我们可以采取暴力措施,因为只有一位可以不同,所以我们干脆枚举这一位是哪一位,然后将每个字符串除开这一位的前缀和后缀相互比较,如果相同那么就是合法的一对。前缀和后缀的比较可以预处理 $ Hash $ 得到。

期望复杂度: $ O(m\times n\times logn) $

$ code $ :

const ll mod=20190816170251; //纪念CCF关门的日子是个质数
//小常识:1e18+3和1e18+9都是质数很好记,998244353和998244853和993244853也是。

ll ans;
int n,m,k;
ll q[30005];
ll f[30003][202]; //前缀hash
ll s[30003][202]; //后缀hash
char a[30005];

int main(){
    n=qr(); m=qr(); k=qr();
    for(rg i=1;i<=n;++i){
        scanf("%s",a+1); //scanf读入快一点     //107这个数不要太大,防溢出
        for(rg j=1;j<=m;++j) f[i][j]=(f[i][j-1]*107+a[j])%mod; //前缀hash
        for(rg j=m;j>=1;--j) s[i][j]=(s[i][j+1]*107+a[j])%mod; //后缀hash
    } ll x=1;                //模数不能太大,否则这里会炸掉
    for(rg i=1;i<=m;++i){ //每个位置分开做会快些,正确性也高一些
        for(rg j=1;j<=n;++j)
            q[j]=f[j][i-1]*x+s[j][i+1]; //前后合并
        sort(q+1,q+n+1);
        for(rg j=1,o=1;j<=n;j=++o){
            while(q[j]==q[o+1]) ++o; //找到连续一段相同的
            ans+=((ll)(o-j+1)*(o-j))>>1; //注意答案贡献为C[x][2],从一段中取一对
        } x=x*107%mod;
    }
    printf("%lld\n",ans);
    return 0;
}

猜你喜欢

转载自www.cnblogs.com/812-xiao-wen/p/11600101.html