• 后缀自动机


    下面的 (SAM) 都是默认对于字符串 (s) 的。

    定义

    SAM 是一张有向无环图,其中,节点被称作状态,边被称作状态间的转移。
    图有一个源点 (t_0) ,称作初始状态,其它各节点均可从 (t_0) 出发到达。
    每个转移都标有一个字母,从每个节点出发的所有转移所标的字母均不同。
    存在若干个终止状态。从初始状态出发走到任意一个终止状态,经过的路径上的所有转移所标的字母连起来一定为 (s) 的一个后缀。(s) 的每个后缀均对应一条这样的路径。
    SAM 即为满足以上条件的节点数最少的自动机。
    一个性质
    任意从初始状态 (t_0) 开始的路径,路径上的所有转移所标的字母连起来一定为 (s) 的一个子串。(s) 的每个子串也都对应着一条这样的路径。
    endpos
    对于字符串 (s) 的任意非空子串 (t) ,记 (endpos(t))(t) 在字符串 (s) 中的所有结束位置组成的集合。
    若两个子串的 (endpos) 集合相等,则它们在同一等价类中。
    可以发现,SAM 的每个状态就对应着一个等价类,所以 SAM 的状态总数就等于等价类的个数 (+1)
    几个结论
    1.若字符串 (s) 的两个子串 (x)(y) ((|x|<|y|)) 的 (endpos) 相同,则 (x)(s) 中每次都是以 (y) 的后缀的形式存在的。
    2.对于两个非空子串 (x)(y) ((|x|<|y|)) ,若 (x)(y) 的后缀,则 (endpos(x) subseteq endpos(y)) ,反之, (endpos(x) cap endpos(y) = varnothing)
    3.对于一个等价类中的任意两个子串,较短者一定为较长者的后缀。同时,该等价类中的子串长度一定恰好覆盖一整个区间 ([x,y])
    link
    考虑 SAM 中某个不是源点的状态 (v) ,设它对应的等价类中最长的字符串为 (w) 。该等价类中的其它字符串均为 (w) 的后缀。
    可以发现, (w) 的最长几个后缀都存在于这个等价类中,且其它后缀存在于其它等价类中。(因为有空后缀,所以一定会存在这样的后缀)
    (t) 为最长的这样的后缀,就将 (v) 的后缀链接 (link(v)) 连到 (t)
    下面,假设初始状态 (t_0) 也为一个只包含自己的等价类(即只包含空串),规定 (endpos(t_0)={0,1,2, cdots ,|s|})
    又是几个结论
    4.所有后缀链接构成一棵以 (t_0) 为根的树。
    后缀链接一定连到严格比当前字符串短的串。
    5.通过 (endpos) 集合构造的树与通过后缀链接构造的树相同。
    根据上面的结论 (2) ,可以发现 (endpos) 集合构成一棵树。
    考虑任意一个不是 (t_0) 的状态 (v) ,可以发现 (endpos(v) subsetneq endpos(link(v)))
    结合这些,通过后缀链接构造的树本质上就是通过 (endpos) 构造的树。

    下面,对于一个状态 (v) ,设
    (longest(v)) 为该状态下最长的字符串, (maxlen(v)) 为它的长度。
    (shortest(v)) 为最短的字符串, (minlen(v)) 为它的长度。
    显然,有 (minlen(v)=maxlen(link(v))+1)

    构造 SAM

    首先,对于初始状态 (t_0) ,指定 (maxlen=0)(link=0)
    设当前要插入的字符为 (c) ,插入 (c) 之前的字符串为 (s)
    首先,创建一个新状态 (cur) ,对应于字符串 (s+c)
    (last)(s) 对应的状态,则 (maxlen(cur)=maxlen(last)+1)
    接下来,从 (last) 开始通过后缀链接遍历,对于遍历到的每个状态,尝试增加一条标着 (c) 的转移,如果原来就有了,直接结束循环。
    如果最后遍历到了 (0) ,则表示字符串 (s) 中没有出现过 (c) ,所以 (link(cur)=1)
    反之,假设已经存在的标着 (c) 的转移为 ((p,q))
    设状态 (p) 对应于字符串 (x) ,因为 (p)(last) 的后缀链接上,所以 (x)(s) 的后缀。
    因为存在 (q) 这个状态,所以 (x+c) 一定作为 (s) 的子串出现过。
    此时,我们就要把 (cur) 的后缀链接连到一个满足其中最长的字符串为 (x+c) 的状态上。
    (q) 满足这个条件,则 (maxlen(q)=maxlen(p)+1)
    所以,满足上面的式子时, (link(cur)=q)
    考虑最后一种情况,即 (maxlen(q)>maxlen(p)+1) 的情况。
    此时,考虑把 (q) 拆开,使其中一个状态的 (maxlen=maxlen(p)+1)
    新建一个状态 (v)
    (maxlen(v)) 赋为 (maxlen(p)+1)
    (q) 的所有转移赋给 (v) ,把 (link(v)) 赋为 (link(q)) ,把 (link(q)) 赋为 (v) ,把 (link(cur)) 赋为 (v)
    最后,遍历一遍 (q) 的后缀链接,直到到了 (0) 或到了一个不能直接转移到 (q) 的状态。
    把这些状态到 (q) 的转移改为到 (v) 的转移即可。

    void add(char c)
    {
    	int cur=++cnt;
    	s[cur].len=s[lst].len+1;
    	int p=lst;
    	while(p&&!s[p].nxt.count(c))
    		s[p].nxt[c]=cur,p=s[p].link;
    	if(!p) s[cur].link=1;
    	else
    	{
    		int q=s[p].nxt[c];
    		if(s[p].len+1==s[q].len) s[cur].link=q;
    		else
    		{
    			int v=++cnt;
    			s[v]=s[q],s[v].len=s[p].len+1;
    			while(p&&s[p].nxt[c]==q)
    				s[p].nxt[c]=v,p=s[p].link;
    			s[q].link=v,s[cur].link=v;
    		}
    	}
    	lst=cur;
    }
    

    应用

    1
    检查模式串 (t) 在文本串 (s) 中是否出现。
    (t) 构造后缀自动机,从 (t_0) 开始根据 (s) 的字符转移,
    如果能转移完整个 (s) ,则出现过,反之,没有出现。
    2
    对于一个 SAM 上的状态 (x) ,它贡献的不重复的子串即为 (maxlen(x)-maxlen(link(x)))

    #include<iostream>
    #include<cstring>
    #include<cstdio>
    #include<map>
    #define int long long
    using namespace std;
    const int N=1e5+10;
    int cnt,lst,ans,n;
    struct SAM
    {
    	int len,link;
    	map <int,int> nxt;
    }s[N*2];
    void add(int c)
    {
    	int cur=++cnt;
    	s[cur].len=s[lst].len+1;
    	int p=lst;
    	while(p&&!s[p].nxt.count(c))
    		s[p].nxt[c]=cur,p=s[p].link;
    	if(!p) s[cur].link=1;
    	else
    	{
    		int q=s[p].nxt[c];
    		if(s[p].len+1==s[q].len) s[cur].link=q;
    		else
    		{
    			int v=++cnt;
    			s[v]=s[q],s[v].len=s[p].len+1;
    			while(p&&s[p].nxt[c]==q)
    				s[p].nxt[c]=v,p=s[p].link;
    			s[q].link=v,s[cur].link=v;
    		}
    	}
    	ans+=s[cur].len-s[s[cur].link].len,lst=cur;
    }
    signed main()
    {
    	cnt=1,lst=1;
    	scanf("%lld",&n);
    	for(int i=1;i<=n;i++)
    	{
    		int x;
    		scanf("%lld",&x);
    		add(x),printf("%lld
    ",ans);
    	}
    	return 0;
    }
    

    3
    给定一个字符串,计算所有不同子串的总长度。
    状态 (x) 的贡献为 (frac{maxlen(x)*(maxlen(x)+1)}{2}-frac{maxlen(link(x))*(maxlen(link(x))+1)}{2})
    4
    复制一遍串,在 SAM 上贪心地跑 (n) 步即可。

    #include<iostream>
    #include<cstring>
    #include<cstdio>
    #include<map>
    #define fi first
    #define se second
    #define int long long
    using namespace std;
    const int N=1e6+10;
    int cnt,lst,n,a[N];
    struct SAM
    {
    	int len,link;
    	map <int,int> nxt;
    }s[N*2];
    void add(int c)
    {
    	int cur=++cnt;
    	s[cur].len=s[lst].len+1;
    	int p=lst;
    	while(p&&!s[p].nxt.count(c))
    		s[p].nxt[c]=cur,p=s[p].link;
    	if(!p) s[cur].link=1;
    	else
    	{
    		int q=s[p].nxt[c];
    		if(s[p].len+1==s[q].len) s[cur].link=q;
    		else
    		{
    			int v=++cnt;
    			s[v]=s[q],s[v].len=s[p].len+1;
    			while(p&&s[p].nxt[c]==q)
    				s[p].nxt[c]=v,p=s[p].link;
    			s[q].link=v,s[cur].link=v;
    		}
    	}
    	lst=cur;
    }
    signed main()
    {
    	cnt=1,lst=1;
    	scanf("%lld",&n);
    	for(int i=1;i<=n;i++) scanf("%lld",&a[i]);
    	for(int i=1;i<=n;i++) add(a[i]);
    	for(int i=1;i<=n;i++) add(a[i]);
    	int p=1;
    	for(int i=1;i<=n;i++)
    	{
    		printf("%lld ",(*s[p].nxt.begin()).fi);
    		p=(*s[p].nxt.begin()).se;
    	}
    	return 0;
    }
    

    5
    因为要求的是最大值,所以,对于每一个状态,一定只有 (longest) 有用。
    (f_i)(longest(i)) 出现的次数。
    考虑一个状态 (v) ,若 (link(v)=x)
    那么 (longest(x)) 一定是 (longest(v)) 的后缀。
    所以 (f_i=1+ sum limits_{link(x)=i} f_x)

    #include<iostream>
    #include<cstring>
    #include<cstdio>
    #include<vector>
    #include<map>
    #define int long long
    using namespace std;
    const int N=1e6+10;
    int cnt,lst,sz[N*2],n,ans;
    char a[N];
    vector <int> e[N*2];
    struct SAM
    {
    	int len,link;
    	map <char,int> nxt;
    }s[N*2];
    int MAX(int x,int y)
    {
    	return x>y?x:y;
    }
    void add(char c)
    {
    	int cur=++cnt;
    	s[cur].len=s[lst].len+1;
    	int p=lst;
    	while(p&&!s[p].nxt.count(c))
    		s[p].nxt[c]=cur,p=s[p].link;
    	if(!p) s[cur].link=1;
    	else
    	{
    		int q=s[p].nxt[c];
    		if(s[p].len+1==s[q].len) s[cur].link=q;
    		else
    		{
    			int v=++cnt;
    			s[v]=s[q],s[v].len=s[p].len+1;
    			while(p&&s[p].nxt[c]==q)
    				s[p].nxt[c]=v,p=s[p].link;
    			s[q].link=v,s[cur].link=v;
    		}
    	}
    	sz[cur]=1,lst=cur;
    }
    void dfs(int x)
    {
    	for(int i=0;i<e[x].size();i++)
    		dfs(e[x][i]),sz[x]+=sz[e[x][i]];
    	if(sz[x]!=1) ans=MAX(ans,s[x].len*sz[x]);
    }
    signed main()
    {
    	cnt=1,lst=1;
    	scanf("%s",a+1);
    	n=strlen(a+1);
    	for(int i=1;i<=n;i++) add(a[i]);
    	for(int i=2;i<=cnt;i++) e[s[i].link].push_back(i);
    	dfs(1),printf("%lld",ans);
    	return 0;
    }
    

    6
    先考虑 (t=0) 的情况。
    从初始状态开始顺着 SAM 跑,能选小的字母则选小的字母。
    此时,每个点的权值均为 1。
    考虑 (t=1) 的情况,其实只是改变了点的权值。
    对于每个点,新权值就是以它结尾的不同子串的数量,也就是 5 中的 (f)

    #include<iostream>
    #include<cstring>
    #include<cstdio>
    #include<map>
    #define int long long
    using namespace std;
    const int N=1e6+10;
    int cnt,lst,sz[N*2],n;
    int d[N*2],id[N*2],tp,f[N*2];
    char a[N];
    struct SAM
    {
    	int len,link;
    	map <char,int> nxt;
    }s[N*2];
    int MAX(int x,int y)
    {
    	return x>y?x:y;
    }
    void add(char c)
    {
    	int cur=++cnt;
    	s[cur].len=s[lst].len+1;
    	int p=lst;
    	while(p&&!s[p].nxt.count(c))
    		s[p].nxt[c]=cur,p=s[p].link;
    	if(!p) s[cur].link=1;
    	else
    	{
    		int q=s[p].nxt[c];
    		if(s[p].len+1==s[q].len) s[cur].link=q;
    		else
    		{
    			int v=++cnt;
    			s[v]=s[q],s[v].len=s[p].len+1;
    			while(p&&s[p].nxt[c]==q)
    				s[p].nxt[c]=v,p=s[p].link;
    			s[q].link=v,s[cur].link=v;
    		}
    	}
    	f[cur]=1,lst=cur;
    }
    void O()
    {
    	for(int i=1;i<=cnt;i++) d[s[i].len]++;
    	for(int i=1;i<=cnt;i++) d[i]+=d[i-1];
    	for(int i=1;i<=cnt;i++) id[d[s[i].len]--]=i;
    	for(int i=cnt;i>=1;i--)
    	{
    		if(tp) f[s[id[i]].link]+=f[id[i]];
    		else f[id[i]]=1;
    	}
    	f[1]=0;
    	for(int i=cnt;i>=1;i--)
    	{
    		sz[id[i]]=f[id[i]];
    		for(char j='a';j<='z';j++)
    			if(s[id[i]].nxt.count(j))
    				sz[id[i]]+=sz[s[id[i]].nxt[j]];
    	}
    }
    void query(int k)
    {
    	if(k>sz[1])
    	{
    		puts("-1");
    		return;
    	}
    	int p=1;
    	while(k) for(char i='a';i<='z';i++)
    		if(s[p].nxt.count(i))
    		{
    			if(sz[s[p].nxt[i]]>=k)
    			{
    				k-=f[s[p].nxt[i]],putchar(i),p=s[p].nxt[i];
    				break;
    			}
    			else k-=sz[s[p].nxt[i]];
    		}
    }
    signed main()
    {
    	cnt=1,lst=1;
    	scanf("%s",a+1);
    	n=strlen(a+1);
    	for(int i=1;i<=n;i++) add(a[i]);
    	int k;
    	scanf("%lld%lld",&tp,&k);
    	O(),query(k);
    	return 0;
    }
    
  • 相关阅读:
    黑客工具包ShadowBrokers浅析
    浅谈Miller-Rabin素数检测算法
    辗转相除法(欧几里得算法)的证明
    2019年年终感言
    详解矩阵乘法
    计数类问题中的取模运算总结
    浅谈同余方程的求解与中国剩余定理
    模板测试题
    洛谷 P3811 【模板】乘法逆元
    同余知识点全析
  • 原文地址:https://www.cnblogs.com/zhs1/p/14283678.html
Copyright © 2020-2023  润新知