1. 程式人生 > 實用技巧 >[學習筆記] 字尾陣列

[學習筆記] 字尾陣列

上一次把字尾自動機的部落格補了之後,現在我又來把字尾陣列這個坑給填了吧。

但有一說一這東西還是比字尾自動機好理解的,我完全看懂也沒花多久。我還是儘量把關鍵點都講清楚,在此基礎上儘量縮小篇幅,首先感謝一下這位大佬的部落格,我是看著他的部落格學的。

字尾陣列是什麼?

字尾陣列,顧名思義,我們肯定要求一個數組來完成許多複雜的功能。字尾陣列通常指 \(sa[i]\) 即字尾 \(i\) 的字典序排序,還要 \(height[i]\) 等等。不要著急,下文我們會詳細講。

對比於後綴自動機,字尾陣列的應用更為複雜,通常需要一些結論和高超的技巧。所以弱智的我通常選擇使用字尾自動機,他們能解決的問題很大程度上是重合的。但也有不少毒瘤題只能用字尾陣列來做,學習他是很有必要的。

如何求 sa?

先給出一些基礎的定義:\(rk[i]\) 表示把所有的字尾都放在一起字典序排序,字尾 \(i\) 的排名。\(sa[i]\) 表示排名為 \(i\) 的字尾是什麼(也就是他在原陣列中的起始下標)

暴力求他們是 \(O(n^2\log n)\) 的,優化用到了 倍增 的思想。

所以倍增什麼呢?我們選擇倍增 當前比較的長度 \(k\),具體來說,我們只找到每個字尾的前 \(k\) 位得到的排序結果。然後嘗試用這個結果快速擴充套件到 \(2k\),如果這個過程能做到 \(O(n)\) ,那麼時間複雜度就能做到 \(O(n\log n)\)

我們設 \(x[i]\) 為字尾 \(i\)

的第一關鍵字,也就是隻看前 \(k\) 位是排在第幾名的(如果前 \(k\) 為相同的話排名是一樣的),這個陣列是我們的已知條件 ,一定要利用好。

\(y[i]\) 為第二關鍵字,但是因為好寫的原因所以他表示的是 第二關鍵字排名為 \(i\) 的字尾是什麼 ,求出他對於求出在 \(2k\) 意義下的 \(x'\) 陣列具有重要意義。

問題變成了怎麼求 \(y\) 陣列,首先對於 \([n-k+1,n]\) 這些字尾是沒有第二關鍵字的,所以可以直接放在最前面(空串的字典序最小嘛)。然後倍增的思想一定會用到 \(k\) 意義下的結果的,最經典的就是 \(fa[i][j]=fa[fa[i][j-1]][j-1]\)

。類似地,我們可以考慮用 \(i+k\) 的第一關鍵字來搞 \(i\) 的第二關鍵字。

我們按 \(k\) 意義下的排名列舉字尾(也就是當前的 \(sa\) 陣列),如果 \(sa[i]>k\) ,那麼可以把字尾 \(sa[i]-k\) 加入 \(y\) 陣列中(所以我們要按字典序來嘛)

得到 \(y\) 以後我們先用 \(x\) 建一個桶,然後 \(y\) 在後面的字尾就先拿他的排名(保證 \(x\) 同類按 \(y\) 排序),這樣我們就得到了新的 \(sa\) ,然後我們在處理新的 \(x\) 就可以了,這一部分不是關鍵,可以直接看程式碼。

但是我懶得寫註釋了,我就直接用了大佬帶註釋的程式碼,侵刪(有些奇怪的巨集定義不用管):

inv get_SA()
{
	for (rint i=1;i<=n;++i) ++c[x[i]=s[i]];
	//c陣列是桶 
	//x[i]是第i個元素的第一關鍵字 
    for (rint i=2;i<=m;++i) c[i]+=c[i-1]; 
    //做c的字首和,我們就可以得出每個關鍵字最多是在第幾名 
    for (rint i=n;i>=1;--i) sa[c[x[i]]--]=i; //排名為...的字尾是i 
    for (rint k=1;k<=n;k<<=1)
    {
        rint num=0;
        for (rint i=n-k+1;i<=n;++i) y[++num]=i;
        //y[i]表示第二關鍵字排名為i的數,第一關鍵字的位置 
		//第n-k+1到第n位是沒有第二關鍵字的 所以排名在最前面 
        for (rint i=1;i<=n;++i) if (sa[i]>k) y[++num]=sa[i]-k;
        //排名為i的數 在陣列中是否在第k位以後
		//如果滿足(sa[i]>k) 那麼它可以作為別人的第二關鍵字,就把它的第一關鍵字的位置新增進y就行了
		//所以i列舉的是第二關鍵字的排名,第二關鍵字靠前的先入隊 
		//所以這裡應該是看排名為i的數的貢獻 
        for (rint i=1;i<=m;++i) c[i]=0;
        //初始化c桶 
        for (rint i=1;i<=n;++i) ++c[x[i]];
        //因為上一次迴圈已經算出了這次的第一關鍵字 所以直接加就行了 
        for (rint i=2;i<=m;++i) c[i]+=c[i-1];//第一關鍵字排名為1~i的數有多少個 
        for (rint i=n;i>=1;--i) sa[c[x[y[i]]]--]=y[i],y[i]=0;
        //因為y的順序是按照第二關鍵字的順序來排的 
        //第二關鍵字靠後的,在同一個第一關鍵字桶中排名越靠後 
        //基數排序 
        swap(x,y);
		//這裡不用想太多,因為要生成新的x時要用到舊的,就把舊的複製下來,沒別的意思 
        x[sa[1]]=1;num=1;
        for (rint 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;
        //因為sa[i]已經排好序了,所以可以按排名列舉,生成下一次的第一關鍵字 
        if (num==n) break;
        m=num;
        //這裡就不用那個122了,因為都有新的編號了 
    }
    for (rint i=1;i<=n;++i) putout(sa[i]),putchar(' ');
}

如何求 height?

如果字尾陣列只能排序的話那真是太雞肋了,他的更廣闊的應用需要 \(height[i]\) 也就是 \(sa[i-1]\)\(sa[i]\) 的最長公共字首,我們來證明若干結論(有關求法和應用),約定 \(lcp(i,j)\) 表示字尾 \(sa[i],sa[j]\) 的最長公共字首。

結論1:\(lcp(i,k)=\min(lcp(i,j),lcp(j,k))\;\;1\leq i\leq j\leq k\leq n\)

\(p=\min(lcp(i,j),lcp(j,k))\),那麼 \(lcp(i,j)\geq p,lcp(j,k)\geq p\),這個不等式其實是有物理意義的,也就是 \(sa[i],sa[j]\) 的前 \(p\) 個字元相等,\(sa[j],sa[k]\) 的前 \(p\) 個字元相等,所以 \(lcp(i,k)\) 至少是 \(p\)

然後再用反證法,假設 \(lcp(i,k)\)\(p+1\),那麼 \(s_i[p+1]=s_k[p+1]\),而我們知道 \(s_i[p+1]\not=s_j[p+1]\) 或者 \(s_j[p+1]\not=s_k[p+1]\),所以可以推出來是不成立的,那麼 \(lcp(i,k)=p\)

這個結論有一些引申出來的結論,譬如:\(lcp(i,j)=\min(lcp(k,k+1))\;\;i\leq k<j\) ,其實這個就相當於一個 \(dp\),不難證明。那麼我們求出來了 \(height[i]\) 之後就可以用 \(st\)\(O(1)\) 求出兩個字尾 \(i,j\) 的最長公共字首。

結論2:\(h[i]\geq h[i-1]-1\)

這個結論是用來求 \(height\) 陣列的,\(h[i]\) 的定義是原來的字尾 \(i\) 與字尾 \(sa[rk[i]-1]\) 的最長公共字首,它的定義是基於原串的,根據定義可以知道:\(height[i]=h[rk[i]]\) ,那麼問題就轉化成了求 \(h\) 陣列。

設排名在後綴 \(i-1\) 的字尾是 \(k\) ,那麼他們的最長公共字首是 \(h[i-1]\),我們考慮字尾 \(k+1\)\(i\) 的關係,但請注意:他們兩個的關係並不能直接計算 \(h[i]\) ,但是考慮他們的關係會有奇效。

字尾 \(k+1\) 可以看成字尾 \(k\) 去掉首字元,字尾 \(i\) 可以看成字尾 \(i-1\) 去掉首字元,所以他們的最長公共字首是 \(h[i-1]-1\)(當前 \(h[i-1]=0\) 的情況顯然成立所以不予討論)

\(k+1\) 並不是排名在 \(i\) 前面的字尾,設真正排名在 \(i\) 前面的字尾是 \(j\),那麼 \(i,j\) 的最長公共字首一定大於等於 \(i,k+1\) 的最長公共字首,因為如果他不成立的話排名在 \(i\) 前面的字尾就是 \(k+1\) 了(還是反證法)


知道了這兩個結論求 \(height\) 不是有手就行?因為 \(h[i]\) 每次最多減少 \(1\),所以暴力跑的話複雜度是 \(O(n)\) 的,這種思想在字串問題中很常見了,比如:\(\tt kmp,exkmp,manacher\) 都是這種先繼承再暴力的思想。

inv get_height()
{
    rint k=0;
    for (rint i=1;i<=n;++i) rk[sa[i]]=i;
    for (rint i=1;i<=n;++i)  
    {
        if (rk[i]==1) continue;//第一名height為0 
        if (k) --k;//h[i]>=h[i-1]+1;
        rint j=sa[rk[i]-1];
        while (j+k<=n && i+k<=n && s[i+k]==s[j+k]) ++k;
        height[rk[i]]=k;//h[i]=height[rk[i]];
    }
    putchar(10);for (rint i=1;i<=n;++i) putout(height[i]),putchar(' ');
}

字尾陣列應用

做到了的話會慢慢補充的

\[\tt To\;\;be\;\;continued...... \]