【模板】【笔记】字符串相关

昨天开始学的…随便写点记录一下以免忘了…
因为学的比较混乱,欢迎在评论互相交流,欢迎打脸

KMP

这是复习…

求nxt数组这个DP,思想好好想想就没问题,代码好好看看就没问题,不难

会了这个,才能继续往下学…

void getnxt(char s[])
{
    nxt[0] = nxt[1] = 0;
    int l = strlen(s);
    for(int i = 1;i < l;i ++)
    {
        int j = nxt[i];
        while(j && s[i] != s[j]) j = nxt[j];
        nxt[i + 1] = s[i] == s[j] ? j + 1 : 0;
    }
}

int kmp(char s1[],char s2[])
{
    getnxt(s1);
    int ans = 0;
    int n = strlen(s1),m = strlen(s2);
    for(int i = 0,j = 0;i < m;i ++)
    {
        while(j && s1[j] != s2[i]) j = nxt[j];
        if(s1[j] == s2[i]) j ++;
        if(j == n) ans ++;
    }
    return ans;
}

trie树

这个也是复习…

代码太好写,就不写了…

空间复杂度好谜啊,我还是开到内存限制吧…

AC自动机

kmp是单模式串匹配,那AC自动机就是多模式串匹配…

经典裸题是:给n个串,再给一个串s,询问n个串总共在s中出现多少次。

AC自动机 = trie树+KMP,在trie树上做kmp即可…构造方法和kmp很像

kmp重点在nxt数组,也就是fail指针,AC自动机也是这样。

模仿KMP:KMP求nxt数组是线性的DP,按第几位划分阶段,nxt一定指向前面某个状态。因为在trie树上有好多串,所以可以bfs构造。若用str(u)表示根节点到u号节点构成的串,那么str(fail[u])是str(u)的一个后缀,这是由定义可知的。

有了这个就可以方便做很多事。因为str(fail[u])是str(u)的一个后缀,也是str(u的子树中的节点)的一个子串。

fail指针是多对一的,反向后可以得到一颗树。可以反映为某一节点是它子孙节点的子串。在树中保存一些信息,可以把字符串查询问题转化成树上问题,就可以用各种东西维护了。

关于复杂度,我研究了一下…也不知道对不对…
若有n个串,长度为L,建trie树的时间复杂度是 O(NL) ,建立AC自动机的复杂度是 O(NL+L) 。若拿长度为m的串做匹配,时间复杂度是 O(NL+M)

代码给出插入字符串、建立AC自动机和查询匹配次数。

void insert(char s[])
{
    int p = 0;
    int l = strlen(s);
    for(int i = 0;i < l;i ++)
    {
        int c = s[i] - 'a';
        if(!ch[p][c]) ch[p][c] = ++ sz;
        p = ch[p][c];
    }
    val[p] ++;
}

void build_ac()
{
    fail[0] = 0;
    for(int i = 0;i < 26;i ++)
    {
        int u = ch[0][i];
        if(u) { q.push(u); fail[u] = 0; }
    }
    while(q.size())
    {
        int f = q.front(); q.pop();
        for(int i = 0;i < 26;i ++)
        {
            int u = ch[f][i];
            if(!u) continue;
            q.push(u);
            int v = fail[f];
            while(v && !ch[v][i]) v = fail[v];
            fail[u] = ch[v][i];
        }
    }
}

int add_ans(int p)
{
    int ans = 0;
    for(;p;p = fail[p])
    {
        ans += val[p];
        val[p] = 0;
    }
    return ans;
}

int ask(char s[])
{
    int ans = 0,p = 0;
    int l = strlen(s);
    for(int i = 0;i < l;i ++)
    {
        int c = s[i] - 'a';
        while(p && !ch[p][c]) p = fail[p];
        p = ch[p][c];
        ans += add_ans(p); 
    }
    return ans;
}

trie图

trie图和AC自动机很像。trie图把AC自动机上不存在的边补上,指向它fail的那个边所指向的点(也就是说用它的fail来替换),并且每个点继承它fail的信息。这样得到的图叫trie图,是一张有向图。
因为是有向图,所以可以很容易划分阶段,可以跑DP。

丢一份bzoj1030

#include<cstdio>
#include<cstring>
#include<iostream>
#include<queue>
#include<algorithm>
using namespace std;

const int SZ = 10010;
const int mod = 10007;

int dp[110][SZ];

int ch[SZ][30],sz = 0,val[SZ];

void insert(char s[])
{
    int p = 0;
    int l = strlen(s);
    for(int i = 0;i < l;i ++)
    {
        int c = s[i] - 'A' + 1;
        if(!ch[p][c]) ch[p][c] = ++ sz;
        p = ch[p][c];
    }
    val[p] ++;
}

queue<int> q;
int fail[SZ];

void build_trieg()
{
    fail[0] = 0;
    for(int i = 1;i <= 26;i ++)
    {
        int u = ch[0][i];
        if(u) { q.push(u); fail[u] = 0; }
    }
    while(q.size())
    {
        int f = q.front(); q.pop();
        val[f] += val[fail[f]];
        for(int c = 1;c <= 26;c ++)
        {
            int u = ch[f][c];
            if(!u) { ch[f][c] = ch[fail[f]][c]; continue; }
            q.push(u);
            fail[u] = ch[fail[f]][c];
        }
    }
}


char s[SZ];

int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;i ++)
    {
        scanf("%s",s);
        insert(s);
    }
    build_trieg();

    dp[0][0] = 1;
    for(int len = 1;len <= m;len ++)
    {
        for(int u = 0;u <= sz;u ++)
        {
            if(val[u]) continue;
            for(int c = 1;c <= 26;c ++)
            {
                int v = ch[u][c];
                dp[len][v] = (dp[len][v] + dp[len - 1][u]) % mod;
            }
        }
    }
    int ans = 0,tot = 1;
    for(int i = 1;i <= m;i ++) tot = tot * 26 % mod;
    for(int i = 0;i <= sz;i ++)
        if(!val[i]) ans = (ans + dp[m][i]) % mod;
    printf("%d",(tot - ans + mod) % mod);
    return 0;
}

后缀数组

好吧这个本来不打算学的,结果暂时没看懂后缀树和后缀自动机,就先学这个了…
我打的 O(nlog2n) 的,好写好调,双关键字计数排序简直恶心…再说如果考场上见到这种题我应该不会写后缀数组…

倍增法求sa数组,O(n)求height数组,没什么问题。

sa数组的用处是可以快速找到字典序第几大的后缀,用处不是很大。
而height数组就厉害得多了…

利用height数组可以方便查询子串信息,可以把两后缀的最长公共前缀转化成RMQ问题。二分答案+height分组验证是很常用的个方法。

利用后缀数组处理两个以及更多的子串,可以用特殊符号把它们连起来。

后缀数组的计数问题。例如用后缀数组计算一个串有多少个不同的子串,贡献是n-sa[i]-height[i],即当前后缀所表示的串减去重复的串。

ydc:后缀数组处理点对有一个技巧,就是启发式合并。按height分组后,从大到小枚举height然后启发式合并计算贡献… Orzydc,我看不懂

大概就是这样了…


bool cmp_sa(int i,int j)
{
    if(rank[i] != rank[j]) return rank[i] < rank[j];
    else
    {
        int x = i + k <= n ? rank[i + k] : -1;
        int y = j + k <= n ? rank[j + k] : -1;
        return x < y;
    }
}

void get_sa(char s[])
{
    for(int i = 0;i <= n;i ++)
    {
        sa[i] = i;
        rank[i] = i < n ? s[i] : -1;
    }
    for(k = 1;k <= n;k <<= 1)
    {
        sort(sa,sa + 1 + n,cmp_sa);

        tmp[sa[0]] = 0;
        for(int i = 1;i <= n;i ++)
            tmp[sa[i]] = tmp[sa[i - 1]] + (cmp_sa(sa[i - 1],sa[i]) ? 1 : 0);
        for(int i = 0;i <= n;i ++)
            rank[i] = tmp[i];
    }
}

void get_lcp(char s[])
{
    int h = 0;
    lcp[0] = 0;
    for(int i = 0;i <= n;i ++)
        rank[sa[i]] = i;
    for(int i = 0;i < n;i ++)
    {
        int j = sa[rank[i] - 1];
        if(h) h --;
        while(i + h < n && j + h < n)
        {
            if(s[i + h] == s[j + h]) h ++;
            else break;
        }
        lcp[rank[i] - 1] = h;
    }
}

后缀自动机

卧槽刚刚搞懂,赶紧记下来QAQ

参考资料:
后缀自动机学习总结
【转】后缀自动机
以及CLJ课件:
2012年noi冬令营陈立杰讲稿

先说如何构建。

后缀自动机每个点保存:这个点的parent指针、根节点到当前点的最长串长度(step),以及这个点的儿子。

记录上一个插入的节点last,新字符c。新建节点np,将last向np连一条c边,然后开始找last的parent指针一直指到根节点路径上的节点,若没有c儿子则把c儿子设为np,然后找到第一个有c儿子的节点p(若p为根节点则跳出),p的c儿子称为q,看看q的step是否等于p的step+1,如果是则直接让np -> par = q;若不等于,则需要新建nq节点,拷贝q节点所有信息,然后若p到根节点的par构成的链中,从p开始若有c儿子指向q,则将其改为nq,否则直接break。

说的有点乱,之后看代码吧…

关于某些不是很显然的东西的证明(个人理解):
为什么若p有c儿子q且q -> step == p -> step + 1则直接np -> par = q呢?

首先,后缀自动机是后缀树+kmp
而step其实是根节点到当前节点的最长串的长度。

若q -> step == p -> step + 1,q是由状态p接受的,p转移到q的串就是last转移到np的串的后缀,因为在原串str后加了c字符,那么由last索引到的串就需要扩充它的Right,添加上Len(str+1),而因为是后缀,索引直接添加np -> par = q没有问题。

为什么若p有c儿子q且q -> step != p -> step + 1则需要拆点等等的事情呢?

先要明确一个性质:一个点的所有入边若边上字母相同则它们构成一个parent链。这个怎么证明…好像仔细想想就有了…

若存在q -> step != p -> step + 1,那同样是连向q的c边,q不是p接受的。定义q的入边发出者为p1,p2…pm,其中p1 -> par = p2,p2 -> par = p3…。则p1 -> step + 1 == q,设当前p为pi,pi把p组成的集合劈成两部分,可以证明pi~pm加上c转移的串不会出现在末尾,也就是Right(str(trans(pj,c)))不会有Len,这样转移就不合法了。

解决方法就是,把q拆点,q接受p1~pi,nq接受pi~pm,nq需要拷贝属于q的所有内容。这样nq成了接受态,p->par = nq,还可得q -> par = nq(kmp嘛233)。然后对于pi~pm,pi,pi+1,pi+2…依次,若trans(pi,c)=q,则改连nq,若碰到一个trans(pi,c)!=q,则立刻break。

虽然有些细节没搞懂,但还是理解了好多…就这样吧…

丢个链接:
后缀自动机(SAM)学习指南
有好多题以及简单题解

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值