1. 程式人生 > 實用技巧 >2020牛客暑期多校訓練營(第一場)題解

2020牛客暑期多校訓練營(第一場)題解

Name Date Rank Solved A B C D E F G H I J
2020 Multi-University,Nowcoder Day 1 2020.7.12 101/1216 3/10 Ø Ø × Ø × O × × O O

A.B-Suffix Array(字尾陣列+結論)

題目描述

  給出一個長為 \(n\) 字串 \(s\),定義字串 \(s_1\) ~ \(s_n\) 對應的 \(B_1\) ~ \(B_n\) 滿足 \(B_i=\min\limits_{1\leq j < i,s_j=s_i}\{i-j\}\),如果不存在這樣的 \(j\),那麼 \(B_i=0\)

,將 \(s\) 的所有後綴按照其對應的 \(B\) 序列的字典序排列並輸出。

  多組輸入,字符集只包括 \(a,b\)\(1\leq n\leq 10^5\)

分析

  定義 \(C_i=\min\limits_{i<j\leq n,s_j=s_i}\{j-i\}\)。對於不存在 \(j\)\(C_i\),使它比其他 \(C_i\) 都大,即設 \(C_i=n\),最後再把 \(n+1\) 這個數字放在 \(C\) 序列的結尾,求出 \(C\) 的字尾陣列,去掉最後一位倒著輸出就是答案。

證明

  \(B_i\) 相當於當前位置 \(i\) 前面最近的一個與 \(s_i\) 相同的字元到 \(i\)

的距離,而 \(C_i\) 相當於當前位置 \(i\) 後面的最近的與 \(s_i\) 相同的字元到 \(i\) 的距離。

  \(1.\) 首先考慮一種特殊情況。

  對於字尾 \(aaaab\cdots\),其 \(B\) 陣列開頭的一段為 \([0,1,1,1,0,\cdots]\),兩個 \(0\) 代表第一次出現的 \(a\) 字元和 \(b\) 字元。

  考慮兩個 \(0\) 中間連續 \(1\) 子序列的長度,它的長度越長,字典序就越大,比如對於字尾 \(X=aaab\)\(Y=aaaab\) ,其 \(B\) 陣列分別為 \([0,1,1,0]\)\([0,1,1,1,0]\),顯然後者的字典序更大。

  考慮這兩個字尾的 \(C\) 陣列,分別為 \([1,1,4,4,5]\)\([1,1,1,5,5,6]\),可以發現前者的字典序更大。

  也就是說,對於開頭的 \(1\) 長度不同的兩個字尾,如果字尾 \(X\)\(B\) 陣列比字尾 \(Y\)\(B\) 陣列字典序小,則字尾 \(X\)\(C\) 陣列一定比字尾 \(Y\)\(C\) 陣列的字典序大。

  \(2.\) 接下來考慮一般情況。

  假設兩個字尾 \(X,Y\)\(B\) 陣列有一段字首是相同的,那麼 \(X,Y\) 的這一段字首一定也是相同的,假設這段公共字首是若干個 \(a\),那麼肯定滿足 \(X=\cdots aaab\cdots,Y=\cdots aaaab\cdots\)

  他們的 \(B\) 陣列分別為:\([z,1,1,x,\cdots]\)\([z,1,1,1,y,\cdots]\)。所以後綴 \(X\)\(B\) 陣列的字典序比字尾 \(Y\)\(B\) 陣列的字典序大。

  考慮它們的 \(C\) 陣列,相當於把 \(B\) 陣列的每個字元與其前面第一個相同的字元對齊,\(C\) 陣列分別為 \([z,1,1,x,\cdots],[z,1,1,y,\cdots]\)

  可以發現,\(x,y\) 之前的位都是相同的,也就是說,\(x,y\) 之間的大小關係決定了這兩個字尾的字典序。對於一開始 \(x,y\) 的位置,可以發現 \(x\) 那個位置離上一個 \(b\) 更近一些,即滿足 \(x<y\),所以後綴 \(X\)\(C\) 陣列的字典序比字尾 \(Y\)\(C\) 陣列的字典序要小。

  綜合上述兩種情況,可以知道,對於兩個字尾 \(X,Y\),假如 \(X\)\(B\) 陣列的字典序比 \(Y\)\(B\) 陣列的字典序要小,那麼總有 \(X\)\(C\) 陣列的字典序比 \(Y\)\(C\) 的字典序要大。

  由數學歸納法可知,求出 \(C\) 陣列的字尾陣列再反過來就是答案。

  證畢。

  仔細思考可以發現,\(C\) 陣列相對於 \(B\) 陣列的優越性在於:字串某個字尾的 \(C\) 陣列一定是這個字串的 \(C\) 陣列的某個字尾,所以求出 \(C\) 的字尾陣列,就相當於將原字串的字尾進行排序了。而 \(B\) 陣列不滿足這個性質,對於字串的某個字尾,它的 \(B\) 陣列不一定是原字串的 \(B\) 陣列的字尾,所以不能直接字尾排序。

  為什麼末尾要放一個 \(n+1\)

  假如末位是 \(ab\),對應兩個字尾 \(ab,b\),它們的 \(B\) 陣列分別為 \([0,0],[0]\),所以 \(b\)\(B\) 陣列字典序比 \(ab\)\(B\) 陣列字典序小,那麼 \(b\) 的字典序比 \(ab\) 小;而我們最後要將 \(C\) 陣列的 \(sa\) 陣列倒序輸出,也就是說,在倒序之前,\(b\)\(C\) 陣列的字典序比 \(ab\)\(C\) 陣列的字典序要大。

  但是事實上 \(b\)\(C\) 陣列為 \([2]\)\(ab\)\(C\) 陣列為 \([2,2]\),即 \(b\)\(C\) 陣列字典序比 \(ab\) 小,在 \(C\) 陣列末尾加一個 \(n+1\) 就可以解決這個問題。

程式碼

#include <bits/stdc++.h>
using namespace std;
const int N=1000010;
char s[N];
int n,m,w,C[N];
int sa[N],rk[N<<1],oldrk[N<<1],ID[N],px[N],cnt[N];
bool cmp(int x,int y,int w)
{
    return oldrk[x]==oldrk[y]&&oldrk[x+w]==oldrk[y+w];
}
void SA(int n,int m,int *s)
{
    m=n;
    for(int i=1;i<=n;i++)
    {
        rk[i]=s[i];
        cnt[rk[i]]++;
    }
    for(int i=2;i<=m;i++)
        cnt[i]=cnt[i]+cnt[i-1];
    for(int i=n;i>=1;i--)
    {
        sa[cnt[rk[i]]]=i;
        cnt[rk[i]]--;
    }
    int p;
    for(w=1;w<n;w<<=1,m=p)
    {
        p=0;
        for(int i=n;i>n-w;i--)
            ID[++p]=i;
        for(int i=1;i<=n;i++)
            if(sa[i]>w)
                ID[++p]=sa[i]-w;
        for(int i=1;i<=m;i++)
            cnt[i]=0;
        //memset(cnt,0,sizeof(cnt));
        for(int i=1;i<=n;i++)
        {
            px[i]=rk[ID[i]];
            cnt[px[i]]++;
        }
        for(int i=1;i<=m;++i)
            cnt[i]=cnt[i]+cnt[i-1];
        for(int i=n;i>=1;--i)
        {
            sa[cnt[px[i]]]=ID[i];
            cnt[px[i]]--;
        }
        for(int i=1;i<=n;i++)
            swap(rk[i],oldrk[i]);
        //memcpy(oldrk,rk,sizeof(rk));
        rk[sa[1]]=p=1;
        for(int i=2;i<=n;i++)
        {
            if(cmp(sa[i],sa[i-1],w))
                rk[sa[i]]=p;
            else
                rk[sa[i]]=++p;
        }
        if(p==n)
            break;
    }
}
int main()
{
    while(~scanf("%d",&n))
    {
        scanf("%s",s+1);
        for(int i=1;i<=n;i++)
        {
            for(int j=i+1;j<=n;j++)
            {
                if(s[i]==s[j])
                {
                    C[i]=j-i;
                    break;
                }
            }
            if(C[i]==0)
                C[i]=n;
        }
        C[n+1]=n+1;
        SA(n+1,m,C);
        for(int i=n;i>=1;i--)
            printf("%d ",sa[i]);
        puts("");
        for(int i=1;i<=n+1;i++)
            C[i]=sa[i]=rk[i]=oldrk[i]=ID[i]=px[i]=cnt[i]=0;
    }
    return 0;
}