【2022国赛模拟】(论文题)Sasha and Swag Strings——后缀数据结构

来源

题目描述

给定一个仅由小写字母组成的字符串 S   ( 1 ≤ ∣ S ∣ ≤ 1 0 6 ) S\ (1\leq |S|\leq 10^6) S (1S106) ,求出其后缀树每条边上的字符串本质不同的子串的个数之和。(1s

题解

原题目的范围是 1 0 5 10^5 105,要求太低了所以改成了 1 0 6 10^6 106


算法1:(1e5)

我们把后缀树建出来,那么每条边上的字符串在原串上是一个区间,直接套用LCT+线段树的做法询问区间本质不同子串个数即可 O ( n log ⁡ 2 n ) O(n\log^2 n) O(nlog2n) 解决,但是理论上只能通过 1 0 5 10^5 105 的点。
(为什么说是理论上,是因为其实稍微有点不好卡,如果数据太水可能就过了)


算法2:

我们建立后缀树的过程其实是对反串建立了后缀自动机SAM,后缀树每条边的字符串就是自动机上每个节点比 f a t h e r father father 多出来的那部分。而根据SAM的性质我们知道,在有边连向某个节点的所有节点中,一定有一个恰好是它所代表字符串的前缀。因为边上只有一个字符,这就意味着,如果我们只保留这些边,那么形成的是一棵 T r i e \rm Trie Trie 树。这棵 T r i e \rm Trie Trie 树,是SAM上所有节点的字符串顺着插入形成的,也是原来的那棵后缀树的所有节点的字符串倒着插入形成的。(这棵 T r i e \rm Trie Trie 树,我不知道应该叫什么,子串树?等价类 T r i e \rm Trie Trie?)

考察一下这个 T r i e \rm Trie Trie(不妨记为 T T T)的性质:首先 T T T 上的每个节点一一对应着SAM上的每个节点,点数 O ( n ) O(n) O(n)。其次,SAM上的某个节点比 f a t h e r father father 多出来的那部分字符串一定是它在 T T T 上的某个祖先,也就是说,我们在 T T T 上做 DFS 时把串长用桶记录一下即可 O ( 1 ) O(1) O(1) 得到这个祖先节点。剩下只需要想办法求出这些节点代表的字符串内本质不同子串个数即可。

我们可以通过在 T T T 上DP的方式来获得每个串内本质不同子串个数。记 f a x fa_x fax 表示点 x x x T T T 上的父亲节点,那么 d p x dp_x dpx 就等于它父亲的DP值+字符串 x x x不在父亲中出现的后缀数量。我们知道这些后缀一定刚好 ≥ \ge 某个长度,而这个长度就是 x x x 与它的祖先在SAM的 p a r e n t   t r e e \rm parent\,tree parenttree 上的最深 LCA 的串长。

到此为止,前面的所有工作都可以 O ( n σ ) O(n\sigma) O(nσ) 完成( σ \sigma σ 为字符集大小),只剩这个求最深 LCA 的问题。由于是最深的,所以只需要与 DFS 序最靠近的两个祖先求 LCA。我们用线段树查找(不用 set,是因为它太慢了)求出两个祖先,然后树剖求 LCA 即可做到小常数的 O ( n σ + n log ⁡ n ) O(n\sigma+n\log n) O(nσ+nlogn)


算法3:

在上面的做法中,我们显然不需要求出每个节点的DP值。若我们把需要求出DP值的节点称为黑点,那么所有 T T T 上子树中没有黑点的黑点代表的字符串的串长总和是 O ( n ) O(n) O(n)

为什么呢?因为这其实就是原串的压缩后缀自动机上的每个节点最长入边的长度和(好像还小一些?)。关于压缩后缀自动机,可以看 2020 年 cz_xyx 的国集论文。

所以我们只需暴力建SAM来求本质不同子串个数即可做到 O ( n σ ) O(n\sigma) O(nσ)

代码

给一下算法2的代码吧。代码中的串是01串,所以 σ = 2 \sigma=2 σ=2

#include<bits/stdc++.h>//JZM yyds!!
#define ll long long//God JZM!!
#define lll __int128//JZM RollInDark!!
#define uns unsigned
#define fi first
#define se second
#define IF (it->fi)
#define IS (it->se)
#define lowbit(x) ((x)&-(x))
#define END putchar('\n')
#define inline jzmyyds
using namespace std;
const int MAXN=1e6+5;
const ll INF=1e16;
ll read(){
	ll x=0;bool f=1;char s=getchar();
	while((s<'0'||s>'9')&&s>0){if(s=='-')f^=1;s=getchar();}
	while(s>='0'&&s<='9')x=(x<<1)+(x<<3)+(s^48),s=getchar();
	return f?x:-x;
}
int ptf[50],lpt;
void print(ll x,char c='\n'){
	if(x<0)putchar('-'),x=-x;
	ptf[lpt=1]=x%10;
	while(x>9)x/=10,ptf[++lpt]=x%10;
	while(lpt)putchar(ptf[lpt--]^48);
	if(c>0)putchar(c);
}
struct SAM{
	int ch[2],fa,len;
}sam[MAXN<<1];
int las=1,tot=1;
int samadd(bool c){//SAM 的主部分
	int p=las,np=las=++tot;sam[np].len=sam[p].len+1;
	for(;p&&!sam[p].ch[c];p=sam[p].fa)sam[p].ch[c]=np;
	if(!p)sam[np].fa=1;
	else{int q=sam[p].ch[c],nq;
		if(sam[q].len==sam[p].len+1)sam[np].fa=q;
		else{
			nq=++tot,sam[nq]=sam[q],sam[nq].len=sam[p].len+1;
			sam[q].fa=sam[np].fa=nq;
			for(;p&&sam[p].ch[c]==q;p=sam[p].fa)sam[p].ch[c]=nq;
		}
	}return np;
}
vector<int>G[MAXN<<1],D[MAXN<<1];
char in[MAXN];
ll dp[MAXN<<1];
int n,pr[MAXN<<1];
int siz[MAXN<<1],dep[MAXN<<1],hs[MAXN<<1],tp[MAXN<<1];
int hd[MAXN<<1],id[MAXN<<1],IN;
void pdfs(int x){//parent tree做树剖
	siz[x]=1,hs[x]=0,dep[x]=dep[sam[x].fa]+1;
	for(int i=0;i<2;i++)if(sam[x].ch[i])pr[sam[x].ch[i]]=x;//为建“T”做准备
	for(int v:G[x]){
		pdfs(v),siz[x]+=siz[v];
		if(siz[v]>siz[hs[x]])hs[x]=v;
	}
}
void pdfs2(int x){
	hd[x]=++IN,id[IN]=x,tp[x]=(x==hs[sam[x].fa]?tp[sam[x].fa]:x);
	if(hs[x])pdfs2(hs[x]);
	for(int v:G[x])if(v^hs[x])pdfs2(v);
}
int lca(int u,int v){
	if(!u||!v)return 1;
	while(tp[u]^tp[v]){
		if(dep[tp[u]]<dep[tp[v]])swap(u,v);
		u=sam[tp[u]].fa;
	}return dep[u]<dep[v]?u:v;
}
int sk[MAXN];
ll ans;
int MAX(int x,int y){return dep[x]>dep[y]?x:y;}
struct zkw{//小常数线段树
	bool f[MAXN*6];int p;
	void init(int n){for(p=1;p<n+2;p<<=1);}
	void chg(int x,bool d){
		for(f[p+x]=d,x=(p+x)>>1;x;x>>=1)f[x]=f[x<<1]|f[x<<1|1];
	}
	int schl(int x){
		for(x=p+x;x>1;x>>=1)if((x&1)&&f[x^1]){x^=1;break;}
		if(x<2)return 0;
		for(;x<p;x<<=1,x^=f[x^1]);
		return x-p;
	}
	int schr(int x){
		for(x=p+x;x>1;x>>=1)if((~x&1)&&f[x^1]){x^=1;break;}
		if(x<2)return 0;
		for(;x<p;x<<=1,x^=f[x]^1);
		return x-p;
	}
}T;
void solve(int x){
	sk[sam[x].len]=x;
	int ls=1,len=sam[x].len-sam[sam[x].fa].len;
	ls=MAX(lca(id[T.schl(hd[x])],x),lca(id[T.schr(hd[x])],x));
	dp[x]=sam[x].len-sam[ls].len+dp[pr[x]];//其实只用求查询节点的DP值
	if(sam[x].len<2)dp[x]=sam[x].len;
	T.chg(hd[x],1);
	for(int v:D[x])solve(v);
	T.chg(hd[x],0),ans+=dp[sk[len]];
}
int main()
{
	*new(int)=scanf("%s",in+1),n=strlen(in+1);
	reverse(in+1,in+1+n);
	for(int i=1;i<=n;i++)samadd(in[i]^48);
	for(int i=2;i<=tot;i++)G[sam[i].fa].push_back(i),pr[i]=1;
	pdfs(1),pdfs2(1);
	for(int i=2;i<=tot;i++)D[pr[i]].push_back(i);
	T.init(tot),solve(1);
	print(ans);
	return 0;
}

拓展

考虑这么一个问题:给定一个字符串 S   ( 1 ≤ ∣ S ∣ ≤ 1 0 6 ) S\ (1\leq |S|\leq 10^6) S (1S106) ,求出其后缀树每个节点的字符串本质不同的子串的个数之和。

问题等价于求 T T T 上每个节点的DP值,显然不能用算法3了,这个时候可以用算法2做到 O ( n log ⁡ n ) O(n\log n) O(nlogn) 的复杂度。

能不能做到 O ( n σ ) O(n\sigma) O(nσ) 呢?

我们知道广义SAM可以解决 T r i e \rm Trie Trie 上的子串问题。然而现在我们要求对 T r i e \rm Trie Trie 上每个单独的串求子串种数,貌似只能边 DFS 边做可回退的SAM。

这就关系到 T r i e \rm Trie Trie 上SAM的玄学复杂度问题。有国家队大佬说 DFS 建广义SAM的复杂度是串长总和而不是 T r i e \rm Trie Trie 的节点数,然后称 BFS 建广义SAM的复杂度“貌似”是 O ( n σ ) O(n\sigma) O(nσ)。但是在 T r i e \rm Trie Trie 上做可回退SAM与前面两者都不同,而我是个彩笔,根本不会研究这个复杂度。

这是个新的问题,希望将来能有神来解决吧。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值