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\)
多組輸入,字符集只包括 \(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\)
\(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;
}