zoukankan      html  css  js  c++  java
  • Lyndon分解学习小记

    参考资料:

    https://www.luogu.com.cn/blog/wucstdio/solution-p6127

    https://oi-wiki.org/string/lyndon/

    因为有参考资料所以这里不写详细。


    Lyndon串:如果一个串是Lyndon串,当且仅当所有后缀中字典序最小的串是这个串本身。

    另一个定义:所有循环同构串中字典序最小的串是这个串本身。两者等价。

    Lyndon分解:将一个字符串(s)分解成(s_1s_2dots s_k),满足(s_1ge s_2ge dotsge s_k),并且(s_i)为Lyndon串。Lyndon分解存在且唯一


    引理1:如果有两个串(u,v)为Lyndon串,且(u<v),则(uv)为Lyndon串。

    引理2:如果存在串(v)和字符(c)满足(vc)为某个Lyndon串的前缀,则如果有字符(d>c)(vd)为Lyndon串。

    这里有证明:https://www.luogu.com.cn/blog/wucstdio/solution-p6127


    Duval 算法

    现在把字符串分成三个部分:(S_1S_2S_3)

    其中(S_1)已经完成Lyndon分解,(S_2)为Lyndon近似串,即可以分解成:(wwwdots wv)(w)为Lyndon串,(v)(w)的前缀。

    有三个指针(i,j,k),其中(S_1=[1,i-1],S_2=[i,k-1])(j=k-|w|)

    现在尝试将(s_k)加入(S_2)中,考虑指针的变化:

    1. 如果(s_k=s_j),则(j)(k)分别后移。
    2. 如果(s_k>s_j),根据引理2(vs_k)为Lyndon串。再根据引理1,它要和前面的若干个(w)合并。也就是说(S_2s_k)合并成一个Lyndon串,成为新的(S_2),即(jleftarrow i)。(此时只是表明它们合并成了Lyndon串,并不表明它们已经分解好,所以没有归到(S_1)
    3. 如果(s_k<s_j),把(S_2)的前缀(若干个(w))丢入(S_1)中,留下(v)(i)位置变更,(kleftarrow i+1)(jleftarrow i)

    具体见代码(洛谷模板):

    using namespace std;
    #include <bits/stdc++.h>
    #define N 5000005
    int n;
    char s[N];
    int q[N],cnt;
    int main(){
    	freopen("in.txt","r",stdin);
    	scanf("%s",s+1);
    	n=strlen(s+1);
    	int i=1;
    	while (i<=n){
    		int k=i+1,j=i;
    		while (k<=n && s[k]>=s[j]){
    			if (s[k]==s[j])
    				++j;
    			else
    				j=i;
    			++k;
    		}
    		while (i<=j){
    			q[++cnt]=i+k-j-1;
    			i+=k-j;
    		}
    	}
    	int ans=0;
    	for (int i=1;i<=cnt;++i)
    		ans^=q[i];
    	printf("%d
    ",ans);
    	return 0;
    }
    

    时间复杂度分析:

    首先(i)最多增加(O(n))。考虑最后那个while(i)要变化成什么,最后一定是(j<ile k)

    转化成这样的问题:有一个队列,有个指针(k),一开始在位置(1)。每次指针会往后移动一位。你可以在任意时刻,把长度至少(frac{k}{2})的前缀截掉,并将(k)移到剩下的队列的位置(1)。问整个队列都被截掉为止,指针最多会移动多少次。

    可以这么想:一开始势能为(n),截掉一个长度为(l)的前缀增加(l)的势能。于是至多只会移动(2n)次。

    oi-wiki上说内层循环次数不超过(4n-3)。它说的循环和我算的大概不是一个东西,但总之就是(O(n))

    进一步思考:

    1. 在进行算法时,如果遇到(s_k=s_j),称其为2情况;如果遇到(s_k>s_j),称其为3情况;特殊地,如果有一个刚刚重置的(i),把它视作(k),称其为1情况。对于一个(k),如果经历了一次3情况,那么它不会再次经历1情况和2情况和3情况。

      解释:遇到3情况时,此时(S_2=w=s[i,k])(i)不变的时候,这个(k)不可能再被遍历到;(i)如果变了,一定会变到大于等于(k+1)的位置(指这里的(k)而不是那时的(k))。

    2. (S_2=wwwdots wv),其中(v)也有可能会由(w'w'w'dots)组成(即存在一个循环前缀)。所以在(i)变化后,需要将(k,j)重置。

    3. Lyndon串的前缀不一定是Lyndon串。设这个Lyndon串为(s),取前缀(s[1,d])。此时可能存在位置(x),满足(s[x,d]<s[1,d]),此时一定有(s[x,d])(s[1,d])的前缀。

    (下面例题中的代码中有有关性质的测试)


    例题

    hdu6761 Minimum Index

    题意:给出一个字符串,对于每个前缀求最小后缀的位置。(nle 10^6,sum nle 2*10^7)

    上一年多校的时候遇见的,当时rush了一个SAM做法,挂掉了。半年后再看,当时rush的SAM写了好多错误的细节啊……

    SAM做法:考虑一个子串(s[l,r])可以贡献到(r)的答案当中。建SAM之后,按照转移边按字典序从小到大遍历SAM(遍历过的节点不再遍历),把这个节点在fail树上的子树打标记。打标记暴力打,如果存在了标记就不打。时间(O(n)),有(26)的小常数。本地跑官方数据大概10s。

    using namespace std;
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    #define N 1000010
    #define mo 1000000007
    #define ll long long
    int n;
    char s[N];
    struct Node{
        Node *c[26],*fail;
        int len;
    } d[N*2],*S,*T;
    int cnt;
    Node *newnode(){++cnt;memset(&d[cnt],0,sizeof(Node));return &d[cnt];}
    void insert(int ch){
        Node *nw=newnode();
        nw->len=T->len+1;
        Node *p=T;
        for (;p && p->c[ch]==0;p=p->fail)
            p->c[ch]=nw;
        if (!p)
            nw->fail=S;
        else{
            Node *q=p->c[ch];
            if (q->len==p->len+1)
                nw->fail=q;
            else{
                Node *clone=newnode();
                memcpy(clone->c,q->c,sizeof q->c);
                clone->fail=q->fail;
                clone->len=p->len+1;
                for (;p && p->c[ch]==q;p=p->fail)
                    p->c[ch]=clone;
                nw->fail=q->fail=clone;
            }
        }
        T=nw;
    }
    int id[N];
    struct EDGE{
        int to;
        EDGE *las;
    } e[N*2];
    int ne;
    EDGE *last[N*2];
    int fa[N*2];
    int cov[N*2];
    bool vis[N*2];
    void cover(int x,int l){
        if (cov[x])
            return;
        cov[x]=l;
        for (EDGE *ei=last[x];ei;ei=ei->las)
            cover(ei->to,l);
    }
    void dfs(int x,int s){
        if (vis[x])
            return;
        vis[x]=1;
        if (x!=S-d)
            cover(x,s);
        for (int i=0;i<26;++i)
            if (d[x].c[i])
                dfs(d[x].c[i]-d,s+1);
    }
    int main(){
    	freopen("in.txt","r",stdin);
    //	freopen("out.txt","w",stdout);
        int Q;
        scanf("%d",&Q);
        while (Q--){
            scanf("%s",s+1);
            n=strlen(s+1);
            cnt=0;
            S=T=newnode();
            for (int i=1;i<=n;++i)
                insert(s[i]-'a'),id[i]=T-d;
            ne=0;
            memset(last,0,sizeof(EDGE*)*(cnt+1));
            for (int i=2;i<=cnt;++i){
                fa[i]=d[i].fail-d;
                e[ne]={i,last[fa[i]]};
                last[fa[i]]=e+ne++;
            }
            memset(cov,0,sizeof(int)*(cnt+1));
            memset(vis,0,sizeof(bool)*(cnt+1));
            dfs(S-d,0);
            ll ans=0;
            for (int i=1,pw=1;i<=n;++i,pw=(ll)pw*1112%mo){
                int x=i-cov[id[i]]+1;
                printf("%d ",x);
    //            (ans+=(ll)x*pw)%=mo;
            }
    //        printf("
    ");
            printf("%lld
    ",ans);
        }
        return 0;
    }
    

    重新看这题的时候尝试想SA做法,想出个常数比较大的,而且不够优(根据后面对Lyndon分解的分析可以对此进行大幅度优化),所以不讲。

    【Lyndon分解做法1】

    首先进行Lyndon分解。询问前缀(s[1,d])的最小后缀,首先这个后缀的开头一定在(d)所在Lyndon串(记为(s[h,d]))内。如果有后缀(s[x,d]<s[h,d]),那么一定有(s[x,d])(s[h,d])的前缀。

    考虑对于每个Lyndon串分别处理。对于一个Lyndon串(s[h,t])来说,(s[x,dots])可以贡献的(d)([x,x+LCP(s[h,t],s[x,t])-1])中。求串中所有后缀和整个串的LCP,这就是exkmp中的(next)数组。跑个exkmp。然后扫一遍,用个单调栈来维护后面哪些区间贡献是多少。

    时间(O(n))。然而实测大概和上面SAM做法差不多,10s?

    using namespace std;
    #include <bits/stdc++.h>
    #define N 1000005
    #define mo 1000000007
    #define ll long long
    #define fi first
    #define se second
    #define mp(x,y) make_pair(x,y)
    int n;
    char s[N];
    int p[N],cnt;
    int a[N];
    void doit(char s[],int a[],int n,int os){
    	static int nxt[N];
    	nxt[0]=n;
    	int pos=0,mr=0;
    	for (int i=1;i<n;++i){
    		nxt[i]=(i<mr?min(nxt[i-pos],mr-i):0);
    		while (i+nxt[i]<n && s[nxt[i]]==s[i+nxt[i]])
    			++nxt[i];
    		if (mr<nxt[i])
    			mr=nxt[i],pos=i;
    	}
    	a[0]=0+os;
    	static int st[N];
    	int tp=1;
    	st[1]=0;
    	for (int i=1;i<n;++i){
    		while (tp && st[tp]+nxt[st[tp]]<i+nxt[i])
    			--tp;
    		st[++tp]=i;
    		while (tp && st[tp]+nxt[st[tp]]<=i)
    			--tp;
    		a[i]=st[tp]+os;			
    	}
    }
    int main(){
    	freopen("in.txt","r",stdin);
    	freopen("out.txt","w",stdout);
    	int T;
    	scanf("%d",&T);
    	while (T--){
    		scanf("%s",s+1);
    		n=strlen(s+1);
    		cnt=0;
    		int i=1;
    		while (i<=n){
    			int k=i+1,j=i;
    			while (k<=n && s[k]>=s[j]){
    				j=(s[k]==s[j]?j+1:i);
    				++k;
    			}
    			while (i<=j){
    				p[++cnt]=i;
    				i+=k-j;
    			}
    		}
    		p[cnt+1]=n+1;
    		for (int i=1;i<=cnt;++i)
    			doit(s+p[i],a+p[i],p[i+1]-p[i],p[i]);
    		ll ans=0;
    		for (int i=1,w=1;i<=n;++i,w=(ll)w*1112%mo)
    			(ans+=(ll)a[i]*w)%=mo;
    		printf("%lld
    ",ans);			
    	}
    	return 0;
    }
    

    【Lyndon分解做法2】

    如果暴力对(s)的每个前缀做Lyndon分解,那么最后一个Lyndon串显然就是答案。那么有没有增量构造的Lyndon分解算法?

    至少我想不出来。但尽管不是想象中的增量构造,还是有“伪”增量构造方法:

    (a_i)表示前缀(s[1,i])的最小后缀。跑一个普通的Lyndon分解,在算法中加入几句话:

    (i)刚刚被重置时,(a_ileftarrow i)。(记作1情况)

    (s_j=s_k)时,(a_kleftarrow a_j+k-j)。(记作2情况)

    (s_j<s_k)时,(a_kleftarrow i)。(记作3情况)

    解释:1情况,(s[1,i])最后一个Lyndon串是(s[i,i]);2情况,因为(S_2=wwwdots wv)(v)(w)的前缀,(a_k)应该在(v)中,因为(j)(k)相当于在(w)中同样的位置,(a_j)也应该在(v)中,两者等价,可以继承;3情况,(s[i,k])成为一个Lyndon串。

    时间(O(n))。常数极小,可以通过。

    可能存在的问题:

    1. (a_k)不一定是(v)的开头。因为如果单对(v)分解,有可能还是会分成多个Lyndon串,因为Lyndon串的前缀不一定是Lyndon串。
    2. 由于(i)重置过后(j,k)都需要被重置,所以某个(a_k)可能会被赋值多次。然而每次赋的值都是一样的。
    using namespace std;
    #include <bits/stdc++.h>
    #define N 1000005
    #define mo 1000000007
    #define ll long long
    int n;
    char s[N];
    int a[N],b[N],c[N];
    int main(){
    	freopen("in.txt","r",stdin);
    	freopen("out.txt","w",stdout);
    	int T;
    	scanf("%d",&T);
    	while (T--){
    		scanf("%s",s+1);
    		n=strlen(s+1);
    		memset(a,0,sizeof(int)*(n+1));
    		memset(b,0,sizeof(int)*(n+1));
    		int i=1;
    		while (i<=n){
    			int k=i+1,j=i;
    			if (a[i])
    				assert(a[i]==i);
    			static int tim;
    			if (b[i]==3)
    				printf("fuck
    ");
    			a[i]=i,b[k]=1;
    			while (k<=n && s[k]>=s[j]){
    				if (s[k]==s[j]){
    					if (a[k])
    						assert(a[k]==a[j]+k-j);
    					if (b[k]==3)
    						printf("fuck
    ");
    					a[k]=a[j]+k-j,b[k]=2;
    					++j,++k;
    				}
    				else{
    					if (a[k])
    						assert(a[k]==i);
    					++tim;
    					if (b[k]==3)
    						printf("fuck
    ");
    					a[k]=i,b[k]=3;
    					j=i,++k;
    				}
    			}
    			i=k-(j-i)%(k-j); 
    //			while (i<=j)
    //				i+=k-j;
    		}
    		ll ans=0;
    		for (int i=1,w=1;i<=n;++i,w=(ll)w*1112%mo)
    			(ans+=(ll)a[i]*w)%=mo;
    		printf("%lld
    ",ans);			
    	}
    	return 0;
    }
    
  • 相关阅读:
    8bit数据 转换为 16bit数据的四种方法
    可变长度的结构体定义
    【转】typedef和#define的用法与区别
    编程事项
    FreeRTOS不允许在中断服务程序和临界段中执行不确定的性的操作
    低优先级任务在执行过程中高优先级任务在干什么
    使用FreeRTOS在SD卡驱动使用非系统延时导致上电重启不工作的情况
    PMOS 与 NMOS
    keil优化等级设置
    #define用法之一
  • 原文地址:https://www.cnblogs.com/jz-597/p/14587731.html
Copyright © 2011-2022 走看看