天天看点

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

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

$ By~wcwcwch $

一、字符串专题:

1. 【模板】$ manacher $ 算法

模型:

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

$ solution $ :

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

于复杂度最关键的一句话: $ 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 $ 的每一个后缀】的最长公共前缀 。

$ solution $ :

$ 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 $

$ solution $ :

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

  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) $

$ 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. $

$ solution $ :

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

首先我们不难发现这是一道与回文串有关的题目,我们可以发现如果 $ 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. $

$ solution $ :

总算搞了一道 $ 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;
}                

6. 洛谷 $ P3167 $ : $ CQOI~2014 $ 通配符匹配

题意概括:

给出一个带通配符的字符串 $ 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. $

$ solution $ :

想法颇多,但实现艰巨的一道题,写了一晚上加一上午。

讲课时和 $ Itst $ 讨论觉得是 $ AC $ 自动机,结果大鸽鸽一讲是 $ Hash $ ,题解第一篇居然也是 $ Hash $ ?!但是 $ AC $ 自动机本就和 $ Hash $ 完成的东西一样,都要求两段子串是否相等, $ AC $ 自动机预处理, $ Hash $ 在线 $ O(1) $ 求,所以同出一源。

首先这道题的突破口是通配符无疑!因为数量太少,我们可以根据通配符划分模式串,得到不多于 $ 10 $ 个模式串。然后我们可以考虑在给的 $ n $ 个串中找到这些模式串的位置,然后想办法合并这些位置以判断是否合法。想法是好的,实现起来却十分残酷,首先 $ AC $ 自动机找串复杂度 $ |S|^2 $ ,直接扑街;但是我们很快发现模式串不多,而且只需要知道其是否在某个位置出现,我们考虑状压,每次不暴力访问,直接位运算(期望复杂度 $ O(|S|) $ )。

然后我们发现实现这个匹配是非常难的。但是我们也可以发现匹配是线性的按顺序的匹配,和动态规划很像,于是我们考虑动态规划,对每一个位置记录他可以匹配完哪一个的模式串。然后我们对于每一个状态(某一个位置是某一个模式串的末尾)可以根据这个模式串之前是哪一个通配符来转移,如果是 $ ? $ 那么直接找到模式串开头前一个位置,看它是否匹配了上一个模式串;如果是 $ * $ 就找之前所有状态,是否有匹配了上一个模式串的位置(用前缀或(位运算)和)。利用状压我们可以直接位运算解决所有转移。

$ code $ :
#include<iostream>
#include<cstdio>
#include<iomanip>
#include<algorithm>
#include<cstring>
#include<cstdlib>
#include<ctime>
#include<cmath>
#include<vector>
#include<queue>
#include<map>
#include<set>

#define ll long long
#define db double
#define rg register int

using namespace std;

int n,m,t;
int s[10005];
int b[10005];
int mx[100005];
int f[100005];
int k[100005];
char a[100005];

struct AC{
    int tt;
    int end[100005];
    int fail[100005];
    int son[100005][26];
    inline void add(int l,int r,int v){
        rg now=0,*to;
        for(rg i=l;i<=r;++i){
            to=&son[now][a[i]-'a'];
            if(*to)now=*to;
            else now=*to=++tt;
        }end[now]|=(1<<v); //将这个模式串编号状压到自动机里面
    }
    inline void failed(){
        queue<int> q;
        for(rg i=0;i<26;++i)
            if(son[0][i]){
                end[son[0][i]]|=end[0]; //fail树的应用,这个位置的字符串包含哪些模式串
                q.push(son[0][i]);
            }
        rg *to,next;
        while(!q.empty()){
            rg i=q.front(); q.pop(); next=fail[i];
            for(rg j=0;j<26;++j){
                to=&son[i][j];
                if(*to){ q.push(*to);
                    fail[*to]=son[next][j];
                    end[*to]|=end[son[next][j]]; //fail树的应用,这个位置的字符串包含哪些模式串
                }else *to=son[next][j];
            }
        }
    }
    inline void find(int l){ //找到文本串里每个位置对应哪些模式串末尾
        rg now=0; f[0]=end[now];
        for(rg i=1;i<=l;++i){
            now=son[now][a[i]-'a'];
            f[i]=end[now]; //这个位置是那些模式串的末尾
        }
    }
}T;

inline int qr(){
    register char ch; register bool sign=0; rg res=0;
    while(!isdigit(ch=getchar()))if(ch=='-')sign=1;
    while(isdigit(ch))res=res*10+(ch^48),ch=getchar();
    if(sign)return -res; else return res;
}

int main(){
    scanf("%s",a+1); m=strlen(a+1); a[++m]='s'; //最后加个字符是为了防通配符在末尾
    for(rg i=1,j=1,x=0;i<=m;i=++j){
        while(j<=m&&a[j]>='a'&&a[j]<='z')++j; //以通配符为界,划分为若干模式串
        T.add(i,j-1,++t); s[(1<<t)]=x; b[(1<<t)]=j-i; //记录上一个通配符是哪一种,以及串长
        if(j<=m&&a[j]=='?') x=1; else x=2; //辨别这个通配符是哪一种
    }
    T.failed(); rg n=qr();
    for(rg i=1;i<=n;++i){ //每个待匹配串都视为文本串,跑AC自动机
        scanf("%s",a+1); m=strlen(a+1);
        a[++m]='s'; T.find(m); k[0]=mx[0]=1; //预处理+初始化
        for(rg j=1;j<=m;++j) k[j]=mx[j]=0; //初始化
        for(rg j=0;j<=m;++j){
            for(rg x=f[j];x;x-=x&-x){ //这层循环的复杂度为通配符个数
                rg y=x&-x;
                if(s[y]==0) k[j]|=y&(k[j-b[y]]<<1); //如果之前没有通配符
                if(s[y]==1) k[j]|=y&(k[j-b[y]-1]<<1); //上一个通配符是?就直接看前面对应位置是否可以匹配
                if(s[y]==2) k[j]|=y&(mx[j-b[y]]<<1); //上一个通配符是*就直接看全局,因为中间字符可消
                mx[j]|=k[j]; //这个对于*操作很重要,记录这个位置之前的最高匹配度
            } mx[j+1]=mx[j]; //这个对于*操作很重要,记录这个位置之前的最高匹配度
        }
        if(k[m]&(1<<t))puts("YES"); //看最后一位的匹配度是否达到最高
        else puts("NO");
    }
    return 0;
}
                

7. 洛谷 $ P3193 $ : $ HNOI~2008~GT $ 考试

题意概括:

给一个数字串 $ S $ ,求有多少长度为 $ n $ 的数字串 $ T $ 不包含 $ S $ 作为子串。 $ 1 \le |S| \le 100, 1 \le n \le 10^9. $

$ solution $ :

并没有想象中(上一道题)的这么难,唯独就是要写严格 $ O(1) $ 的 $ kmp $ 算法。感觉比普通的还好写一点。

首先我们求的串中必须不包含 $ S $ ,这个我们其实不难想到动态规划。设 $ F[i][j] $ 表示已经构造到第 $ i $ 个位置,在末尾最多有 $ j $ 个位置和 $ S $ 的前 $ j $ 个位置一致。之所以这样设状态是因为我们构造合法串时只需要知道串后面的字符和 $ S $ 的匹配程度,这样可以知道我们放下一个字符会不会使得一段后缀变为 $ S $ (任何一个子串都可以表示为一个前缀的后缀)。然后我们就需要转移方程,这个其实就是一个 $ kmp $ 的过程。但是为了快速转移我们需要知道在当前串后面加入某个字符会使匹配转移到哪一个位置!这个需要用严格 $ O(1) $ 的 $ kmp $ 算法。再然后我们就可以将这个转移过程用矩阵来完成,通过改变矩阵系数使得一次转移等同一次乘法。然后快速幂。

$ code $ :
int n,m,k;
int nx[105];
int f[105][10];
char a[105];

struct su{ //矩阵
    int s[101][101];
    inline void operator *=(const su &x){ //矩阵乘法
        su res;
        for(rg i=0;i<m;++i){
            for(rg j=0;j<m;++j){
                rg y=0;
                for(rg k=0;k<m;++k)
                    y+=s[i][k]*x.s[k][j];
                res.s[i][j]=y%k; //有时候将取模放外面会快些
            }
        } *this=res;
    }
    inline su operator ^(int y){ //矩阵快速幂
        su res,x=*this;
        for(rg i=0;i<m;++i) res.s[i][i]=1;
        while(y){
            if(y&1)res*=x;
            x*=x; y>>=1;
        }return res;
    }
}ans,xx;

int main(){
    n=qr(); m=qr(); k=qr();
    scanf("%s",a+1);
    ans.s[0][0]=1;
    for(rg i=1;i<=m;++i){
        nx[i]=f[i-1][a[i]-'0']; //似乎比不严格的好写?
        f[i-1][a[i]-'0']=i; //f[i][j]表示从这一位在后面加字符j会匹配到哪个位置
        for(rg j=0;j<=9;++j) //复杂度比一般kmp高一点
            f[i][j]=f[nx[i]][j]; //就是严格O(1)的kmp
    }
    for(rg i=0;i<m;++i)
        for(rg j=0;j<=9;++j)
            ++xx.s[i][f[i][j]]; //加入矩阵系数
    ans*=xx^n; int tot=0; //矩阵快速幂
    for(rg i=0;i<m;++i)
        tot+=ans.s[0][i]; //统计所有不含所给串的串的个数
    printf("%d\n",tot%k);
    return 0;
}
                

8. 洛谷 $ P2414 $ $ NOI~2011 $ 阿狸的打字机

题意概括:

给你一棵 $ n $ 个节点的 $ Trie $ 树, $ q $ 次询问 $ Trie $ 树上 $ x $ 节点代表的字符串在 $ y $ 节点代表的字符串中出现了多少次。

$ 1 \le n, q \le 10^6. $

$ solution $ :

很难的一道题,可以说是很多算法的结合: $ AC $ 自动机, $ fail $ 树, $ dfs $ 序,树状数组

首先是读入,一般读入肯定超时,因为放进 $ AC $ 自动机的字符串很多很长,但是他们很多前缀相同。于是根据题意模拟,我们在 $ trie $ 数上跑操作序列,只要记录每一个节点的父亲,就可以支持加入字符后的撤销操作,然后打印操作直接在当前 $ trie $ 的节点处打标机即可(详细见代码)

然后是关于 $ fail $ 树,我们对于每一个 $ fail $ 指针件一条反向边,因为一个节点只有一个 $ fail $ 指针,得到的图一定是棵有根树。然后我们思考这棵树的意义:(假设每个节点对应一个从根到这个节点的字符串)我们在普通情况下从某个节点(对应字符串 $ S $ )沿 $ fail $ 指针跑,遍历的字符串都是 $ S $ 的子串且是后缀!于是反过来,我们在 $ fail $ 树上从某个节点(对应 $ S $ )遍历其子树,遍历到的字符串都一定包含 $ S $ 作为子串,且 $ S $ 是它们后缀!

然后我们还知道,一个子串一定可以表示成母串的前缀的后缀。于是如果我们要在 $ trie $ 上找 $ x $ 节点代表的字符串在 $ y $ 节点代表的字符串中出现了多少次,我们只需要将 $ y $ 字符串的每一个前缀对应节点(在 $ trie $ 上为到根链)都标记,然后在 $ fail $ 树上的 $ x $ 好节点开始遍历子树,有多少节点被标记那么 $ x $ 节点代表的字符串在 $ y $ 节点代表的字符串中出现了多少次。

但是我们发现询问很多,字符串长度很大。但是我们可以离线:因为字符串的所有前缀对应节点在 $ trie $ 上为一条到根的链,我们马上回忆到一种题型(在 $ dfs $ 一棵树时维护节点到根路径的信息:在遍历到某一节点时加入节点贡献,在离开节点时删掉节点贡献)。再联想一下我们需要知道 $ fail $ 的子树信息和,我们就可以想到先找到树的 $ dfs $ 序,用树状数组维护以 $ dfs $ 序为对应位置的数组,然后遍历整棵 $ trie $ 树,在遍历到某一节点时给树状数组对应位置加一,在离开节点时减一。然后如果遍历 $ trie $ 树时,当前节点为询问中的 $ y $ 字符串(所有前缀都以标记),就到树状数组里找到对应询问 $ x $ 字符串对应位置的子树和。

$ code $ :
#include<iostream>
#include<cstdio>
#include<iomanip>
#include<algorithm>
#include<cstring>
#include<cstdlib>
#include<ctime>
#include<cmath>
#include<vector>
#include<queue>
#include<map>
#include<set>

#define ll long long
#define db double
#define rg register int

using namespace std;

int tim;
int n,m,tot;
int a[100005];
int tr[100005];
int dfn[100005];
int low[100005];
int ans[100005];

struct bian{
    int to,next;
}b[1000005];
int tou[100005],top;

struct lian{
    int to,res,id,next;
}q[100005];
int hd[100005],len;

struct AC{
    int tt;
    int fa[100005];
    int end[100005];
    int fail[100005];
    int son[100005][26];
    bool vis[100005][26];
    inline void add(const string &ch){
        rg now=0,*to,l=ch.size();
        for(rg i=0;i<l;++i){
            if(ch[i]>='a'&&ch[i]<='z'){
                to=&son[now][ch[i]-'a'];
                if(*to) now=*to; 
                else *to=++tt,fa[*to]=now,now=*to;
            }
            if(ch[i]=='B') now=fa[now]; //额外计一个父亲用来撤销
            if(ch[i]=='P') a[++n]=now;
        }
    }
    inline void failed(){
        queue<int> q;
        for(rg i=0;i<26;++i){
            if(son[0][i]){
                fail[son[0][i]]=0;
                q.push(son[0][i]);
            }
        }
        rg *to,next;
        while(!q.empty()){
            rg i=q.front(); q.pop(); next=fail[i];
            for(rg j=0;j<26;++j){
                to=&son[i][j];
                if(*to){ q.push(*to);
                    fail[*to]=son[next][j];
                }else vis[i][j]=1,*to=son[next][j];
            } //因为之后需要遍历原始字典树,所以需要一个vis数组记录更改
        }
    }
    inline void get_eg(){
        for(rg i=1;i<=tt;++i){ rg x=fail[i],y=i;
            b[++top]=bian{y,tou[x]}; tou[x]=top;
        } //根据fail指针建fail树的边(反边)
    }
}ac;

inline int qr(){
    register char ch; register bool sign=0; rg res=0;
    while(!isdigit(ch=getchar()))if(ch=='-')sign=1;
    while(isdigit(ch))res=res*10+(ch^48),ch=getchar();
    if(sign)return -res; else return res;
}

inline void add(int x,int v){
    for(;x<=tim;x+=x&-x) tr[x]+=v;
} //需要注意树状数组的范围

inline int ask(int x){ rg res=0;
    for(;x;x-=x&-x) res+=tr[x];
    return res;
}

inline void yu(int i){
    dfn[i]=++tim;
    for(rg j=tou[i];j;j=b[j].next) yu(b[j].to);
    low[i]=tim;
} //有向树的dfs序,dfn和low记录子树区间

inline void dfs(int i){
    add(dfn[i],1);
    for(rg j=hd[i];j;j=q[j].next){
        q[j].res=ask(low[q[j].to])-ask(dfn[q[j].to]-1);
    } //用链式前向星记录询问
    for(rg j=0;j<26;++j)
        if(ac.son[i][j]&&!ac.vis[i][j])
            dfs(ac.son[i][j]);
    add(dfn[i],-1);
} //遍历原始字典树是因为每一个子串都是前缀的后缀
//字典树遍历所有前缀,而fail树的dfs序与前缀的后缀有关

int main(){
    //freopen(".in","r",stdin);
    //freopen(".out","w",stdout);
    string ch; getline(cin,ch); ac.add(ch);
    ac.failed(); ac.get_eg();
    yu(0); m=qr();
    for(rg i=1;i<=m;++i){
        rg x=a[qr()],y=a[qr()]; //将询问具体到哪个字符串
        q[i]=lian{x,0,i,hd[y]}; hd[y]=i;
    } dfs(0);
    for(rg i=1;i<=m;++i) ans[q[i].id]=q[i].res;
    for(rg i=1;i<=m;++i) printf("%d\n",ans[i]);
    return 0;
}                

9. 拯救紫萱学姐(试题)

题意概括:

定义对于两个字符串 $ a $ 和 $ b $ ,如果 $ a $ 既是 $ b $ 的前缀也是 $ b $ 的后缀,那么称 $ a $ 和 $ b $ 相似(空字符串和任何字符串相似)。一个字符串可以通过编辑变换成一个比它短而且与它相似的字符串,付出的代价为这两个字符串的长度之差的平方。两个字符串通过编辑而变为同一个字符串所花费的最小代价被称为最短编辑距离。现给定一个字符串,你需要知道这个字符串的每一对前缀的最短编辑距离中的最大值是多少。

$ 1 \le |S| \le 10^6 $

$ solution $ :

其实看到 $ a $ 既是 $ b $ 的前缀也是 $ b $ 的后缀时,就可以想到 $ kmp $ 思想。然后我们就会发现每一次编辑变换,就是沿着 $ kmp $ 的 $ next $ 数组往前跳。然后为了代价最小,我们肯定每次只跳一步。于是可以得到一个结论,对于两个前缀他们沿 $ next $ 数组向前跳,第一个相同的位置所对应的前缀就是最终状态(类似于将 $ next $ 数组反向连边,在树上跑 $ LCA $ )。于是我们根据 $ next $ 数组从后往前遍历,每次用当前位置跟新它 $ next[] $ 对应位置的最大代价,同时记录答案。

$ code $ :
ll ans;
int n;
int f[2000005];
ll s[2000005];
char a[2000005];

int main(){
    scanf("%s",a+1); n=strlen(a+1); f[1]=0;
    for(rg i=2,j=0;i<=n;++i){ //kmp
        while(j&&a[i]!=a[j+1]) j=f[j];
        if(a[i]==a[j+1]) f[i]=++j; //next数组
    }
    for(rg i=n;i>=1;--i){
        ll j=f[i];
        ll x=(ll)(i-j)*(i-j); //记录这一步的代价
        ans=max(ans,s[i]+x+s[j]); //更新答案
        s[j]=max(s[j],s[i]+x); //更新最大代价
    }printf("%lld\n",ans);
    return 0;
}                

10. 洛谷 $ P3279 $ : $ SCOI~2013 $ 密码

题意概括:

给出一个长度为 $ n $ 的数组 $ {p_i} $ 和一个长度为 $ n $ 的数组 $ { q_i } $ ,分别表示以字符串中每个字符以及每两个相邻字符的间隙为中心的最长回文串长度。你需要根据给出的 $ { p_i } $ 和 $ {q_i} $ ,构造出一个满足条件的字符串 $ S $ 。 $ n \le 10^6 $

$ solution $ :

将 $ manacher $ 倒着做一遍,从前往后构造字符串。

  1. 因为回文串对称性质,前面一般直接赋值到后面一半。
  2. 因为回文串对称性质,我们记录一个 $ bool $ 数组,回文串的后面第一个字符不能等于回文串前一个字符。

然后贪心放小的字符即可。

$ code $ :
int n;
int a[1000005];
int b[1000005];
int s[1000005];
bool f[1000005][27];

int main(){
    n=qr(); rg r=1; s[0]=26;
    for(rg i=1;i<=n;++i)a[i]=qr()>>1; //注意
    for(rg i=1;i<n;++i) b[i]=qr()>>1;
    for(rg i=1;i<=n;++i){
        if(i>=r){ ++r; //贪心放最小的
            for(rg j=0;j<26;++j)
                if(!f[i][j]){s[i]=j; break;}
        } rg x=i-a[i],y=i+a[i]; //记录边界
        f[y+1][s[x-1]]=1; //后面第一个字符不能等于回文串前一个字符
        while(r<=y) s[r]=s[i*2-r],++r;
        x=i-b[i]+1,y=i+b[i]; //同上
        f[y+1][s[x-1]]=1;
        while(r<=y) s[r]=s[i*2-r+1],++r;
    }
    for(rg i=1;i<=n;++i)
        printf("%c",s[i]+'a');
    puts("");
    return 0;
}                

11. \(LOJ~6187~Odd\)

题意概括:

有一个 $ n $ 个数的数组 $ A $ 。求有多少个子区间,满足每个区间里出现过的数都只出现了奇数次。

$ n \leq 2 \times 10^5, a_i \leq 10^6 $

$ solution $ :

很难调的一道题,细节很多,小技巧也很多。(似乎上一道我这么认为的题也是分块?)

我们考虑这道题的“奇偶”二字,说明本题很可能和状压、位运算什么的有关。考场上以为状压一下、求个前缀异或和、再开个桶就可以解决,奈何数据范围大、“出现过”三字太毒瘤!但是仔细摸索一番我们不难想到 $ Hash $ 。因为我们要求每个区间,所以很直接的想法就是枚举一个端点,寻找合法的另一个端点。而要快速知道一个区间内数的奇偶性,我们只能依靠异或操作,和后缀和(通过异或操作,使得合法区间一定满足异或和为 $ 0 $ ,这样根据右端点后缀异或和就能得到左端点后缀异或和,然后查存后缀异或和的桶子就好)

为了能够方便异或操作,我们给每个值随机一个大数,这样正确性会很高(总共后缀异或和的数量为 $ n $ 个,值域为 $ 2^{63} $ ,安全)。然后我们枚举右端点 $ r $ 并实时维护后缀(显然右端点以右的点的后缀异或和不需要管,因为两次异或得0)。因为合法区间的异或和为 $ 0 $ 而区间里的数又是奇数次,所以我们可以忽略每一个数的最后一个。于是当我们右端点向右移时假设下一个数为 $ v_i $ ,那么对于前面所有后缀,它肯定是里面等于 $ v $ 的书中最后一个。(假设上一个 $ v $ 出现位置为 $ j $ )于是从 $ [1,j] $ 的所有后缀均需要异或 $ v $ ( $ [j+1,i] $ 不需要!)。然后因为右端点后缀为 $ 0 $ ,合法区间异或和为零,我们只需要知道前面有多少位置的后缀异或和为 $ 0 $ 即可!

然后讲一下实现,首先我们需要随机 $ rand $ 大数。然后我们需要记录每一个位置的后缀异或和,并支持区间(某个前缀区间)异或修改,以及区间(前缀区间)查询 $ 0 $ 的出现次数!因为值域很大所以我们得考虑 $ Hash $ 表查询,对于区间修改查询我们可以线段树,但是线段树被卡内存了,仅分块还活着!然后对于区间异或我们可用类似线段树的 $ lazytag $ 一样给每个块打标记!查询这个块时有多少0,就是查询有多少 $ 0~xor~lazytag $ !(分块操作有点绕,要仔细分析)

注意 $ Hash $ 表因为分块的缘故需要支持删除和加入,我们要链表删除,回收空间!不能暴力更改,空间会爆!

$ code $ :
ll ans;
int n,m;
int a[200005];
ll b[1000005]; //随机化权值
ll s[1000005]; //后缀(随着右端点向右移,会变化的后缀)
int t[1000005]; //上一个出现位置
int f[200005]; //分块

struct Hash{
    int tt,top;
    ll vv[505]; //数值
    int da[505]; //数量
    int to[505]; //链表指向
    int stc[505]; //我删掉的位置(回收栈)
    int hd[2005];
    inline void add(ll x,int v){
        rg y=x%2003; rg i=0; //先Hash一下
        for(rg j=hd[y];j;i=j,j=to[j]){ //链式前向星存储
            if(vv[j]==x){
                da[j]+=v; //更新权值
                if(!da[j]){
                    stc[++top]=j; //计入回收栈
                    if(!i) hd[y]=to[j]; //链表删除
                    else to[i]=to[j];
                } return ;
            }
        }
        i=top?stc[top--]:(++tt); //找到一个没用的位置
        vv[i]=x; da[i]=1; to[i]=hd[y]; hd[y]=i; //存储
    }
    inline int ask(ll x){
        for(rg j=hd[x%2003];j;j=to[j]) //链式前向星访问
            if(vv[j]==x) return da[j];
        return 0;
    }
}ha[505];
ll lz[505];

int main(){
    srand(time(NULL)); //随机化
    n=qr(); m=sqrt(n-1)+1; //分块的大小
    for(rg i=1;i<=n;++i) f[i]=(i-1)/m+1; //这个下标在哪个块
    for(rg i=1;i<=n;++i) a[i]=qr(),b[a[i]]=rand()+((ll)rand()<<31); //随机权值
    for(rg i=1;i<=n;++i){
        ha[f[i]].add(0,1); //加入初始值
        if(t[a[i]]){ rg x=f[t[a[i]]]; ll v=b[a[i]]; //上一个出现的位置;该位置的随机权值
            for(rg j=1;j<x;++j) lz[j]^=v; //块上打标记
            for(rg j=m*(x-1)+1;j<=t[a[i]];++j)
                ha[x].add(s[j],-1), s[j]^=v, ha[x].add(s[j],1); //先取出,后修改,再加入
        } t[a[i]]=i; //该位置变为最后一个出现位置
        for(rg j=1;j<=f[i];++j) ans+=ha[j].ask(lz[j]); //询问(直接访问大块就可以了!)
    }
    printf("%lld\n",ans);
    return 0;
}                

二、搜索专题:

$ hncpc~2019E~Numbers $

题意概括:

给一个数字串 $ S $ ,求将其划分成 $ [0,99] z$ 不含前导零且互不相同的数的方案数。

$ solution $ :

湖南师范大学的 $ ACM $ 比赛题,没有 $ source $ 。反正比较简单,直接口糊。

爆搜,枚举每个位置是否是小于 $ 10 $ 的位置,其他位置的数就确定了。于是边搜索边 $ check $ 就好了!

题意概括:
$ solution $ :
$ code $ :

三、图论专题:

题意概括:
$ solution $ :
$ code $ :

四、数据结构专题:

题意概括:
$ solution $ :
$ code $ :

转载于:https://www.cnblogs.com/812-xiao-wen/p/11600101.html