后缀自动机[板子+弦论]

后缀自动机的原理和构造:

b站视频:https://www.bilibili.com/video/BV1ez4y117VF

代码blog:https://www.cnblogs.com/xzyxzy/p/9186759.html

先是一个板子题:

后缀自动机 (SAM)

题目描述

给定一个只包含小写字母的字符串S

请你求出 S 的所有出现次数不为 1 的子串的出现次数乘上该子串长度的最大值。

输入格式

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

输出格式

一个整数,为 所求答案

输入 

abab

输出 

4

说明/提示

对于10%的数据,|S|<=1000

对于100%的数据,|S|<=10^6

source:https://www.luogu.com.cn/problem/P3804  洛谷P3804

一道板子题,直接根据字符串建立自动机,然后选掉只出现一次的就行,关于解释都在代码里了

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <string.h>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
const int maxn=2e6+10;
char text[maxn];
int sam[maxn][26],textlen,siz[maxn],link[maxn],len[maxn],nodecnt=1,cur,last=1,temp[maxn],sa[maxn];
void insert(int c)
{
	int k=last;
	cur=++nodecnt;
 	last=cur;
	len[cur]=len[k]+1;
	siz[cur]=1;
	while(k&&(!sam[k][c]))
	{
		sam[k][c]=cur;
		k=link[k];
	}
	if(!k)//退出的时候走到了根节点对应第一种插入情况 
		link[cur]=1;
	else if(len[sam[k][c]]==len[k]+1)//第二种情况 
		link[cur]=sam[k][c];
	else//第三种 
	{
		int clone=++nodecnt,x=sam[k][c];
		len[clone]=len[k]+1;
		link[clone]=link[x];
		link[x]=link[cur]=clone;
		memcpy(sam[clone],sam[x],sizeof(sam[clone]));
		while(k&&sam[k][c]==x)
		{
			sam[k][c]=clone;
			k=link[k];
		}
	}

}
int main()
{
	int T;
	ll K,ans=0;
	scanf("%s",text);
	textlen=strlen(text);
	for(int i=0;i<textlen;i++)//建立后缀自动机 
	    insert(text[i]-'a');
	//下面是对自动机的处理,size代表该endpose类在字符串text中出现的次数
	//为了求出每个节点对应的size值,要对link树用dfs序,每个子树的根节点的size是该子树所有节点的size总和
	//叶子节点的size必然是1,因为叶子说明在加入当前字符的时候该节点的len是最长的,在字符串text里面只出现了一次
	//为了简化对link树的反转工作,这里以len从小到大对节点排序,再从大到小遍历,就可以实现dfs的功能,排序方法是桶(基数)排序 
	for(int i=1;i<=nodecnt;i++)
	    temp[len[i]]++;
	for(int i=1;i<=nodecnt;i++)
	    temp[i]+=temp[i-1];
	for(int i=nodecnt;i>=1;i--)//这里从1到nodecnt也可以,不像后缀数组,这里不要求要稳定(不知道为啥) 
	    sa[temp[len[i]]--]=i;
	for(int i=nodecnt;i>=1;i--)
    {
    	siz[link[sa[i]]]+=siz[sa[i]];//这就是实现dfs功能的精髓,因为先遍历到len较大的,是在最后插入的,就是先处理叶子,在处理枝和根 
    	if(siz[sa[i]]>1)
    	    ans=max(ans,1ll*siz[sa[i]]*len[sa[i]]);
	}
	printf("%lld\n",ans);
    return 0;
}

然后再来一道也比较简单的题

 弦论

题目描述

为了提高智商,ZJY 开始学习弦论。这一天,她在《String theory》中看到了这样一道问题:对于一个给定的长度为 n 的字符串,求出它的第 k 小子串是什么。你能帮帮她吗?

输入格式

第一行是一个仅由小写英文字母构成的字符串 s。

第二行为两个整数 t 和 k,t为 0 则表示不同位置的相同子串算作一个,t 为 1 则表示不同位置的相同子串算作多个。k 的意义见题目描述。

输出格式

输出数据仅有一行,该行有一个字符串,为第 k 小的子串。若子串数目不足 k 个,则输出 −1。

输入输出样例

输入 #1

aabc
0 3

输出 #1

aab

输入 #2

aabc
1 3

输出 #2

aa

输入 #3

aabc
1 11

输出 #3

-1

说明/提示

数据范围

对于 10% 的数据, n≤1000。

对于 50% 的数据,t=0。

对于 100% 的数据,1≤n≤5×1e5,0≤t≤1,1≤k≤1e9。

source:洛谷P3975

思路:先把文本建个后缀自动机,然后更具T为0或1判断一下,如果是1那size就没啥问题,如果是0,那么size就都赋值为1,因为不同位置的子串只算一个

然后题目要求第k小子串,我们可以考虑size的性质,是该endpose含有的maxleng串的合法位置个数(这里合法就指的是如果T是零,那么不管位置有几个都是1)那么怎么求第k小串呢,我现在写博文的时候觉得后缀数组是不是更好一点呢?还是说自动机咋搞,因为后缀自动机的性质,任意子串都可以由从根节点出发的一个子串得到,任意从根节点出发的一条路径对于一个子串,那么好了,每一个子串都有一条路径与之对应,那么每个节点原本代表一个endpose类,现在只让他代表endpose里面maxleng的子串,然后统计一下以该节点为根的自动机(DAG有向无环图)(不是link树)里面由多少合法字符串(以该节点为根的DAG对应的子串都已该根的maxleng串为前缀),存一下,遍历的时候对于每个正在被访问的节点从a到z遍历,看应该落在那个树上,然后打印该边的字符,在进去这个边的子树接着找就行

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <string.h>
#include<iostream>
#include<algorithm>
//using namespace std;
typedef long long ll;
const int maxn=1e6+10;
char text[maxn];
int sam[maxn][26],textlen,size[maxn],link[maxn],len[maxn],nodecnt=1,cur,last=1,temp[maxn],sa[maxn],sum[maxn];
void insert(int c)
{
	int k=last;
	cur=++nodecnt;
 	last=cur;
	len[cur]=len[k]+1;
	size[cur]=1;
	while(k&&(!sam[k][c]))
	{
		sam[k][c]=cur;
		k=link[k];
	}
	if(!k)
		link[cur]=1;
	else if(len[sam[k][c]]==len[k]+1)
		link[cur]=sam[k][c];
	else
	{
		int clone=++nodecnt,x=sam[k][c];
		len[clone]=len[k]+1;
		link[clone]=link[x];
		link[x]=link[cur]=clone;
		memcpy(sam[clone],sam[x],sizeof(sam[clone]));
		while(k&&sam[k][c]==x)
		{
			sam[k][c]=clone;
			k=link[k];
		}
	}

}
void quary(int u,int k)
{
	if(k<=size[u]) 
	return;
	k-=size[u];//记得把该店对应的合法字符串数目剪掉 
	for(int i=0;i<26;i++)
	{
		int t=sam[u][i];
		if(!t)//如果没有这个边就跳过 
		    continue;
		if(k>sum[t])//大了说明不在该子树里面 
	    {
	    	k-=sum[t];
	    	continue;
		}
		putchar(i+'a');//说明就在该子树里面 
		quary(t,k);
		return;
	}
}
int main()
{
	int T;
	ll K;
	scanf("%s",text);
	scanf("%d %lld",&T,&K); 
	textlen=strlen(text);
	for(int i=0;i<textlen;i++)
	    insert(text[i]-'a');//建立后缀自动机 
	
	for(int i=1;i<=nodecnt;i++)//用桶排序计算size 
	    temp[len[i]]++;
	for(int i=1;i<=nodecnt;i++)
	    temp[i]+=temp[i-1];
	for(int i=nodecnt;i>=1;i--)
	    sa[temp[len[i]]--]=i;
	for(int i=nodecnt;i>=1;i--)
        size[link[sa[i]]]+=size[sa[i]];
	for(int i=1;i<=nodecnt;i++)
	    T?(sum[i]=size[i]):(sum[i]=size[i]=1);//这就是看T来修正size 
	size[1]=sum[1]=0;
	for(int i=nodecnt;i>=1;i--)//看一下该节点为根的自动机上由多少节点,然后存进sum 
	    for(int j=0;j<26;j++)
		    if(sam[sa[i]][j])
			    sum[sa[i]]+=sum[sam[sa[i]][j]];
			    
	if(sum[1]<K)
	printf("-1\n");
	else
	{
		quary(1,K);
    	printf("\n");
	}
    return 0;
}

2021.8.17

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值