BZOJ 3998 [TJOI2015]弦论【后缀自动机(总结+安利

80 篇文章 0 订阅
8 篇文章 0 订阅

某天……咸鱼我问学长

”学后缀自动机看啥啊“

“clj的ppt,慢慢看”

哦……好啊……clj大爷的ppt的确是学后缀自动机的必备材料23333……然而没图啊QuQ,全程没图根本看不懂啦

于是开始翻blog

1.这个blog 配合ppt食用,基本能懂了。这家的图很多……比较方便理解【而且举的例子不是回文串23333

2.一觉醒来忘记SAM是个啥时,这个blog 是不错的选择

3.黄学长的这个SAM,虽然作为模板并不算简洁优美……但是真的比模板容易理解【趴倒

==============================================================================

于是写写自己对后缀自动机的理解吧QuQ

……在学SAM前是先食用了AC自动机和trie树的……于是总觉得SAM的pre指针和AC自动机的fail指针神tm相似,不怂就是怼x

在SAM里能找到这个串的所有子串……子串数量是n^2级别的,如果直接建成trie的话肯定有n^2级别的节点数,但SAM的节点数≤2n……

很明显一个节点能代表的串不止一个……

SAM把一堆后缀相同的子串合并了,于是减少了很多重复的节点

在建SAM的时候,如果发现在新建节点时有冲突,而且长度不同的话,会重新复制一个节点【设原有节点为q,新节点为nq】,q的pre会指向nq,而pre[x]的意思是x节点无法记录的最长后缀所在节点处……也不知道我的理解对不对(如果哪位神犇发现我是错的跪求指教QuQ)……其实这nq和q本质上是一个节点……至少用途上是,所以在计数赋初值的时候……复制出来的那个节点的siz是0

可以理解成……SAM其实就是一棵trie树,但是吧他们相同的后缀合并了,于是很多节点共用了一些儿子,也共用了一些父亲,共用儿子不在意,但是共用父亲的话会出事(一个节点不会有两个标号相同的边往外连) 于是就复制了一个假的父亲,用来进行一些py交易【大雾】,进行后续的更新

==============================================================================

顺便吐槽……为什么……这道题的代码都没有注释啦……大家都说是裸题然而我这种数据结构弱菜表示……看不懂啊看不懂……

而且写这道题的方法大概有两种……

一种是bfs之后再dfs的,传说在T的边缘……于是我这种自带INF常数的弱菜果断放弃

第二种就是计数排序胡搞乱搞【详情见后文注释】的……代码复杂度和时间复杂度都十分的优【可是好难懂啊为什么没有注释w(゚Д゚)w  naive的新人表示快要哭出来了】


#include<bits/stdc++.h>
#define MAXN 500057
using namespace std;	int n,t,k;

int son[MAXN<<1][26],pre[MAXN<<1],dis[MAXN<<1];
int last=1,cnt=1;
int siz[MAXN<<1];

void insert(int x){
	int p=last , np=++cnt;
	dis[np]=dis[p]+1;
	siz[np]=1;
	for(;p&&!son[p][x];p=pre[p])	son[p][x]=np;
	last=np;
	
	if(!p)	return pre[np]=1,void();
	int q=son[p][x];
	if(dis[q]==dis[p]+1)	pre[np]=q;
	else{
		int nq=++cnt;	dis[nq]=dis[p]+1;
		pre[nq]=pre[q];
		pre[np]=pre[q]=nq;
		memcpy(son[nq],son[q],sizeof son[q]);
		for(;p&&son[p][x]==q;p=pre[p])	son[p][x]=nq;
	}
}

int v[MAXN<<1],qq[MAXN<<1];
int sum[MAXN<<1];
void getsize(){
	for(int i=1;i<=cnt;++i)	++v[dis[i]];
	for(int i=1;i<=n;++i)	v[i]+=v[i-1];
	for(int i=1;i<=cnt;++i)	qq[v[dis[i]]--]=i;
	//这上面是计数排序,把所有节点按照拓扑序排了一遍【SAM显然是个DAG
	for(int i=cnt;i;--i)	
		if(t)	siz[pre[qq[i]]]+=siz[qq[i]];
		else	siz[qq[i]]=1;
	//↑如果不同位置的子串只算一次的话,就不用管right集合大小了(right集合大小是这个子串出现的次数),全部赋值为1;如果不同位置的子串算多次的话,就按照parent树里从叶子节点一层一层往上推的顺序,计算每个串出现的次数
	siz[1]=0;
	
	for(int i=cnt;i;--i){
		int t=qq[i];
		sum[t]=siz[t];	
		for(int j=0;j<26;++j)
			if(son[t][j])	sum[t]+=sum[son[t][j]];
	}//这个就是统计每个节点往后推有多少个子串了……显然……
}


char s[MAXN];
int main(){
	scanf("%s%d%d",s,&t,&k);
	n=strlen(s);
	
	for(int i=0;i<n;++i)	insert(s[i]-'a');
	getsize();
	
	if(k>sum[1])	return puts("-1"),0;
	
	int ii=1;
	while((k-=siz[ii])>0){
		 int j=0;
		 while(k>sum[son[ii][j]])	k-=sum[son[ii][j++]];
		 ii=son[ii][j];
		 putchar('a'+j);
	}
	//上面那个……应该yy一下就支持了吧
	return 0;
}


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值