zoukankan      html  css  js  c++  java
  • 后缀数组及关于排序的一个小问题 ACM-ICPC 2015 Changchun Preliminary Contest F. Favorite Donut

    题目链接:https://nanti.jisuanke.com/t/A1838

    如果没学过后缀数组:

    推荐博客1:https://www.cnblogs.com/shanchuan04/p/5324009.html

    推荐博客2(理解height数组):https://blog.csdn.net/Mtrix/article/details/52079650#commentBox

    推荐博客3:https://blog.csdn.net/yxuanwkeith/article/details/50636898

    额,先看1,2和3交叉看吧(仅供参考)。

    题目意思:给出一个字符串,求它顺时针方向和逆时针方向字典序最大的那个串,把字符串看成首尾连接的字符串,假如现在有字符串abcd,那么顺时针方向有abcd、bcda、cdab、dabc;逆时针方向有dcba、cbad、badc、adcb。那么字典序最大的就是逆时针方向上的dcba,它的位置是4(首字母在原串的位置)。注意:假如字典序最大的有多个,那么取位置(也就是首字母在原串中的位置)最小的那个,如果位置也相同,那么取顺时针方向的那个。最终答案是要我们输出字典序最大的子串的位置和方向(顺时针用0表示,逆时针用1表示)。

    我也是刚刚学后缀数组,比较菜,可能写的不是很明白。

     

     

    首先说思路(有其他做法,这里只说后缀数组,因为其他的我不会):

    我们先计划把顺时针中字典序最大的和逆时针中字典序最大的字符串找出来(有多个就拿位置最小的那个),最后比较一下这两个就行了。

     

     

    首先对于顺时针的所有字符串,我们把原串复制一遍到后面,就是abcd变成abcdabcd(这时字符串长度为8),再跑出sa数组和height数组(height数组在顺时针可以不跑),那么字典序最大的字符串位置就是sa[8]。我一开始补题的时候没搞懂为啥直接拿最后一个就是了,万一有多个字典序最大,你怎么保证位置最小的那个字符串的排位最大呢?

     

     

    我们换一个例子,假如现在字符串是abab,复制一遍就是abab abab。那么它的后缀有abab|abab(我在这里加一个符号'|'用来分隔)、baba|bababab|abbaba|b、abab、bab、ab、b。其中粗体部分是我们所需要的(包含了原串所有的可能字符串)。如果是原串abcd的话,会有两个abab的情况出现对吧,在复制后的串中就是abab|abab、abab|ab ,对于这两个,谁字典序更大?当然是前面的那个,谁的位置更小?也是前面的那个(位置分别是1和3),我们发现原串中字典序相同的字符串中,谁的位置小,那么在复制之后的串中,它符号'|'后面的字符串就越长,那么也就代表着它在复制之后串中的字典序越大,因为符号'|'后面的字符部分就是符号'|'前面字符串的前缀,abcd是abcd的前缀,ab是abcd的前缀。

    abab|ab  ab

    abab|ab 

    上面粗体部分是一 一对应的,是相等的,但是上面那个最后多了一个ab,所以说在原串中字典序相同时,谁的位置更小,那么在复制一遍后的串中,原串位置小的字符串在复制串中的后缀就更长,又因为谁更长谁就更大,所以,我们发现对原串复制一遍,再求sa数组,在字典序相同时,原串中位置小的排位会更大,位置大的排位会更小。所以我们直接拿sa[8]位置上的字符串,至于上面四个非粗体的后缀,我们可以用sa[i]<=4这个条件来筛掉它。

    来一个代码测试一下(rank数组在这里变成了x数组,风格可能不太一样):

    #include<iostream>
    #include<cstring>
    #include<algorithm>
    #include<queue>
    #include<map>
    #include<stack>
    #include<cmath>
    #include<vector>
    #include<set>
    #include<cstdio>
    #include<string>
    #include<deque> 
    using namespace std;
    typedef long long LL;
    #define eps 1e-8
    #define INF 0x3f3f3f3f
    #define maxn 1000005
    char s[maxn];
    int sa[maxn],x[maxn],y[maxn],c[maxn];
    //数组sa:构造完成前表示关键字数组,下标表示名次,值表示关键字的首字符位置,
    //值相同的时候名次根据在原串中相对位置的先后决定;
    //构造完成后表示后缀数组,下标表示名次,值表示后缀的首字符位置。
    
    //数组x:表示rank数组,下标表示关键字的位置,值表示关键字大小(rank),相同的值有相同的rank。
    //初始化为字符串r的每个字符大小(此时x并不代表rank,只借助其值比较相对大小)。
    //在每次迭代后,根据sa重新赋值,并代表rank。
    
    //数组y:排序后的第二关键字数组,下标表示名次,值代表第二关键字的首字符位置
    int n,m,k,t;
    void build_sa(int n){
        //先来一次基数排序 
        m=128;
        for(int i=1;i<=m;i++) c[i]=0;//先把桶清空 
        for(int i=1;i<=n;i++) c[x[i]=s[i]]++;
        //s[i]现在表示的是ascll码值,也就是排位,现在就是计算各个排位的字符数量 
        for(int i=2;i<=m;i++) c[i]+=c[i-1];//计算一下前缀和,下面要用 
        for(int i=n;i>=1;i--) sa[c[x[i]]--]=i;//sa的下标表示当前关键字排位,值表示关键字的位置 
        // 如果排位相同,那么同一排位按照出现次序从小到大排序 ,注意这里的i是倒序,从n到1 
        for(int k=1;k<n;k<<=1){//增加的关键字长度, 
            int num=1;
            for(int i=n-k+1;i<=n;i++)//在不含有第二关键字的数后面补0,这样他们的第二关键字最小,都放在前面 
            y[num++]=i;
            for(int i=1;i<=n;i++){
                if(sa[i]>k)
                y[num++]=sa[i]-k;
            }
            
            for(int i=1;i<=m;i++) c[i]=0;
            for(int i=1;i<=n;i++) c[x[i]]++; 
            for(int i=2;i<=m;i++) c[i]+=c[i-1];
            for(int i=n;i>=1;i--) sa[c[x[y[i]]]--]=y[i],y[i]=0;
            
            swap(x,y);
            num=1,x[sa[1]]=1;
            for(int i=2;i<=n;i++)
            x[sa[i]]=y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k]?num:++num;
            if(num>=n)
            break;
            m=num;     
        }
    }
    int main()
    {
        while(scanf("%s",s+1)!=EOF){
            int len=strlen(s+1);
            for(int i=1;i<=len;i++)
            s[len+i]=s[i];
            len*=2;
            build_sa(len);
            for(int i=1;i<=len;i++){
                if(sa[i]<=len/2)//筛一下 
                cout<<sa[i]<<' ';
            }
            cout<<endl;
        }
        return 0;
    }
    /*输入输出 
    abab
    3 1 4 2
    */
    View Code

     

    现在我们讨论逆时针,假设还是abab,翻转一下就是baba,再复制一下就是baba baba,问题就转化为了求这个字符串中顺时针方向的字典序最大的后缀,它的所有后缀有baba|babaabab|abababa|baabab|a、baba、aba、ba、a。因为我们把它翻转了一下,所以在字典序相同时原串中位置小的在这个翻转之后的串中位置反而是更大的,我们在sa[8]这个位置上的字符串确实是字典序最大的,但是在字典序最大的字符串有多个的时候他的在原串中的位置反而是最大的了,不是最小的了,因为我们翻转了翻转了翻转了(我指的是所有字典序最大的串中),所以这个时候就需要设法找到字典序最大并且在原串中位置最小的那个字符串位置了,这里需要用到height数组,height[i]指的是字典序排名为i的后缀和字典序排名为i-1的后缀之间的最长公共前缀(这个如果不懂的话看,可以试着看上面博客)

     

    我们接着,对于baba baba这个字符串,假如height[i]的值>=4,那么就说明字典序排名为i的后缀和字典序排位为i-1的后缀之间的最长公共子串长度大于等于4,那么其实我们就可以把这两个后缀"看成"字典序相同的字符串了,因为它们包含的原串部分是相同的(符号|前面的部分是一样的),符号'|'后面的部分不理他,并且排位为i-1的后缀它在这个翻转复制处理之后的字符串里的位置一定是更大的(相对于排位为i的字符串),这个我们在上面顺时针情况时就讨论了,但是因为我们翻转了,所以在原串中的位置反而是更小了,所以我们一直往前判断height[i]>=4是否成立,一旦成立,那么更新字典序最大的字符串为sa[i-1](也就是更新为排位为i-1的字符串了),否则立刻跳出。这样我们就在逆时针方向上找到了字典序最大并且在原串中位置最小的字符串了。

    最后顺时针和逆时针的两个字典序最大的字符串比较一下,判断一下就可以了。

    代码:

     

    #include<iostream>
    #include<cstring>
    #include<algorithm>
    #include<queue>
    #include<map>
    #include<stack>
    #include<cmath>
    #include<vector>
    #include<set>
    #include<cstdio>
    #include<string>
    #include<deque> 
    using namespace std;
    typedef long long LL;
    #define eps 1e-8
    #define INF 0x3f3f3f3f
    #define maxn 60005
    int n,m,k,t,num;
    char s[maxn],ss[maxn];
    int x[maxn],y[maxn],sa[maxn],c[maxn],height[maxn]; 
    void build_sa(){
        m=127;
        for(int i=1;i<=m;i++) c[i]=0;
        for(int i=1;i<=n;i++) c[x[i]=s[i]]++;
        for(int i=2;i<=m;i++) c[i]+=c[i-1];
        for(int i=n;i>=1;i--) sa[c[x[i]]--]=i;
        for(int k=1;k<n;k<<=1){
            num=0;
            for(int i=n-k+1;i<=n;i++)
            y[++num]=i;
            for(int i=1;i<=n;i++){
                if(sa[i]>k)
                y[++num]=sa[i]-k;
            }
            
            for(int i=1;i<=m;i++) c[i]=0;
            for(int i=1;i<=n;i++) c[x[i]]++;
            for(int i=2;i<=m;i++) c[i]+=c[i-1];
            for(int i=n;i>=1;i--) sa[c[x[y[i]]]--]=y[i];
            
            swap(x,y);
            num=1;
            x[sa[1]]=1;
            for(int i=2;i<=n;i++)
            x[sa[i]]=y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k]?num:++num;
            m=num;
            if(m>=n) break;
        }
        
    }
    void getHeight(){
        int k=0,j=0;
        for(int i=1;i<=n;i++) x[sa[i]]=i;
        for(int i=1;i<=n;i++){
            if(k) k--;    //    k如果大于0,就减1,如果为0,就说明没有公共前缀,那就没必要减了 
            j=sa[x[i]-1];    //x[i]是位置为i的后缀的排位 
            while(s[i+k]==s[j+k]&&(i+k)<=n&&(j+k)<=n)//相同的话就一直往后比较,控制一下越界 
            k++;
            height[x[i]]=k;//排位为x[i]的后缀和排位为x[i]-1的后缀之间的最长公公共前缀的长度为k 
        }
        
    }
    int cmp(int ans1,int ans2){//比较顺时针位置为ans1的字符串和逆时针位置为ans2的字符串大小 
        for(int i=0;i<n/2;i++){
            if(ss[i+ans1]==s[i+ans2]) continue;
            else if(ss[i+ans1]>s[i+ans2]) return 1;
            else return -1;
        }
        return 0;
    }
    int main()
    {
        scanf("%d",&t);
        while(t--){
            scanf("%d",&n);
            scanf("%s",s+1);
            for(int i=1;i<=n;i++)//复制一遍到后面 
            s[i+n]=s[i];
            s[2*n+1]='';//保险 
            n*=2;//长度翻倍 
            for(int i=1;i<=n;i++)//把s数组拷贝到ss数组里面 
            ss[i]=s[i];
            ss[n+1]='';//保险 
            build_sa();
            int ans1,ans2;
            ans1=sa[n];//得到了顺时针方向字典序最大并且在原串中位置更小的字符串位置 
            for(int i=1;i<=n;i++){//s数组翻转,abab abab->baba baba 
                s[i]=ss[n-i+1];
            }
            build_sa();
            getHeight();//这里要求一遍height数组 
            ans2=sa[n];
            for(int i=n;i>=2;i--){
                if(sa[i-1]>n/2) break;//筛一下 
                if(height[i]>=n/2)//如果最长公共前缀长度大于等于n/2,那么就更新,否则跳出 
                ans2=sa[i-1];
                else
                break;
            }
            int ans,d;
            int jug=cmp(ans1,ans2); 
            ans2=n/2-ans2+1;//ans2是逆时针方向上的位置,我们要把它转化为顺时针方向上的位置 
            if(jug==0){
                if(ans1==ans2||ans1<ans2){
                    ans=ans1,d=0;
                }else{
                    ans=ans2,d=1;
                }
            }else if(jug<0){
                ans=ans2,d=1;
            }else{
                ans=ans1,d=0;
            }
            printf("%d %d
    ",ans,d);
        }
        return 0;
    }
  • 相关阅读:
    HDU- 1075 What Are You Talking About (Trie or map)
    HDU-1251 统计难题 (Trie)
    Trie树模板(插入和查询)
    HDU 5120 Intersection
    HDU 5025 Saving Tang Monk(状压bfs)
    Light OJ 1094 (树的直径)
    zzuli 1877 蛤玮打扫教室
    HDU 5642 King's Order(数位dp)
    POJ 2689 Prime Distance
    hdu 3853 LOOPS(概率dp)
  • 原文地址:https://www.cnblogs.com/6262369sss/p/11250193.html
Copyright © 2011-2022 走看看