字符串全纪录

字符串可以说是我比较薄弱的一块(想当初c++入门的时候就没有系统的学过字符串的读入。。。)

KMP

KMP算法是一种改进的字符串匹配算法
用于解决询问串中出现了多少次模式串
可以说是字符串中最简单的一种算法了

匹配串的时候,如果匹配成功,模式串指针也要跳到失配上,便于下一步的匹配

//字符串下标从0开始
int t[N];

void KMP() {
    t[0]=-1;
    int len=strlen(s);
    for (int i=0;i<len;i++) {
        int p=t[i];
        while (p!=-1&&s[p]!=s[i]) p=t[p];
        t[i+1]=p+1;
    }
}

void solve() {
    int len=strlen(S),l=strlen(s);
    int i,j=0;
    for (int i=0;i<len;i++) {
        while (S[i]!=s[j]&&j!=-1) j=t[j];
        j++;
        if (j==l) printf("%d\n",i-l+1),j=t[j];
    }
}

Manacher

manacher详解
manacher算法,计算以每个字符为中心的最长回文串
裸题不多,但是和其他算法的结合难度不小

经典例题:manacher+FFT
难题:manacher+双hash

//字符串下标从1开始
const int N=100010;
char s[N],ss[N<<1]; 
int RL[N];

int prepare() {
    int len=strlen(ss+1);
    s[0]='@';
    for (int i=1;i<=len;i++) {
        s[2*i-1]='#';
        s[2*i]=ss[i];
    }
    s[2*len+1]='#';
    s[2*len+2]='$';
    return 2*len+1;
}

void manacher() {
    int len=prepare();
    int mx=0,pos=0;
    for (int i=1;i<=len;i++) {
        int j=pos*2-i;
        if (i<mx) RL[i]=min(mx-i,RL[j]);
        else RL[i]=1;           //如果i>=mx,要从头开始匹配

        while (s[i+RL[i]]==s[i-RL[i]]) RL[i]++;

        if (i+RL[i]>mx) {
            mx=i+RL[i];
            pos=i;
        }
    }
}

AC自动机

Aho-Corasick automaton,著名的多模匹配算法
前面介绍的KMP算法可以解决字符串匹配的问题,然而如果模式串有多个,我们就需要一种全新的算法:AC自动机

AC自动机总结

因为AC自动机的题本来就做的不多,所以还是多看看题(蓝皮上的讲解还是不错的)

经典例题:AC+fail
经典例题:AC+dp(一)
经典例题:AC+dp(二)
经典例题:AC+dp+矩阵加速(一)
经典例题:AC+dp+矩阵加速(二)
经典例题:AC+dp+矩阵加速(三)

贴一个daloa的刷题列表

AC自动机:给出字典集和一个长串,询问长串中每个模式串出现的次数

const int N=100010;
int ch[N][26],sz=0,n,m,root=0,fail[N];
int ed[N],word[N],cnt[N],q[N];
char s[N]; 

void insert(int bh) {
    int len=strlen(s);
    int now=root;
    for (int i=0;i<len;i++) {
        int x=s[i]-'a';
        if (!ch[now][x]) ch[now][x]=++sz;
        now=ch[now][x];
    }
    ed[now]=1;
    word[bh]=now;                //记录单词的ed 
} 

void make_fail() {
    int tou,wei;
    tou=wei=0;
    for (int i=0;i<26;i++)
        if (ch[root][i])
            q[++wei]=ch[root][i];
    while (tou<wei) {
        int now=q[++tou];
        for (int i=0;i<26;i++) {
            if (!ch[now][i]) {
                ch[now][i]=ch[fail[now]][i];
                continue;
            }
            fail[ch[now][i]]=ch[fail[now]][i];
            ed[ch[now][i]]|=ed[fail[ch[now][i]]];
            q[++wei]=ch[now][i];
        }
    }
}

void solve() {
    int len=strlen(s);
    int now=root;
    for (int i=0;i<len;i++) {
        int x=s[i]-'a';
        while (now&&!ch[now][x]) now=fail[now];
        now=ch[now][x];
        cnt[now]++;
        int f=fail[now];
        while (f) {              //沿着fail跑 
            cnt[f]++; f=fail[f];
        }
    }
    //cnt[word[i]]就是模式串i在长串中的出现次数 
}

void build() {
    scanf("%d",&n);
    for (int i=1;i<=n;i++) {
        scanf("%s",s);
        insert(i);
    }
    make_fail();
    scanf("%s",s);
    solve();
}

上面给出的是比较暴力的做法,还有一种比较优美的做法
我们每次只在匹配时经过的结点 的cnt++,最后利用make_fail时得到的bfs序,逆序累加到fail上即可

AC自动机:给出字典集和一个长串,询问长串中出现了多少模式串

void solve() {
    int len=strlen(s);
    int now=root;
    for (int i=0;i<len;i++) {
        int x=s[i]-'a';
        while (now&&!ch[now][x]) now=fail[now];
        now=ch[now][x];
        int f=now;
        while (f&&!cnt[f]) {
            cnt[f]++;
            f=fail[f];
        }
    }
    int ans=0;
    for (int i=1;i<=n;i++) if (cnt[word[i]]) ans++;
    printf("%d\n",ans);
} 

Trie树

Trie树

怎么说呢,感觉就是失去的fail指针的AC自动机
感觉很废,然而有些题目不能在fail指针上乱跑,这个时候就必须用到Trie了

Trie树的一个经典题目就是:
给出一个字符串集合S,定义P(S)为所有字符串的公共前缀长度*|S|
给定n个字符串,从中选出一个集合S,使P(S)最大
:建出Trie树,那么Trie上的任何一个结点到根结点的路径就是若干字符串的LCP
我们字需要dfs一遍,计算 max(deep[i]cnt[i]) m a x ( d e e p [ i ] ∗ c n t [ i ] )
其中 cnt[i] c n t [ i ] 表示 i i 子树中的叶节点数量(每个叶结点都代表一个字符串)

在构造Trie的时候,由于结点数较多,我们在存储的时候用的是左儿子右兄弟法

struct Trie{
    int son[N];          //左儿子 
    int nxt[N];          //右兄弟 
    char ch[N];          //第i个结点的字符 
    int cnt[N];          //子树的叶结点数 即该子树内包含的不同字符串个数 
    int sz=0;

    void init() {
        memset(son,0,sizeof(son));
        memset(nxt,0,sizeof(nxt));
        memset(cnt,0,sizeof(cnt));
        sz=0;
    }

    void insert(char *s) {
        int pre=0,now;
        int len=strlen(s);
        cnt[0]++;                       // 
        for (int i=0;i<len;i++) {
            bool ff=0;
            for (now=son[pre];now;now=nxt[now])
                if (ch[now]==s[i]) {ff=1;break;}
            if (!ff) 
            {
                now=++sz;
                ch[now]=s[i];
                nxt[now]=son[pre];
                son[pre]=now; 
            }
            pre=now;                    // 
            cnt[pre]++;
        }
    }

    void dfs(int now,int dep) {
        if (son[now]==0) ...
        else {
            ...
            for (int i=son[now];i;i=nxt[i]) {
                ...
                dfs(i,dep+1);
                ...
            }
            ...
        }
    }
};

后缀数组

后缀数组的原理一句两句话是说不清楚的
不过SA的应用可以说是变化多端(dada的总结

单个字符串相关问题

常见思路:构造后缀数组,然后求height数组,用这两个来求解

重复子串问题

可重叠的最长重复子串问题
因为可重复,所以这类问题比较简单,只需要求height数组的最大值
因为 height[i] h e i g h t [ i ] 表示排名为 i i i1的后缀的LCP,只需要最大值即可
时间复杂度是线性的

不可重叠的最长重复子串问题

有了不可重叠的限制稍微复杂一点,要用到 height h e i g h t 数组的性质
二分答案,将问题转化为判定性问题
假设我们二分的长度为 k k ,在SA中相近的字符串肯定会挨在一起
所以我们把连续一段heightk的后缀划分成一段,
如果有某一段满足段中最大的SA值与最小值之差大于等于 k k (在字符串中的位置相差至少k)
那么当前解就是可行的,满足条件一定不重叠
注意:这种分段的思想在后缀数组相关问题中很常用

可重叠的 k 次最长重复子串

例:(JZOJ2265. 【Usaco DEC06 Gold】Milk Patterns)给定一个字符串,求至少出现k次的最长重复子串。
先二分答案,只需判断当前段内是否出现k个后缀即可

子串计数问题

重复出现子串计数问题
例:(JZOJ1598. 文件修复)求一个字符串中有多少个至少出现两次的子串
这是比较简单的SA题,设每个后缀 rank r a n k i i ,其最多能贡献height[i]height[i1]个不同重复子串
Ans=max(height[i]height[i1],0) ∴ A n s = ∑ m a x ( h e i g h t [ i ] − h e i g h t [ i − 1 ] , 0 )

不相同子串计数问题

例:(spoj694,spoj705)给定一个字符串,求不相同的子串的个数
和上面思路大相径庭
每个后缀 k k 会产生nsa[k]+1个前缀,但是会重复计数,所以要减去前面相同的前缀
最后就是 nsa[k]+1height[k] n − s a [ k ] + 1 − h e i g h t [ k ] 个子串。

字典序第K子串问题

例:(JZOJ2824. 【GDOI2012】字符串)给出一个字符串S,问该字符串的所有不同的子串中,按字典序排第K的字串
应该勉强算计数问题吧。。。其实就是不相同子串个数的扩展
算出每个后缀贡献的不同子串个数 nsa[i]+1height[i] n − s a [ i ] + 1 − h e i g h t [ i ] ,在二分找出最终子串位置(必然是某个后缀的前缀)

连续重复子串问题

连续重复子串问题
例:给定一个字符串 L,已知这个字符串是由某个字符串S重复R次而得到的, 求R的最大值
比较简单的重复子串问题。枚举串 S S 长度k,如果 rank[1] r a n k [ 1 ] rank[k+1] r a n k [ k + 1 ] height=|L|k h e i g h t = | L | − k 那么当前答案合法
rank[i] r a n k [ i ] 表示后缀 i i 的排名)

这实际上就是求字符串最小循环节
用KMP算法一秒出解:strlen(s)next(strlen(s))

重复次数最多的连续重复子串问题

例:给定一个字符串L,求重复次数最多的连续重复子串
还是枚举子串长度 k k ,看这个长度对应每个位置(即L[0],L[k],L[k2]...)之间的LCP是否等于 k k
最长能扩到哪里,就是重复出现次数

多个字符串相关问题

常见做法:
将多个串连在一起,并且中间插入不同且没出现过的字符隔开
这种题比较多变,给出一些经典模型

一个字符串在所有字符串中出现次数问题

例:(JZOJ3258. 【TJOI2013】单词)给定N个字符串,求每个字符串在所有字符串中出现的次数
对于当前字符串S,设其在总串中起始位置为 ST[i] S T [ i ] ,那么我们在 height h e i g h t 上二分区间
[1,rank[ST[i]]],[rank[ST[i]]+1,n] [ 1 , r a n k [ S T [ i ] ] ] , [ r a n k [ S T [ i ] ] + 1 , n ] 内满足 height|S| h e i g h t ≥ | S | ,这之间的后缀数量就是答案

SA解法直接把我弄蒙了,其实这道题就是AC自动机的入门题,计算每个单词在字典集中的出现次数

字符串子串重复出现k次问题

例:(JZOJ3975. 串)给定你n个字符串,询问每个字符串有多少子串(不包括空串)是所有n个字符串中至少k个字符串的子串(注意包括本身)
SA+线段树维护

其他相关问题

字符串不同种连续子串问题
例:(JZOJ4473. 生成魔咒)给定n个操作,每个操作在字符串S尾插入一个字符,求当前操作后共有多少不同种连续子串
仔细分析,这题要减去前面的操作对当前影响(即重复的连续子串)。这个应该算是“前缀数组”吧。具体来说就是将字符串翻转后求一边SA,此时所得就是原串的前缀数组。然后在线段树维护一下。

经典例题:后缀数组+二分
经典例题:后缀数组+单调栈(难)

int sa[N],hei[N],rak[N],wx[N],wy[N],cc[N];

int cmp(int *y,int a,int b,int k) {
    int ra1=y[a];
    int rb1=y[b];
    int ra2=(a+k>=len)? -1:y[a+k];
    int rb2=(b+k>=len)? -1:y[b+k];
    return ra1==rb1&&ra2==rb2;
}

void make_sa(int len) {
    int i,j,k,m,p;
    int *x=wx,*y=wy;
    m=26;                     //字符集 

    for (i=0;i<m;i++) cc[i]=0;
    for (i=0;i<len;i++) cc[x[i]=s[i]-'a']++;
    for (i=1;i<m;i++) cc[i]+=cc[i-1];
    for (i=len-1;i>=0;i--) sa[--cc[x[i]]]=i;

    for (k=1;k<=len;k<<=1) {
        p=0;
        for (i=len-k;i<len;i++) y[p++]=i;
        for (i=0;i<len;i++) if (sa[i]>=k) y[p++]=sa[i]-k;
        for (i=0;i<m;i++) cc[i]=0;
        for (i=0;i<len;i++) cc[x[y[i]]]++;
        for (i=1;i<m;i++) cc[i]+=cc[i-1];
        for (i=len-1;i>=0;i--) sa[--cc[x[y[i]]]]=y[i];
        swap(x,y);
        x[sa[0]]=0;
        p=1;
        for (int i=1;i<len;i++)
            x[sa[i]]=cmp(y,sa[i-1],sa[i],k)? p-1:p++;
        if (p>=len) break;
        m=p;  
    }
}

void make_hei(int len) {
    for (int i=0;i<len;i++) rak[sa[i]]=i;
    hei[0]=0;
    int k=0;
    for (int i=0;i<len;i++) {
        if (!rak[i]) continue;
        int j=sa[rak[i]-1];
        if (k) k--;
        while (s[i+k]==s[j+k]&&i+k<len&&j+k<len) k++;
        hei[rak[i]]=k;
    }
}

SAM

对于自己不清楚的算法,不敢胡言乱语,一下总结的还是比较优美的
这种算法算是比较困难的了,还是需要好好理解

后缀自动机详解

感觉曲神的总结还是挺好的
有一些真的点醒了我:
我们每次新建立了一个状态,之后还有两件事要干:找出能转移到这个状态的状态,建立链接;确定这个状态的 min m i n ,即找到ta在 parent p a r e n t 树上的父亲
代码中的 dis d i s 实际上就是指 max m a x ,即达到此状态的最长路径(SAM上的每一条路径都代表着一个子串)

  • dis[now] d i s [ n o w ] 表示SAM中到这个结点的最长路径(能匹配的最长子串)

  • 匹配的时候 tmp t m p 表示以i为结尾的最长匹配长度

  • 如果我们在匹配的时候能到达 now n o w ,那么一定能到达 fa[now](dis[fa[now]]) f a [ n o w ] ( d i s [ f a [ n o w ] ] )

  • 在构建的时候,我们可以维护一个 size s i z e 数组, size[now]=1 s i z e [ n o w ] = 1
    如果我们不想记录重复子串, size[i]=1 s i z e [ i ] = 1
    如果我们想记录重复子串, size[fa[i]]+=size[i] s i z e [ f a [ i ] ] + = s i z e [ i ]

  • sum s u m 数组记录的是以 i i 为起点,之后又多少子串

//一个一个字符插入 
int root=1,last=1,sz=1;
int ch[N<<1][26],fa[N<<1],dis[N<<1];

void insert(int x) {
    int pre=last,now=++sz;
    last=now;
    dis[now]=dis[pre]+1;
    for (;pre&&!ch[pre][x];pre=fa[pre]) ch[pre][x]=now; //找Parent树上有没有这个结点

    //维护fa 
    if (!pre) fa[now]=root;
    else {
        int q=ch[pre][x];          //找到now可能的fa 
        if (dis[q]==dis[pre]+1) fa[now]=q;
        else {
            int nows=++sz;
            dis[nows]=dis[pre]+1;  //新建一个结点,更新max 
            memcpy(ch[nows],ch[q],sizeof(ch[q]));
            fa[nows]=fa[q]; fa[q]=fa[now]=nows;   //更新parent
            for (;pre&&ch[pre][x]==q;pre=fa[pre]) ch[pre][x]=nows;
        }
    }
}

广义后缀自动机
广义后缀自动机,可以解决多个串的问题
可以简单的视为把多个串的SAM合并在了一起
所以广义后缀自动机还有一种暴力一点的构建:每次插入一个新的串前,last=root,之后按照普通后缀自动机的方法构造
空间复杂度还是 O(2n) O ( 2 n )

int root=1,sz=1,last=1;
int ch[N<<1][26],fa[N<<1],dis[N<<1]; 

void insert(int x) {
    int now=ch[last][x],pre=last;
    if (now) {
        if (dis[now]==dis[pre]+1) last=now;    //不用新建结点
        else {
            int nows=++sz; 
            last=nows;
            dis[nows]=dis[pre]+1;
            memcpy(ch[nows],ch[now],sizeof(ch[now]));
            fa[nows]=fa[now]; fa[now]=nows;
            for (;pre&&ch[pre][x]==now;pre=fa[pre]) ch[pre][x]=nows;
        } 
    }
    else {     //新建结点,正常的SAM构建
        now=++sz;
        last=now;
        dis[now]=dis[pre]+1;
        for (;pre&&!ch[pre][x];pre=fa[pre]) ch[pre][x]=now;

        if (!pre) fa[now]=root;
        else {
            int q=ch[pre][x];
            if (dis[q]==dis[pre]+1) fa[now]=q;
            else {
                int nows=++sz;
                dis[nows]=dis[pre]+1;
                memcpy(ch[nows],ch[q],sizeof(ch[q]));
                fa[nows]=fa[q]; fa[q]=fa[now]=nows;
                for (;pre&&ch[pre][x]==q;pre=fa[pre]) ch[pre][x]=nows; 
            }
        }
    }
}

最后

拓展知识:字符串最小表示法

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
import java.util.Scanner; public class Main{ public static void main(String[] args){ Scanner sc = new Scanner(System.in); int n = sc.nextInt(); int a = sc.nextInt(); int b = sc.nextInt(); String s = sc.next(); int cnt0 = 0; // 0连续段的长度纪录器 int cnt1 = 0; // 1连续段的长度纪录器 int ans = 0; // 操作次数 int len = 0; // 连续段长度 int flag0 = -1; // 标记下一个要匹配的0连续段的起始位置,初值设为-1 int flag1 = -1; // 标记下一个要匹配的1连续段的起始位置,初值设为-1 if(n % (a+b) != 0) { // 特判:字符串长度 不是 a + b 的倍数 System.out.println(-1); return; } for(int i = 0; i < n; i++) { if(s.charAt(i) == '0') { if(flag0 == -1) flag0 = i; // 标记0连续段的起始位置 cnt0++; if(cnt1 > 0 && cnt1 % b == 0) { // 1连续段长度为 b 的倍数 len = cnt1; while(cnt0 >= a && len > 0) { // 0连续段长度为 a 的倍数 ans += 2; cnt0 -= a; len -= b; } if(len != 0) { // 无法匹配,输出-1 System.out.println(-1); return; } else { // 匹配成功,重置标记 flag1 = -1; cnt1 = 0; } } } else { if(flag1 == -1) flag1 = i; // 标记1连续段的起始位置 cnt1++; if(cnt0 > 0 && cnt0 % a == 0) { // 0连续段长度为 a 的倍数 len = cnt0; while(cnt1 >= b && len > 0) { // 1连续段长度为 b 的倍数 ans += 2; cnt1 -= b; len -= a; } if(len != 0) { // 无法匹配,输出-1 System.out.println(-1); return; } else { // 匹配成功,重置标记 flag0 = 1; cnt0 = 0; } } } } System.out.println(ans); } }

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值