zoukankan      html  css  js  c++  java
  • 字符串算法学习笔记(不定期更新)

    更新至 11.23。

    关于字符串Hash后缀自动机SAM可以转至我之前的博客。

    1.SA

    模拟退火后缀数组(Suffix Array)是一种很奇妙的算法。主要原因是它可以做到在 (O(nlog n)) 时间内完成排序。

    关于如何完成这个比较基础,具体可见洛谷日报

    而后缀排序的重点在于“字典序排序”的一些奇妙性质。所以对于一般字符串的字典序排序,以下性质也适用。

    首先可以发现的是 (operatorname{LCP}(i,j)=min(operatorname{LCP}(i,k),operatorname{LCP}(k,j)),kin[i,j])。这个比较显然主要我也不怎么会严格证明。具体可以见洛谷日报的证明。

    考虑有了这个我们可以干什么。考虑这样一道题:按一定方式给定一堆字符串(总长度可能很大),问其中本质不同前缀的个数。

    那么显然可以发现,相邻两字符串的 (operatorname{LCP}) 就是他们本质相同的前缀。换句话说,除此之外的部分都是本质不同的。

    而根据那个奇怪的性质,相邻两个字符串 ((x,x+1))(operatorname{LCP}) 一定 (geq (i,k),kgeq i+1)(operatorname{LCP})。所以显然成立。

    但是这个相邻的 (operatorname{LCP}) 怎么求呢?

    其实是有一个很simple的 (O(n)) 求法。什么SA-IS?完全不会。

    具体来说,我们可以求出第 (i) 个位置与字典序在它前面的串的 (operatorname{LCP}) (h_i)。可以发现有 (h_{i}=h_{i-1}+1)。于是乎就均摊 (O(n)) 了。

    那么我们可以做什么了呢?求本质不同子串!每个后缀的前缀唯一对应一个子串,所以直接减就好了。

    例:本质不同子串

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #define N 100010
    using namespace std;
    int b[N],sa[N],rk[N],a[N],id[N];
    char s[N];
    void SA_(int n,int m)
    {
    	for(int i=0;i<=m;i++) b[i]=0;
    	for(int i=1;i<=n;i++) b[rk[i]]++;
    	for(int i=1;i<=m;i++) b[i]+=b[i-1];
    	for(int i=n;i>=1;i--) sa[b[rk[id[i]]]--]=id[i];
    }
    void SA(int n)
    {
    	int m=124;
    	for(int i=1;i<=n;i++) rk[i]=s[i]-'0'+1,id[i]=i;
    	SA_(n,m);int t=0;
    	for(int p=1;p<n;m=t,t=0,p<<=1)
    	{
    		for(int i=1;i<=p;i++) id[++t]=n-p+i;
    		for(int i=1;i<=n;i++) if(sa[i]>p) id[++t]=sa[i]-p;
    		SA_(n,m); swap(id,rk); rk[sa[1]]=t=1;
    		for(int i=2;i<=n;i++) rk[sa[i]]=(t+=id[sa[i-1]]!=id[sa[i]] || id[sa[i-1]+p]!=id[sa[i]+p]);
    	}
    }
    int ht[N];
    void get_ht(int n)
    {
    	for(int i=1,p=0;i<=n;ht[rk[i]]=p,i++)
    	if(rk[i]!=1) for(p=p-!!p;sa[rk[i]-1]+p<=n && i+p<=n && s[i+p]==s[sa[rk[i]-1]+p];p++);
    }
    int main()
    {
    	int n;
    	scanf("%d%s",&n,s+1);
    	SA(n);
    	get_ht(n);
    	long long ans=1ll*n*(n+1)/2;
    	for(int i=1;i<=n;i++) ans-=ht[i];
    	printf("%lld
    ",ans);
    	return 0;
    }
    // 压行?怎么可能?这叫 建筑美(
    

    看到这你或许会问:这个不是SAM也能做吗?而且SAM是 (O(n)) 的。

    的确,绝大部分SA能做的SAM都能做,而且SAM跑得快、支持在线、还更好些(所以我学SA干什么)。

    别急,这里还有一个SA的晋升版本:

    2.后缀平衡树(好像不一定是这么叫的)

    没想到吧,后缀平衡树居然不是后缀树变过来的(我也没想到)。

    首先我们还是考虑一般情况:给定一个字符串 (S) 的一堆子串,每次问某个子串 (s0) 与其他每个串的 (operatorname{LCP}) 最大是多少。动态修改子串集合。

    这个可以怎么做?考虑使用平衡树套Hash。具体可以见Hash学习笔记中的一道口胡的题(里面好像还有一个强制在线)。

    这个是 (O(nlog^2 n)) 的,虽然比较暴力已经足够优秀了。但是如果我们插入的字符串有一些规律可循,是不是有更快的做法。

    【模板】后缀平衡树

    题意

    维护一个字符串,支持加一堆字符,删一堆字符,询问某个字符串出现次数。强制在线。总字符串长度 (leq 10^6)

    出题人真的是丧心病狂。。。

    AC自动机能过?那就强制在线。

    SAM还能过?那就每次加一堆字符。

    啥?两只 (log) 艹过去了?那就开到 (10^6)真·5步出题法

    显然我们需要一个更高妙的做法。考虑多一只 (log) 的瓶颈在于每次判断字典序时必须 (O(log n)) 处理。再加上判断 (O(log n)) 次,所以总 (O(log^2 n))

    平衡树的 (log n) 没办法优化,考虑优化判断字典序。可以发现,我们要加入的字符串 (u) 在加入前它的前缀一定已经出现过了,所以前缀和当前要比较的节点 (p) 均出现过。

    可以发现,当前加入的字符串 (u) 除了最后一个字符之外其他都与前缀 (u-1) 完全一致,所以我们先暴力比较 (u)(p) 的最后一个字符,如果相同意味着这个 (u-1)(p-1) 的字典顺序决定了 (u)(p) 的字典顺序。但是直接这样比较还是 (O(log n))

    考虑如果我们维护出了所有前缀的 (rank),那么显然 (rank) 的相对顺序就对应最后的结果。但是我们不能直接维护rank,这样会平白多出一个 (log n)。考虑我们只需要知道 (rank) 的相对顺序即可。考虑利用平衡树的性质,每个点取一个权值 (v_i=frac{L+R} 2),然后根据 (v_i) 将区间分为两段递归处理。可以发现,这样满足 (v_{ls_u}< v_u< v_{rs_u})

    这样建树的时间复杂度 (O(|S|log |S|))

    考虑这题维护的东西:出现次数。

    这个就很好办了。考虑差分,比如要查 ( exttt{AB}),我们就查字典序在 ( exttt{AA[})( exttt{AB[}) 之间的字符。(( exttt{[}) 的字典序大于所有大写字母)。具体来说,由于后缀平衡树中不存在字符 ( exttt{[}) ,我们可以直接用字典序小于 ( exttt{AB[}) 的数量减去小于 ( exttt{AA[}) 的数量。

    由于这里需要保证相对顺序,所以我们必须使用重量平衡树,这里可以是替罪羊树(当然好像treap也行,splay应该会被卡)。

    总时间复杂度 (O(|S|log |S|)),空间复杂度 (O(|S|))

    特别,这里需要支持删除。但是我们不能按照普通的替罪羊树那样打标记,因为这个点会被替换掉。

    所以我们直接暴力删除,按照BST的方式找到左子树中最右端的点,然后拼接上去。由于树深是 (O(log n)),所以这样复杂度也是对的。

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #define N 2000010
    #define db double
    #define alp 0.72
    #define MAXD 1e16
    using namespace std;
    char str[N],s[N];
    db v[N];
    int ch[N][2],siz[N];
    int swp[N],stot,rt;
    void upd(int u){siz[u]=siz[ch[u][0]]+siz[ch[u][1]]+1;}
    void dfs(int u)
    {
    	if(!u) return;
    	dfs(ch[u][0]),swp[++stot]=u;dfs(ch[u][1]);
    	ch[u][0]=ch[u][1]=0;
    }
    void build(int &u,int l,int r,db lf=0,db rf=MAXD)
    {
    	if(l>r) return;
    	int mid=(l+r)>>1;db mf=(lf+rf)/2;
    	u=swp[mid];
    	v[u]=mf;
    	build(ch[u][0],l,mid-1,lf,mf),build(ch[u][1],mid+1,r,mf,rf);
    	upd(u);
    }
    void reb(int &u,db lf,db rf)
    {
    	if(max(siz[ch[u][0]],siz[ch[u][1]])<siz[u]*alp) return;
    	stot=0;dfs(u);
    	build(u,1,stot,lf,rf);
    }
    int cmp(int x,int y){return s[x]==s[y]?v[x-1]<v[y-1]:s[x]<s[y];}
    void insert(int &u,int k,db lf=0,db rf=MAXD)
    {
    	if(!u){siz[u=k]=1;v[u]=(lf+rf)/2;ch[u][0]=ch[u][1]=0;return;}
    	if(cmp(k,u)) insert(ch[u][0],k,lf,v[u]);
    	else insert(ch[u][1],k,v[u],rf);
    	upd(u),reb(u,lf,rf);
    }
    void erase(int &u,int k)
    {
    	if(u==k)
    	{
    		if(!ch[u][0] || !ch[u][1]){u=ch[u][0]|ch[u][1];return;}
    		int p=ch[u][0],las=u;
    		for(;ch[p][1];las=p,p=ch[p][1]) siz[p]--;
    		if(las==u) ch[p][1]=ch[u][1];
    		else ch[p][0]=ch[u][0],ch[p][1]=ch[u][1],ch[las][1]=0;
    		u=p;
    		upd(u);
    		return;
    	}
    	if(cmp(k,u)) erase(ch[u][0],k);
    	else erase(ch[u][1],k);
    	upd(u);
    }
    bool cmp_s(int u){for(int i=1;str[i];i++,u=u-!!u) if(str[i]!=s[u]) return str[i]<s[u];return false;}
    int answer(int u)
    {
    	if(!u) return 0;
    	if(cmp_s(u)) return answer(ch[u][0]);
    	else return answer(ch[u][1])+siz[ch[u][0]]+1;
    }
    void get_c(char s[],int mask)
    {
    	int len=strlen(s);
    	for(int i=0;i<len;i++)
    	{
    		mask=(mask*131+i)%len;
    		char t=s[i];
    		s[i]=s[mask];
    		s[mask]=t;
    	}
    }
    char opt[7];
    int main()
    {
    	int n,m,k,las=0;
    	scanf("%d%s",&m,s+1);n=strlen(s+1);
    	for(int i=1;i<=n;i++)
    	insert(rt,i);
    	for(int i=1;i<=m;i++)
    	{
    		scanf("%s",opt);
    		if(opt[0]=='D'){scanf("%d",&k);while(k --> 0) erase(rt,n),n--;continue;}
    		scanf("%s",str+1);
    		get_c(str+1,las);
    		int l=strlen(str+1);
    		if(opt[0]=='A') for(int j=1;j<=l;j++) s[++n]=str[j],insert(rt,n);
    		else if(opt[0]=='Q')
    		{
    			reverse(str+1,str+l+1);
    			str[l+1]='Z'+1,str[l+2]='';
    			int ans=answer(rt);
    			str[l]--;
    			ans-=answer(rt);
    			printf("%d
    ",ans);
    			las^=ans;
    		}
    	}
    	return 0;
    }
    //压行?不存在的。
    

    bzoj3682 Phorni

    题意

    维护一个 (n) 个元素的序列,每个元素是字符串 (s) 的一个后缀。

    支持:在 (s) 开头加一个字符,改变序列 (a_i) 表示的后缀位置,求序列 ([l,r]) 元素中字典序最小的编号(如有相同输出编号最小)。强制在线

    第一眼:这不是**题吗?直接SA就好了。

    然后发现:强制在线

    SA没了,直接后缀平衡树就好了。本来后缀平衡树也被叫做“动态后缀数组”。

    首先将串反过来,就变成了在末尾加字符,查询前缀。

    用平衡树维护各前缀之间的相对位置,然后用一颗线段树处理 (n) 个前缀的相对权值即可。

    注意这里这个相对权值的精度要求可能比较高,直接比较可能把两个相同的前缀看做不同的。需要特判该情况。

    复杂度 (O(nlog n))

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #define N 2000010
    #define db double
    #define alp 0.74
    #define MAXD 1e16
    using namespace std;
    char s[N];
    db v[N];
    int siz[N],ch[N][2];
    void upd(int u){siz[u]=siz[ch[u][0]]+siz[ch[u][1]]+1;}
    int ton[N],td;
    void dfs(int u)
    {
    	if(!u) return;
    	dfs(ch[u][0]),ton[++td]=u,dfs(ch[u][1]);
    	ch[u][0]=ch[u][1]=0;
    }
    void bld(int &u,int l,int r,db lf=0,db rf=MAXD)
    {
    	if(l>r) return;
    	int mid=(l+r)>>1;
    	u=ton[mid];v[u]=(lf+rf)/2;
    	bld(ch[u][0],l,mid-1,lf,v[u]),bld(ch[u][1],mid+1,r,v[u],rf);upd(u);
    }
    void reb(int &u,db lf,db rf)
    {
    	if(max(siz[ch[u][0]],siz[ch[u][1]])<=siz[u]*alp) return;
    	td=0;dfs(u);
    	bld(u,1,td,lf,rf);
    }
    int cmp(int x,int y){return s[x]==s[y]?v[x-1]<v[y-1]:s[x]<s[y];}
    void insert(int &u,int k,db lf=0,db rf=MAXD)
    {
    	if(!u){siz[u=k]=1;v[u]=(lf+rf)/2;ch[u][0]=ch[u][1]=0;return ;}
    	if(cmp(k,u)) insert(ch[u][0],k,lf,v[u]);
    	else insert(ch[u][1],k,v[u],rf);
    	upd(u);reb(u,lf,rf);
    }
    int val[N<<2];
    int p[N];
    void update(int u){val[u]=p[val[u<<1]]==p[val[u<<1|1]]?min(val[u<<1],val[u<<1|1]):(v[p[val[u<<1]]]<v[p[val[u<<1|1]]]?val[u<<1]:val[u<<1|1]);}
    void build(int u,int l,int r)
    {
    	if(l==r){val[u]=l;return;}
    	int mid=(l+r)>>1;
    	build(u<<1,l,mid),build(u<<1|1,mid+1,r);
    	update(u);
    }
    void change(int u,int l,int r,int k)
    {
    	if(l==r) return;
    	int mid=(l+r)>>1;
    	if(k<=mid) change(u<<1,l,mid,k);
    	else change(u<<1|1,mid+1,r,k);
    	update(u);
    }
    int qry(int u,int l,int r,int L,int R)
    {
    	if(L<=l && r<=R) return val[u];
    	int mid=(l+r)>>1,w1=0,w2=0;
    	if(L<=mid) w1=qry(u<<1,l,mid,L,R);
    	if(R>mid) w2=qry(u<<1|1,mid+1,r,L,R);
    	if(!w1 || !w2) return w1+w2;
    	return p[w1]==p[w2]?min(w1,w2):(v[p[w1]]<v[p[w2]]?w1:w2);
    }
    char opt[3];
    int main()
    {
    	freopen("1.in","r",stdin);
    	freopen("1.ans","w",stdout);
    	int n,m,l,T,rt=0;
    	scanf("%d%d%d%d",&n,&m,&l,&T);
    	scanf("%s",s+1);
    	reverse(s+1,s+l+1);
    	for(int i=1;i<=l;i++) insert(rt,i);
    	for(int i=1;i<=n;i++) scanf("%d",&p[i]);
    	build(1,1,n);
    	int las=0;
    	for(int i=1;i<=m;i++)
    	{
    		int v,w;
    		scanf("%s%d",opt+1,&v);
    		if(opt[1]=='I')
    		{
    			v^=las*T;
    			s[++l]='a'+v;
    			insert(rt,l);
    		}
    		else if(opt[1]=='C')
    		{
    			scanf("%d",&w);
    			p[v]=w;
    			change(1,1,n,v);
    		}
    		else
    		{
    			scanf("%d",&w);
    			printf("%d
    ",las=qry(1,1,n,v,w));
    		}
    	}
    	return 0;
    }
    

    3.SA-IS

    这里

    4.Lyndon 分解

    首先定义 ( ext{Lyndon Word}) :所有后缀中字典序最小的。

    比如 (ababb) 就是一个 ( ext{Lyndon Word})

    Lyndon 分解具体来说,你需要把一个字符串分解为若干个 ( ext{Lyndon Word}),并且字典序单调不增。

    这里先给出算法:

    我们枚举当前串左端点 (i),处理循环串的左右端点 ([l,r])。每次比较 (r+1)(l) 的字典序,如果 (r+1) 大,那么我们当前的分割还是可行的。否则就是不可行。我们直接把 (l) 移回当前串的左端点即可。复杂度 ((n))

    考虑为什么是对的。由于我比较菜,这里只能“显然法”证明。

    假如 (u,v)( ext{Lyndon Word}),而 (u<v),那么 (uv) 也是一个 ( ext{Lyndon Word})。否则 (u|v) 是一个 ( ext{Lyndon}) 分解。(结论看起来很显然)

    所以根据上面这个结论,我们考虑一个性质:对于某个串 ([l,r])([l,r])( ext{Lyndon Word})([1,l-1])( ext{Lyndon}) 分解已经完成。假如 (s_{r+1}<s_{l}),那么在 (r) 处分解一定是一个 ( ext{Lyndon Word})。我们可以直接切开重新处理。

    那么具体来说我们就要分类讨论。当前左端点 (i)(即 ([1,i-1])( ext{Lyndon}) 分解已经完成),同时周期串的开头末尾分别为 ([l,r])

    如果 (s_{l}=s_{r+1}),那么我们可以认为这里存在循环的可能,所以 (r+1) 我们需要保留。同时我们让 (l+1),即现在周期串是 ([l+1,r+1])

    比如当前串是 ( exttt{aabaaba}),那么我们暂定的分解是 ( exttt{aab|aab|a}),而当前周期串为 ( exttt{aba})

    如果 (s_{l}<s_{r+1}),那么后面的串一定会把 ([l,r]) 所有保留的串都合并掉,所以令 (l=i)

    如果 (s_{l}>s_{r+1}),那么至此就是一个合法的 ( ext{Lyndon}) 分解,前面所有的暂定分解都成立,所以直接按暂定分解处理即可。

    #include<cstdio>
    #include<algorithm>
    #include<cstring>
    #define N 5000010
    using namespace std;
    char s[N];
    int p[N],t;
    int main()
    {
    	scanf("%s",s+1);
    	int n=strlen(s+1);
    	for(int i=1,j=1,k=2;i<=n;j=i,k=i+1)
    	{
    		for(;k<=n && s[j]<=s[k];k++)
    		{
    			if(s[j]<s[k]) j=i;
    			else j++;
    		}
    		for(;i<=j;i+=k-j) p[++t]=i+(k-j)-1;
    	}
    	int ans=0;
    	for(int i=1;i<=t;i++) ans^=p[i];
    	printf("%d
    ",ans);
    	return 0;
    }
    

    5.Runs

    由于我比较菜,很多地方有点模棱两可甚至需要“感性证明”,如果想要严谨证明可以参考WC2019课件

    首先先要给出“循环和循环”的标准定义:

    定义一个整数 (p) 是一个字符串 (s) 的 period 当且仅当 (forall iin[1,|s|-p],s_{i}=s_{i+p})

    即某一个字符串不断循环得到 (s)。这里并不一定要整循环,比如 (3) 就是 ( exttt{abbabba}) 的一个 period。

    接下来给出 Run 的标准定义:

    定义三元组 ((i,j,p)) 是一个 Run 当且仅当 (s_{j+1} eq s_{j-p+1},s_{i-1} eq s_{i+p-1})(p)([i,j]) 最小的出现至少两次的循环节。

    通俗来讲就是对于子串 (s[i:j]) 的最小的 period (p(pleq frac{j-i+1}{2})) 且不能往左往右延伸。显然对于任意 ((i,j)) 其最小 period (p) 唯一。

    比如 ( exttt{ababababba}) 中:

    ((1,8,2)) 是一个 Run。

    ((3,8,2)) 不是,因为这个这个周期串可以继续向左延伸。

    ((1,8,4)) 不是,因为 (p=4) 不是 (s[1:8]) 的最小 period。

    ((6,10,3)) 不是,因为 (p=3) 没有出现至少两次,即 (p>frac{10-6+1}{2})


    接下来考虑一些 Run 的性质。首先给出一些关于 Run 的定义:

    定义 (Runs(s)) 是字符串 (s) 的所有 (Run) 的集合。

    定义一个 Run ((l,r,p)) 的指数是 (frac {r-l+1} p),即 period (p)(s[l:r]) 中的出现次数。记作 (e_{(l,r,p)})

    定义两种偏序关系 (<_0,<_1)。由于只要满足 (a<_0 b ightarrow b<_1 a),所以一般这里 (<_0) 表示“字典序较小”,(<_1) 表示“字典序较大”。一般我们认为空字符字典序最小,即 (varnothing<_0 exttt{a} , exttt{a}<_0 exttt{aa})

    定义 (s[lambda_l:lambda_r])((i,j,p))(<) 上的 Lyndon Root,当且仅当 ([lambda_l,lambda_r]subseteq [i,j])(s[lambda_l:lambda_r]) 是一个 Lyndon Word。

    定义 (B(r)) 表示一个 Run (r) 的所有除去开头的 Lyndon Root 的集合。除去开头的意思是集合中所有元素 (s[i:j]) 要求 (i eq r_l)。显然有 (|B(r)|geq lfloor e_r-1 floorgeq 1)

    定义 (Beg(B(r))) 表示 (B(r)) 的所有起始端点组成的集合。

    定义 (l_{ell}(i)) 表示所有从 (i) 开始的 Lyndon Word 中最长的那个。即 (l_{ell}(i)=[i,j],j=max{j'|s[i:j'] ext{是一个 Lyndon Word}})

    为了方便,我们额外定义一个 Runs ((i,j,p))(ell(ellin{0,1})) 型的,当且仅当 (s_{j-p+1}<_{ell}s_{j+1})

    有了以上定义,我们就可以推出一些定理了:

    引理1:对于字符串 (s=t^kt[:p]),其中 (t) 是一个 Lyndon Word,(t[:p]) 表示 (t) 的一个前缀且在这里允许为空。现在在其后加入字符 (a)(a eq t_{p+1})。如果 (a<t_{p+1}) 那么 (s=t^kt[:p]a) 是任意前缀中最长的 Lyndon Word。

    这个其实就是上面 Lyndon 分解所用的结论。

    推论1:对于 (ell) 型 Run ((i,j,p)),其所有定义在 (<_{ell}) 上的 Lyndon Root (lambda=[l_{lambda},r_{lambda}]) 都有 (lambda=l_{ell}(l_{lambda})),即其所有 Lyndon Root 都是相同起点中最长的 Lyndon Word。

    推论2:对于任意 Run (r(i,j,p)) ,其所有的 Lyndon Root 左端点不重复,即有 (|Beg(B(r))|=|B(r)|)

    引理2:对于任意位置 (pin[1,|s|]),有且仅有一个 (ellin{0,1}) 满足 (l_{ell}(i)=s[i:i]),这里默认字符串末尾存在一个空字符。

    证明:考虑找到向右第一个与当前字符不同的位置 (p')(包括最后的空字符)。即 (min{p'|p'>i,s_{p'} eq s_p})。令 (ell) 满足 (s_{p'}<_{ell}s_p),由引理1显然有 (l_{ell}=[i,i])(l_{!ell} eq[i,i])。(此处 WC2019 的课件貌似有误?课件好像取的是 (max))。

    推论3:(forall r,r ext{is a Run},r eq r'),有 (Beg(B(r)) cap Beg(B(r'))=varnothing)

    具体证明大概就是根据引理 2 用反证推出如果存在交集,取交集的位置就能推出周期性,与 Run 的定义不符。

    结论1:任意位置 (p) 最多存在于一个 (Beg(B(r))) 中。由一个 Lyndon Root 能唯一确定一个 Run (r)。由于 (forall r,1 otin Beg(B(r)))(sum|Beg(B(r))|leq n-1),故有 (|Runs(s)|<|s|)

    结论2:由于 (Beg(B(r))>e_r-1)。由结论 1 可推得 (sum{e_r}leq 3n-3)


    接下来考虑如何快速求的 (Runs(s))

    考虑我们先求出 (l_{ell}(i)),即以 (i) 开头最长的 Lyndon Word。

    考虑 Lyndon Word 的性质。对于两个定义于 (ell) 的 Lyndon Word (u,v(u<_{ell}v)),有 (uv) 也是定义于 (ell) 的 Lyndon Word。

    那么我们用一个单调栈维护当前存在的 Lyndon Word,然后从后往前加字符,每次比较与栈顶元素的大小,如果能合并就合并。可以证明这样栈顶得到的串就是最长的 Lyndon Word。

    获得了所有 (l_{ell}(i)) 后,我们令 (p=l_{ell}(i)-i+1),观察其向左向右能延伸到的最远的地方 ([l',r'])。对于三元组 ((l',r',p)),我们只需要判断其是否满足 (pleq frac{r'-l'+1} 2) 即可。

    实现细节:一种比较 simple 的方式是用 Hash+二分处理两个串的 lcp/lcs。对于子串大小比较直接比较 lcp 的后一位即可。判断延伸到的最远的位置 ([l',r']),可以发现对于 (l') 一定有 (s[l':i]=s[l'+p-1:s+p-1]),所以直接取 (s[:i],s[:l_{ell}(i)]) 的 lcp 即可。(r') 同理。这样总复杂度是 (O(nlog n))

    上述复杂度瓶颈在于求 lcp。用 SA-IS 套 (O(n)-O(1)) ST表可以优化到 (O(n))

    至此就完成了 Runs 的求解。

    LOJ173 Runs

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<vector>
    #define N 1000010
    #define ull unsigned long long
    #define B 2333
    using namespace std;
    char str[N];
    int s[N],n;
    ull h[N],bs[N];
    ull get(int l,int r){return h[r]-h[l-1]*bs[r-l+1];}
    int lcp(int x,int y)
    {
        int l=1,r=n-max(x,y)+1,res=0;
        while(l<=r)
        {
            int mid=(l+r)>>1;
            if(get(x,x+mid-1)==get(y,y+mid-1)) l=mid+1,res=mid;
            else r=mid-1;
        }
        return res;
    }
    int lcs(int x,int y)
    {
        int l=1,r=min(x,y),res=0;
        while(l<=r)
        {
            int mid=(l+r)>>1;
            if(get(x-mid+1,x)==get(y-mid+1,y)) l=mid+1,res=mid;
            else r=mid-1;
        }
        return res;
    }
    bool cmp(int l1,int r1,int l2,int r2)//s[l1:r1]<s[l2:r2]
    {
        int l=lcp(l1,l2);
        if(l>min(r1-l1,r2-l2)) return r1-l1<r2-l2;
        return s[l1+l]<s[l2+l];
    }
    struct runs{
        int i,j,p;
        runs(int i=0,int j=0,int p=0):i(i),j(j),p(p){}
        bool operator ==(const runs a)const{return i==a.i && j==a.j && p==a.p;}
        bool operator <(const runs a)const{return i==a.i?j<a.j:i<a.i;}
    };
    vector<runs>ans;
    int st[N],t,run[N];
    void lyndon()
    {
        t=0;
        for(int i=n;i;i--)
        {
            st[++t]=i;
            for(;t>1 && cmp(i,st[t],st[t]+1,st[t-1]);t--);
            run[i]=st[t];
        }
    }
    void init()
    {
        bs[0]=1;
        for(int i=1;i<=n;i++) h[i]=h[i-1]*B+s[i],bs[i]=bs[i-1]*B;
    }
    void get_runs()
    {
        for(int i=1;i<=n;i++)
        {
            int l1=i,r1=run[i],l2=l1-lcs(l1-1,r1),r2=r1+lcp(l1,r1+1);
            if(r2-l2+1>=(r1-l1+1)*2) ans.push_back(runs(l2,r2,r1-l1+1));
        }
    }
    int main()
    {
        scanf("%s",str+1);
        n=strlen(str+1);
        for(int i=1;i<=n;i++) s[i]=str[i]-'a'+1;
        init();
        lyndon();
        get_runs();
        for(int i=1;i<=n;i++) s[i]=27-s[i];
        lyndon();
        get_runs();
        sort(ans.begin(),ans.end());
        ans.erase(unique(ans.begin(),ans.end()),ans.end());
        printf("%d
    ",(int)ans.size());
        for(auto u:ans) printf("%d %d %d
    ",u.i,u.j,u.p);
        return 0;
    }
    

    6.KMP自动机与 border tree

    咕咕咕。

    7.隐式后缀树

    暂时移到这里,密码可以猜一猜或者线下问我。

  • 相关阅读:
    CentOS中用top命令CPU负载
    Oracle文档阅读指南
    修改oracle用户密码永不过期
    让secureCRT以命令行方式启动图形界面
    .NET 2.0 SQL依赖注意事项(转载:原文《使用SQL Server 2005 Query Notification的几个注意事项》)
    SQL Server 2005服务状态管理工具
    一个常用的正则表达验证类
    解决TFS中MSBuild开发项目和开发编译中一些冲突(小技巧)
    ASP.NET 2.0 WebService中传递DataTable参考
    LightningFramework系列(各层详细设计类图)
  • 原文地址:https://www.cnblogs.com/Flying2018/p/13741568.html
Copyright © 2011-2022 走看看