JZOJ4025. 【佛山市选2015】找回密码(后缀自动机SAM)

44 篇文章 0 订阅
7 篇文章 0 订阅

题目描述

Description
Kevin是一个热爱字符串的小孩。有一天,他把自己的微信登录密码给忘记了,万般无奈之下只好点“找回密码”。
这时候,网页上出现了当初设定的密保问题:在字符串st中,有若干个内容不同的子串,请问其中字典序第k小的子串是什么?
很可惜的是,Kevin现在已经不会写程序了,所以,他找到了睿智的你来帮忙。

Input
输入数据包括两行:第一行为字符串st,第二行为正整数k,定义如题目描述。
其中字符串st的长度不超过100,000且只由大小写英文字母组成
Output
一行,为第k小的字符串,如果字符串st中不足k个不同的子串,则输出字典序最大的一个。

Sample Input
AAB
2
Sample Output
AA

Data Constraint
50%的数据,|st| <=1000
100%的数据,|st|<=100,000,K < 2^63

50%

暴力枚举每个串,放在一起排序去重

100%

显然,这样做会发现有很多重复的串
所以需要一种算法来不重复记下一个串所有的子串
于是便有了后缀自动机SAM (Suffix Automaton)

SAM

(本题的正确代码在最下面)
假的SAM就留作反面教材

SAM实质上就是用 O(n) O ( n ) 级别的时间/空间来记录下一个串所有的子串(共 n2 n 2 个)

最简单的方法当然是存下一个串所有的后缀,然后每个后缀的前缀就是一个子串。
那么直接把它放在一个trie里就可以做到不重复

例如:aabbabd
这里写图片描述

但是这样做的话节点数太多了,有 n2 n 2
所以这就是SAM的精髓所在——合并状态

合并状态

仔细观察发现,有不少子串在原串出现的结尾位置集合相同,那么可以把这些串缩成一个点
然后从起点到这个点的任何一条路径所代表的是这些子串

就以aabba为例
这里写图片描述
如图,4号节点所代表的子串就是aabb、abb和bb。
而它们在原串的结尾位置集合也一样,都是{4}
同理,4号节点不能代表子串a和b,因为它们的集合是{1,2,5}和{3,4}

把具有相同集合的子串合并起来,就是SAM的关键所在。
如果一些子串的集合相同,那么其中的一些(个)必定是另一些(个)的后缀
意会

fail指针(图上的蓝色边)

实际就是网上所说的parent树,但实质跟其它自动机的定义差不多。

fail[x]表示
x的集合∈fail[x]的集合且|fail[x]的集合|最
比如上图4节点的集合是{4},而fail[4]=5(b)的集合是{3,4}

简单来讲,就是fail[x]代表的节点都是x代表的节点的后缀
(学过AC和回文自动机的话这里应该很好理解)

至于为什么要搞fail,大概是在新建节点时,让每一个能接的上子串都接上

len数组(图上的红色数字)

就是从起点到每个点的最长串长度
很好求,每次新建节点时+1

用处先猜

next数组(图上的黑色边)

next[x][y]表示节点x接上y后所代表的状态(编号)

(实际上跟其它自动机差不多,只不过SAM一个点代表了多个串)

建自动机

按顺序,依次加上新节点。

每次先在len最长的节点加上一个点x(因为没有能代表新串的节点,所以要新建),
之后顺着fail链往上跳。
设加上的字符为y,当前节点为now

那么可以分几种情况:

①next[now][y]=0

既然所属节点没有y的转移,那么可以直接接上
即next[now][y]=x

②next[now][y]≠0

设z=next[mow][y]

先列出来,等下再解释

1、len[z]=len[now]+1

那么直接把fail[x]设为z

2、len[z]>len[now]+1

把z克隆一遍,再把now及其祖先所以指向z的next边都指向新节点,
新节点继承 z的fail和next边
z和x的fail指向新节点

原因

如果next[now][y]≠0,那么也就是说可能可以直接接上去。
因为now是x的后缀,now+y 可能 是x的后缀

但直接接上可能会出现这样的情况:
这里写图片描述

看图,节点4的串是aabb abb bb,节点3的串是aab ab b
显然bb不是aab的后缀。

原因是因为S->3之间还有1和2两个节点,尽管S是3的前缀,但是因为中间有1和2,导致S+b 不能 和3+b共享后缀
所以必须要单独开一个代表S+b的节点,这样才能把后缀接上去

这里写图片描述
如图,因为S+b到的3不是4的后缀,所以必须单独把S+b提出来
而新的节点继承了老节点的所有信息,所以从起点到现在的5就相当于到3了
So可以把 所有 now的祖先所指向z的点都指向新节点

至于为什么跳fail就能把所有的边都改过来,原因如下:


如果两个点a和b都有一条连向z的边,那么显然a+y和b+y出现的集合相同。
那么a和b中其中一个必然是另一个的后缀(或ab的集合也相同)
所以顺着fail链往上跳就行了。


如果now和z只有一字之差(即len[z]=len[now]+1),那就可以直接接上去
因为next[x]能接上去的点都不同
意会

(2018年5月5日添加)
重新看了一遍自己写的东西发现差点看不懂
因为next[x]接上去的点不同,所以now和z之间一定只有一条边
其实可以手推一下,可以发现如果新加一个节点,那么这个节点一定跟原节点一模一样
所以没有必要新加节点


(2018年7月13日添加)
发现自己又写了假的SAM。。。
计算次数应该先加在last上,最后按照len排序,从儿子加到父亲
复制节点时不用复制出现次数,因为最后可以顺着fail加上去

过程

看这里
https://www.cnblogs.com/oyking/archive/2013/08/02/3232872.html

例题

题目描述

洛谷P3804 【模板】后缀自动机
题目描述
给定一个只包含小写字母的字符串S,
请你求出S 的所有出现次数不为 1的子串的出现次数乘上该子串长度的最大值。
输入输出格式
输入格式:

一行一个仅包含小写字母的字符串
S

输出格式:

一个整数,为 所求答案

输入样例
abab
输出样例
4

题解

裸题,每次顺着后缀往上累加。

伪·code

#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#define fo(a,b,c) for (a=b; a<=c; a++)
#define fd(a,b,c) for (a=b; a>=c; a--)
#define max(a,b) (a>b?a:b)
using namespace std;

int next[2000010][26];
int sum[2000010];
int fail[2000010];
int mx[2000010];
int i,j,k,l,Len,len,last,ans,I,J,K,L;
char s[1000010];
int d[1000010];

void New(int x,int y)
{
    next[x][y]=++len;
    mx[len]=mx[x]+1;
}

int main()
{
    len=1,last=1;
    mx[1]=0;
    sum[1]=1;
    fail[1]=0;

    scanf("%s",s);

    Len=strlen(s)-1;
    fo(i,0,Len)
    {
        New(last,s[i]-'a');

        K=last;
        last=next[last][s[i]-'a'];

        for (j=fail[K]; j; j=fail[j])
        if (!next[j][s[i]-'a'])
        next[j][s[i]-'a']=last;
        else
        {
            if ((mx[j]+1)==mx[next[j][s[i]-'a']])
            fail[last]=next[j][s[i]-'a'];
            else
            {
                k=next[j][s[i]-'a'];

                New(j,s[i]-'a');
                sum[len]=sum[k];

                fo(l,0,25)
                next[len][l]=next[k][l];

                fail[len]=fail[k];
                fail[last]=len,fail[k]=len;

                for (l=j; l; l=fail[l])
                {
                    if (next[l][s[i]-'a']==k)
                    next[l][s[i]-'a']=len;
                }
            }

            break;
        }
        if (!j)
        fail[last]=1;

        for (j=last; j; sum[j]++,j=fail[j]);
    }

    ans=0;
    fo(i,2,len)
    if (sum[i]>1)
    ans=max(ans,mx[i]*sum[i]);

    printf("%d\n",ans);
}

例题*2

题目描述

Description
Kevin是一个热爱字符串的小孩。有一天,他把自己的微信登录密码给忘记了,万般无奈之下只好点“找回密码”。
这时候,网页上出现了当初设定的密保问题:在字符串st中,有若干个内容不同的子串,请问其中字典序第k小的子串是什么?
很可惜的是,Kevin现在已经不会写程序了,所以,他找到了睿智的你来帮忙。

Input
输入数据包括两行:第一行为字符串st,第二行为正整数k,定义如题目描述。
其中字符串st的长度不超过100,000且只由大小写英文字母组成
Output
一行,为第k小的字符串,如果字符串st中不足k个不同的子串,则输出字典序最大的一个。

Sample Input
AAB
2
Sample Output
AA

Data Constraint
50%的数据,|st| <=1000
100%的数据,|st|<=100,000,K < 2^63

题解

没错就是一开始的题目(滑稽)
建出自动机后统计从每个点开始走能构成的字符串总数(dfs一遍),
之后直接找就行了

实际90分才是正解

code

//jzoj4025 Sam
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#define fo(a,b,c) for (a=b; a<=c; a++)
#define fd(a,b,c) for (a=b; a>=c; a--)
#define max(a,b) (a>b?a:b)
using namespace std;

int next[200010][52];
int fail[200010];
int mx[200010];
int i,j,k,l,Len,len,last,ans,I,J,K,L;
long long sum;
long long f[200010];
char s[100010];
int ls[200010];

int d[200010];
bool bz[200010];

int turn(char ch)
{
    if (ch<'[')
    return ch-65;
    else
    return ch-71;
}

int Turn(int s)
{
    if (s<26)
    return s+65;
    else
    return s+71;
}

void New(int x,int y)
{
    next[x][y]=++len;
    mx[len]=mx[x]+1;
}

void find()
{
    int i,t;

    t=1;

    while (sum)
    {
        if (!ls[t] && !next[t][0])
        return;

        fo(i,0,ls[t])
        if (next[t][i])
        {
            if ((f[next[t][i]]+1)>=sum || i==ls[t])
            {
                printf("%c",Turn(i));

                sum--;
                if (sum)
                t=next[t][i];

                break;
            }
            else
            sum-=f[next[t][i]]+1;
        }
    }
}

void init()
{
    int h,t,i,j;

    h=0,t=1;
    d[1]=1;
    bz[1]=1;

    while (h<t)
    {
        h++;

        fo(i,0,51)
        if (next[d[h]][i] && !bz[next[d[h]][i]])
        {
            bz[next[d[h]][i]]=1;

            d[++t]=next[d[h]][i];
        }
    }

    fd(j,t,1)
    {
        fo(i,0,51)
        if (next[d[j]][i])
        {
            ls[d[j]]=i;
            f[d[j]]+=1+f[next[d[j]][i]];
        }
    }
}

int main()
{
    len=1,last=1;
    mx[1]=0;
    fail[1]=0;

    scanf("%s",s);

    Len=strlen(s)-1;
    fo(i,0,Len)
    {
        New(last,turn(s[i]));

        K=last;
        last=next[last][turn(s[i])];

        for (j=fail[K]; j; j=fail[j])
        if (!next[j][turn(s[i])])
        next[j][turn(s[i])]=last;
        else
        {
            if ((mx[j]+1)==mx[next[j][turn(s[i])]])
            fail[last]=next[j][turn(s[i])];
            else
            {
                k=next[j][turn(s[i])];

                New(j,turn(s[i]));

                fo(l,0,51)
                next[len][l]=next[k][l];

                fail[len]=fail[k];
                fail[last]=len,fail[k]=len;

                for (l=j; l; l=fail[l])
                {
                    if (next[l][turn(s[i])]==k)
                    next[l][turn(s[i])]=len;
                }
            }

            break;
        }
        if (!j)
        fail[last]=1;
    }

    init();

    scanf("%lld",&sum);
    find();
    printf("\n");
}

后记

感觉SAM不是很难啊。。。
然而调了3天

参考资料

https://blog.csdn.net/qq_36551189/article/details/79764070
https://blog.csdn.net/doyouseeman/article/details/52245413
https://blog.csdn.net/YxuanwKeith/article/details/51019000
https://blog.csdn.net/wangzhen_yu/article/details/45481269
https://www.cnblogs.com/oyking/archive/2013/08/02/3232872.html

真·code

#include <algorithm>
#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <cstring>
#define fo(a,b,c) for (a=b; a<=c; a++)
#define fd(a,b,c) for (a=b; a>=c; a--)
#define max(a,b) (a>b?a:b)
using namespace std;

int next[2000010][26];
int sum[2000010];
int fail[2000010];
int mx[2000010];
char s[1000010];
int d[1000010];
int b[2000010];
int i,j,k,l,Len,len,last,ans,I,J,K,L;

void New(int x,int y)
{
    next[x][y]=++len;
    mx[len]=mx[x]+1;
}

void swap(int &a,int &b) {int c=a;a=b;b=c;}

void qsort(int l,int r)
{
    int i,j,mid;

    i=l;
    j=r;
    mid=mx[b[(l+r)/2]];

    while (i<=j)
    {
        while (mx[b[i]]>mid) i++;
        while (mx[b[j]]<mid) j--;

        if (i<=j)
        {
            swap(b[i],b[j]);
            i++,j--;
        }
    }

    if (l<j) qsort(l,j);
    if (i<r) qsort(i,r);

    return;
}

int main()
{
    len=1,last=1;
    mx[1]=0;
    sum[1]=1;
    fail[1]=0;

    scanf("%s",s);

    Len=strlen(s)-1;
    fo(i,0,Len)
    {
        New(last,s[i]-'a');

        K=last;
        last=next[last][s[i]-'a'];

        for (j=fail[K]; j; j=fail[j])
        if (!next[j][s[i]-'a'])
        next[j][s[i]-'a']=last;
        else
        {
            if ((mx[j]+1)==mx[next[j][s[i]-'a']])
            fail[last]=next[j][s[i]-'a'];
            else
            {
                k=next[j][s[i]-'a'];

                New(j,s[i]-'a');//这里不用复制sum

                fo(l,0,25)
                next[len][l]=next[k][l];

                fail[len]=fail[k];
                fail[last]=len,fail[k]=len;

                for (l=j; l; l=fail[l])
                {
                    if (next[l][s[i]-'a']==k)
                    next[l][s[i]-'a']=len;
                }
            }

            break;
        }
        if (!j)
        fail[last]=1;

        sum[last]++;//先加在最后的节点上
    }

    fo(i,1,len)
    b[i]=i;

    qsort(1,len);

    fo(i,1,len)
    sum[fail[b[i]]]+=sum[b[i]];//最后再累加sum

    ans=0;
    fo(i,2,len)
    if (sum[i]>1)
    ans=max(ans,mx[i]*sum[i]);

    printf("%d\n",ans);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值