SA-IS

被与掰嘲讽不会SA-IS于是爬来学习了

emmmm...SA-IS有啥用?可以用来\(O(n)\)构建SA数组,吊打与掰,最重要的是常数极小!!!(并且自从我发现 SA 可以用来建后缀树之后已经决定放弃 SAM 了)。

S/L型后缀

我们定义,如果后缀 \(\rm{i}\) 比后缀 \(\rm{i}+\rm{1}\) 字典序小则称\(i\)为S(mall)型后缀,否则为L(arge)型后缀。

于是为了避免一些奇怪的讨论,我们可以在字符串末尾加入一个'#',假装它是一个S型后缀。

\(tp_i\)为i的类型。

然后我们现在有了两个结论(设当前字符串是 \(a\) ):

  1. \(a_i=a_{i+1}\),则 \(tp_i=tp_{i+1}\)
    这个考虑比较两个后缀字典序大小,当 \(a_i=a_{i+1}\) 时要去比较 \((i+1,i+2)\),也就体现出了i+1的类型。
  2. \(a_i=a_j\) 并且 \(tp_i=S,tp_j=L\),则 \(i\)\(j\) 大。
    首先由类型定义得到 \(a_{i+1}\geq a_i,a_{j+1}\leq a_j\),那么除非 \(a_i=a_{i+1}=a_{j}=a_{j+1}\) 都能说明上述结论,否则我们继续比较,还是一样的讨论。

于是我们可以倒着扫一遍知道所有后缀的类型。

诱导排序

由上述结论我们可以发现排序后每一段首字母相同的后缀都是前面一部分L型后面是S型。
于是考虑先把两个类型分别排好之后再放到一起。

假设我们现在已经有了一定的后缀大小关系,那么我们每次取出最小的后缀 \(i\) 判断 \(i-1\) 是否是 \(L\) 型后缀,如果是的话就可以加入到当前集合中,这样我们就能得到一部分 \(L\) 型后缀的大小关系。

和倍增的桶排一样,我们先把手头有的 \(sa\) 数组插到桶里面,然后倒着扫回去复原 \(sa\),之后正着扫一遍 \(sa\) 数组,判断 \(i-1\) 是否为 \(L\) 型后缀,是的话就可以将它插到对应位置上。

如果我们现在已有的信息足够多,比如说知道了所有的 \(S\) 型后缀,就可以还原出 \(L\) 型后缀的大小关系,对此有两个证明:

  1. 不会有后缀被漏掉。
    \(L\) 型后缀定义可知 \(suf_i>suf_{i+1}\),那么 \(i+1\)\(sa\) 数组上一定比 \(i\) 先出现,一定会把 \(i\) 加进来。
  2. 对于任意两个 \(L\) 型后缀 \(i,j\) 满足 \(suf_i<suf_j\) 一定会有 \(i\) 先比 \(j\) 加进来。
    如果两个后缀首字母不同则显然,否则我们去比较 \((i+1,j+1)\) ,符合递归比较的原理。

同理,我们知道了所有的 \(L\) 型后缀也可以还原出 \(S\) 型后缀。这个L和S相互还原的关系大概就是“诱导”二字的由来吧。

递归,lms子串与lms后缀

但是我们又发现了其实由 \(S\) 诱导到 \(L\) 时我们并不需要所有的 \(S\) 型后缀信息,具体表现在递归时 \((i+1,j+1)\) 不一定都是 \(L\) 型后缀,也就是说我们其实只需要用到 \(i-1\)\(L\) 型后缀的 \(S\) 型后缀了。

那么我们定义 \(Lms\)(Left-Most-suffix) 型后缀为满足 \(suf_{i-1}\)\(L\) 型后缀但是 \(suf_{i}\)\(S\) 型后缀的 \(suf_i\)。那么还原 \(L\) 型后缀只需要 \(Lms\) 型后缀的大小关系。

那么我们对上面的诱导排序进行一个升级,先搞出来 \(Lms\) 后缀的大小关系,然后诱导出来 \(L\) 型后缀的大小关系,然后再用 \(L\) 型后缀诱导出来 \(S\) 型后缀,如此以来我们就顺利地得到了 SA 数组。

显然一个长度为 \(n\) 的字符串的 \(Lms\) 数量级是 \(O(\frac{n}{2})\) 的,那么我们就有了一个正确的复杂度:

\[T(n)=T(\frac{n}{2})+O(n)=O(n) \]

\(Lms\) 子串允许我们进行一个递归。

\(Lms\) 子串就是两个 \(Lms\) 后缀中间构成的子串,举个栗子:
\( \begin{matrix} Num & 1 & 2 & 3 & 4 & 5 & 6 & 7 & 8 & 9 & 10 & 11 & 12 & 13 & 14 & 15 & 16 & 17\\ Str & m & m & i & i & s & s & i & i & s & s & i & i & p & p & i & i & \#\\ Typ & L & L & S & S & L & L & S & S & L & L & S & S & L & L & L & L & S\\ Lms & & & * & & & & * & & & & * & & & & & & * \end{matrix} \)

那么对于字符串 mmiissiissppii 它的 \(Lms\) 子串就是 \([3,7],[7,11],[11,17]\)。其中,相邻两个 \(Lms\) 子串会重叠一个字符。

那么我们将 \(Lms\) 子串离散化之后拼到一起,例如上面的就变成了 2 2 1

那么到这里结论又来了:在原串上比较两个 \(Lms\) 后缀等价于比较它们离散化之后的后缀。

先说明 \(Lms\) 子串比较的规则:
我们把比较规则变成二元组 \((c,tp)\) ,先比较第一维(字符),第一维一样就比较第二维(类型),由上面知道 \(S>L\).

我们分离散化后的第一个字符的大小情况讨论:

  1. 大小相同
    这时候两个 \(Lms\) 子串相同,可以递归比较 \((i+1,j+1)\)
  2. 大小不同
    可能第一想法是,既然不同了,字典序岂不是显而易见?
    但是我们忽略了一种情况,就是一个 \(Lms\) 子串是另一个的子串,这样的话我们好像就假掉了...吗?
    其实不存在这种情况,考虑我们的比较规则和子串生成方式,一个子串必然是 \(SS...SSLL...LLS\) 的形式,即一段 \(S\) 一段 \(L\) 最后接上一个 \(S\) ,如果两个串长度不一样,短串的最后一个 \(S\) 必然匹配上一个 \(L\),从而不存在这种情况。

那么我们就可以心安理得地使用 \(O(n)\)\(SA-IS\) 算法了!

离散化lms子串与诱导排序

现在我们考虑如何对 \(lms\) 子串进行一个离散化。

答案还是诱导排序,传统诱导排序要求我们传入一个有序的 \(lms\) 后缀数组进去,但是我们现在乱序传入 \(lms\) 后缀数组,当然会得到一个错误的结果,但是我们可以断言,返回的后缀数组中 \(lms\) 后缀构成的子序列是按照每个 \(lms\) 后缀开头的 \(lms\) 子串为第一关键字排序的。

使用归纳法进行证明。

还是回忆一下排序过程:传入一个"有序"的后缀数组 \(\to\) 诱导出 \(L\) 型后缀 \(\to\) 诱导出 \(S\) 型后缀。

我们一开始传入的 \(lms\) 数组,如果只看第一个字符当然是有序的。

那么第一次诱导之后,对于一个 \(L\) 型后缀从它到后面第一个 \(S\) 型字符的部分一定是有序的。

那么第二次诱导之后,对于一个 \(S\) 型后缀它有序的部分就是从它到第一个 \(lms\) 型字符所在的位置,如果它是一个 \(lms\) 型后缀则有序的是它开头的 \(lms\) 子串。

那么我们就证明了只要再来一轮诱导排序就可以给所有 \(lms\) 子串排好序,然后我们就可以愉快地离散化啦~

实现

不晓得为啥跑的比较慢,学习一下卡常

哦原来是输入输出慢了,那没事了

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
namespace EMT{
	typedef long long ll;typedef double db;
	#define pf printf
	#define F(i,a,b) for(int i=a;i<=b;i++)
	#define D(i,a,b) for(int i=a;i>=b;i--)
	inline int read(){int x=0,f=1;char ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}while(ch>='0'&&ch<='9')x=x*10+ch-'0',ch=getchar();return x*f;}
	inline void file(){freopen("in.in","r",stdin);freopen("my.out","w",stdout);}
	inline int max(int a,int b){return a>b?a:b;}inline int min(int a,int b){return a<b?a:b;}
	inline void pi(int x){pf("%d ",x);}inline void pn(){pf("\n");}
	const int N=1e6+10;
	char s[N];int Emilia[N<<4],*at=Emilia,n;
	inline void give(int *&a,int siz){a=at,at+=siz;}
	namespace SA{
		int n,rk[N],sa[N],b1[N],b2[N],height[N];
		#define pl(x) sa[b2[a[x]]++]=x
		#define pr(x) sa[b2[a[x]]--]=x
		#define inds(lms)\
		F(i,1,n)sa[i]=-1,b1[i]=0;\
		F(i,1,n)b1[a[i]]++;\
		F(i,1,n)b1[i]+=b1[i-1];\
		memcpy(b2,b1,sizeof(int)*(n+1));\
		D(i,m,1)pr(lms[i]);\
		F(i,1,n)b2[i]=b1[i-1]+1;\
		F(i,1,n)if(sa[i]>1&&!tp[sa[i]-1])pl(sa[i]-1);\
		memcpy(b2,b1,sizeof(int)*(n+1));\
		D(i,n,1)if(sa[i]>1&&tp[sa[i]-1])pr(sa[i]-1);
		inline void sais(int n,int *a){
			int *tp=at;at+=n+1;
			int *p=at;at+=n+2;
			tp[n]=1;
			D(i,n-1,1)tp[i]=(a[i]==a[i+1])?tp[i+1]:(a[i]<a[i+1]);
			int m=0;
			F(i,1,n)rk[i]=(tp[i]&&!tp[i-1])?(p[++m]=i,m):-1;
			inds(p);
			int tot=0,*a1=at;at+=m+1;
			p[m+1]=n;
			int x,y;
			F(i,1,n)if(~(x=rk[sa[i]])){
				if(!tot||p[x+1]-p[x]!=p[y+1]-p[y])tot++;
				else{
					for(int p1=p[x],p2=p[y];p2<=p[y+1];p1++,p2++)
						if((a[p1]<<1|tp[p1])^(a[p2]<<1|tp[p2])){tot++;break;}
				}a1[y=x]=tot;
			}
			if(tot==m)F(i,1,m)sa[a1[i]]=i;else sais(m,a1);
			F(i,1,m)a1[i]=p[sa[i]];inds(a1);
		}
		inline void getheight(){
			int k=0;
			F(i,1,n)rk[sa[i]]=i;
			F(i,1,n)if(rk[i]>1){
				int j=sa[rk[i]-1];if(k)k--;
				while(i+k<=n&&j+k<=n&&s[i+k]==s[j+k])k++;
				height[rk[i]]=k;
			}
		}
		inline void init(){
			static int b[300],a[N];
			F(i,1,n)b[s[i]]=1;
			F(i,1,299)b[i]+=b[i-1];
			F(i,1,n)a[i]=b[s[i]]+1;a[++n]=1;
			sais(n,a);getheight();
			F(i,2,n)pi(sa[i]);pn();
			F(i,3,n)pi(height[i]);
		}
	}
	inline short main(){
		scanf("%s",s+1);n=SA::n=strlen(s+1);
		SA::init();
		return 0;
	}
}
signed main(){return EMT::main();}

大量参考

sto shadowice1984

posted @ 2022-06-07 21:05  letitdown  阅读(230)  评论(0编辑  收藏  举报