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

10人阅读 评论(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实质上就是用O(n)级别的时间/空间来记录下一个串所有的子串(共n2个)

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

例如:aabbabd
这里写图片描述

但是这样做的话节点数太多了,有n2
所以这就是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]能接上去的点都不同
意会

过程

看这里
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

查看评论

【佛山市选2015】约瑟夫问题

Description 现有N*(N+1)/2 个人围成一圈,编号从1到N*(N+1)/2,其中编号i与编号i+1的人相邻,编号N*(N+1)/2与编号1相邻。如今,我们要执行N-1个阶段的杀人仪式...
  • Eric1561759334
  • Eric1561759334
  • 2018-04-12 15:49:16
  • 13

约瑟夫问题及其各种优化

约瑟夫问题及其各种优化 【约瑟夫问题】 【问题描述一】:     据说著名犹太历史学家 Josephus有过以下的故事:在罗马人占领乔塔帕特后,39 个犹太人与Josephus及他的朋友躲到...
  • zhjchengfeng5
  • zhjchengfeng5
  • 2012-06-19 20:11:49
  • 1098

【JZOJ4024】【佛山市选2015】石子游戏(SG函数)

Problem   本题有T组测试数据。   给出N堆石子。A 和 B 轮流操作,A 先手。操作者在每一轮中必须选择一堆石子,并且作出下列两种操作中的一种:   (1)移走整堆石子   (2)...
  • qq_36551189
  • qq_36551189
  • 2018-04-09 19:52:11
  • 28

jzoj 4026. 【佛山市选2015】约瑟夫问题

Description 现有N*(N+1)/2 个人围成一圈,编号从1到N*(N+1)/2,其中编号i与编号i+1的人相邻,编号N*(N+1)/2与编号1相邻。如今,我们要执行N-1个阶段的杀人仪式...
  • ssl_lyy
  • ssl_lyy
  • 2018-04-08 21:20:09
  • 14

[SAM]后缀自动机

后缀自动机
  • hbhcy98
  • hbhcy98
  • 2016-04-03 16:46:35
  • 587

后缀自动机(SAM)学习笔记

构图及原理定义算法后缀自动机(SAM)就是一个要实现能存下一个串中所有子串的算法,按一般来说应当有O(N2)O(N^2)个状态,而SAM却可以用O(N)个状态来表示所有子串,因为它把很多个本质相似的子...
  • YxuanwKeith
  • YxuanwKeith
  • 2016-03-30 20:31:38
  • 4555

bzoj 2438 [中山市选2011]杀人游戏

Description 一位冷血的杀手潜入 Na-wiat,并假装成平民。警察希望能在 N 个人里面,查出谁是杀手。警察能够对每一个人进行查证,假如查证的对象是平民,他会告诉警察,他认识的人, 谁是...
  • ssl_lyy
  • ssl_lyy
  • 2018-04-11 20:23:06
  • 2

【后缀自动机】【SAM】【自动机】【数据结构】后缀自动机理解(入门)

引入来吧后缀自动机 我们先来看一看后缀数组可以干一些什么事情 1.可以查看当前后缀在所有后缀的排名 2.可以看子串的最大和 但是缺点呢却也非常的明显——显然这tm是个静态的。。。。 于是只好另...
  • JeremyGJY
  • JeremyGJY
  • 2016-04-08 07:48:31
  • 3243

SAM 后缀自动机——学习笔记

什么是后缀自动机(SAM)? 大概可以理解成对暴力在字母树中插入n个后缀的一种优化。 首先它是一个自动机。 对于一个字符串 sss , SAMSAMSAM 能识别其所有的后缀。还有一系列扩展运用...
  • CHHNZ
  • CHHNZ
  • 2017-07-25 11:08:52
  • 245

从最长公共子串到后缀自动机(LCS->SAM)

Longest Common Substring—Suffix Automaton本文将从最长公共子串开始说起,循序渐进的讲解后缀自动机,希望通过自己的理解帮助到大家,文章目录如下: 最长公共子串问题...
  • w305172521
  • w305172521
  • 2016-01-05 11:19:11
  • 1868
    个人资料
    持之以恒
    等级:
    访问量: 3万+
    积分: 1716
    排名: 2万+
    最新评论