『常見字符串算法概要』

常見字符串算法概要

字符串Hash

一般使用多項式\(\mathrm{Hash}\)賦權的方法,將字符串映射到一個正整數。html

\[f(s)=\sum_{i=1}^{|s|}|s_i|\times P^i\ (\bmod\ p)\]c++

能夠支持\(O(1)\)末端插入字符,\(O(1)\)提取一段字串的\(\mathrm{Hash}\)值。算法

每次查詢的衝突率大概在\(\frac{1}{p}\)左右,若是查詢次數較多,能夠採用雙模數\(\mathrm{Hash}\)數組

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 20 , Mod = 998244353 , P = 131;
inline int inc(int a,int b) { return a + b >= Mod ? a + b - Mod : a + b; }
inline int mul(int a,int b) { return 1LL * a * b % Mod; }
inline int dec(int a,int b) { return a - b < 0 ? a - b + Mod : a - b; }
inline void Inc(int &a,int b) { a = inc( a , b ); }
inline void Mul(int &a,int b) { a = mul( a , b ); }
inline void Dec(int &a,int b) { a = dec( a , b ); }
int Pow[N],val[N],n,m; char s[N];
inline int GetHash(int l,int r) { return dec( val[r] , mul( val[l-1] , Pow[r-l+1] ) ); }
int main(void)
{
    scanf( "%s" , s+1 );
    n = strlen( s + 1 );
    Pow[0] = 1;
    for (int i = 1; i <= n; i++)
        Pow[i] = mul( Pow[i-1] , P ) , val[i] = inc( s[i] - 'a' , mul( val[i-1] , P ) );
    scanf( "%d" , &m );
    for (int i = 1; i <= m; i++)
    {
        int l1,l2,r1,r2;
        scanf( "%d%d%d%d" , &l1 , &r1 , &l2 , &r2 );
        GetHash(l1,r1) == GetHash(l2,r2) ? puts("Yes") : puts("No");
    }
    return 0;
}

Trie樹

肯定性有限狀態自動機,識別且僅識別字符串集合\(S\)中的全部字符串。函數

支持\(O(|s|)\)插入字符串,\(O(|s|)\)檢索字符串。優化

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 20;
struct Trie
{
    int e[N][26],end[N],tot;
    Trie(void) { tot = 1; }
    inline void Insert(char *s)
    {
        int n = strlen( s + 1 ) , p = 1;
        for (int i = 1; i <= n; i++)
        {
            int c = s[i] - 'a';
            if ( !e[p][c] ) e[p][c] = ++tot;
            p = e[p][c];
        }
        end[p] = true;
    }
    inline bool Query(char *s)
    {
        int n = strlen( s + 1 ) , p = 1;
        for (int i = 1; i <= n; i++)
        {
            int c = s[i] - 'a';
            if ( !e[p][c] ) return false;
            p = e[p][c];
        }
        return end[p];
    }
};

Knuth-Morris-Pratt 算法

定義一個字符串的\(\mathrm{Border}\)爲其公共先後綴。ui

定義字符串的前綴函數\[\pi(p)=\max_{s(1,t)=s(p-t+1,p)}\{t\}\]spa

含義即爲字符串\(s\)的前綴\(s_p\)最長\(\mathrm{Border}\)的長度。遍歷字符串,每次從上一個位置的最長\(\mathrm{Border}\)處開始向後匹配,若是匹配失敗則再跳\(\mathrm{Border}\),直至匹配成功便可求出一個字符串的全部前綴函數。指針

定義勢能函數\(\Phi(p)\)爲前綴字符串\(s_p\)的最長\(\mathrm{Border}\)長度,根據\(\mathrm{Knuth-Morris-Pratt}\)算法,有\(\Phi(p)\leq\Phi(p-1)+1\),若暴力跳\(\mathrm{Border}\),則勢能下降,可知總時間複雜度爲\(O(n)\)code

若求出了一個字符串的前綴函數,則能夠實現單模式串的字符串匹配,失配就從最長的\(\mathrm{Border}\)處開始從新匹配便可,時間複雜度爲\(O(n+m)\),分析方法相似。

#include <bits/stdc++.h>
using namespace std;
const int N = 1000020;
int n,m,fail[N]; char s[N],t[N];
int main(void)
{
    scanf( "%s\n%s" , s+1 , t+1 );
    n = strlen( s + 1 ) , m = strlen( t + 1 );
    for (int i = 2 , j = 0; i <= m; i++)
    {
        while ( j && t[j+1] != t[i] ) j = fail[j];
        j += ( t[j+1] == t[i] ) , fail[i] = j;
    }
    for (int i = 1 , j = 0; i <= n; i++)
    {
        while ( j && ( t[j+1] != s[i] || j == m ) ) j = fail[j];
        j += ( t[j+1] == s[i] );
        if ( j == m ) printf( "%d\n" , i - m + 1 );
    }
    for (int i = 1; i <= m; i++)
        printf( "%d%c" , fail[i] , " \n"[ i == m ] );
    return 0;
}

Knuth-Morris-Pratt 自動機

對於一個字符串\(s\),定義其\(\mathrm{KMP}\)自動機知足:

\(1.\) 狀態數爲\(n+1\)
\(2.\) 識別全部前綴。
\(3.\) 轉移函數\(\delta(p,c)\)爲狀態\(p\)所對應前綴接上字符\(c\)後最長\(\mathrm{Border}\)位置前綴對應的狀態

構造方法與\(\mathrm{Knuth-Morris-Pratt}\)算法相似,時間複雜度爲\(O(n\Sigma)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 20;
struct KMPAutomaton
{
    int trans[N][26],n;
    inline void Build(char *s)
    {
        n = strlen( s + 1 ) , trans[0][s[1]-'a'] = 1;
        for (int i = 1 , j = 0; i <= n; i++)
        {
            for (int k = 0; k < 26; k++)
                trans[i][k] = trans[j][k];
            trans[i][s[i]-'a'] = i + 1;
            j = trans[j][ s[i] - 'a' ];
        }
    }
};

Aho-Corasick 自動機

肯定性有限狀態自動機,識別全部後綴在指定字符串集合\(S\)中的字符串。

首先,咱們初始化\(\mathrm{Aho-Corasick}\)自動機爲指定字符串集合\(S\)\(\mathrm{Trie}\)樹,而後按照\(\mathrm{bfs}\)序構造轉移函數\(\delta\)

咱們定義每個狀態有一個\(\mathrm{fail}\)指針,\(\mathrm{fail}(x)=y\)當且僅當狀態\(y\)表明的字符串是狀態\(x\)表明字符串的後綴,且\(y\)表明字符串的長度最長。

咱們只需\(\mathrm{bfs}\)\(\mathrm{Trie}\)樹,當節點\(x\)\(\mathrm{Trie}\)上存在字符爲\(c\)的轉移邊時,咱們令\(\delta(x,c)=\mathrm{Trie}(x,c)\),並更新其\(\mathrm{fail}\)指針爲\(\delta(\mathrm{fail}(x),c)\),反之,則能夠令\(\delta(x,c)=\delta(\mathrm{fail}(x),c)\),易知其正確性。

\(\mathrm{Aho-Corasick}\)自動機能夠實現多模式串的文本匹配,構造和匹配的時間複雜度均爲線性(值得注意的是,計算貢獻若是選擇暴跳\(\mathrm{fail}\),則時間複雜度沒法保證)。

\(\mathrm{Knuth-Morris-Pratt}\)自動機就是隻有一個串\(\mathrm{Aho-Corasick}\)自動機。

#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 20;
struct AhoCorasickautomaton
{
    int trans[N][26],fail[N],end[N],q[N],tot,head,tail;
    inline void insert(char *s,int id)
    {
        int len = strlen( s + 1 ) , now = 0;
        for (int i = 1; i <= len; i++)
        {
            int c = s[i] - 'a';
            if ( !trans[now][c] ) trans[now][c] = ++tot;
            now = trans[now][c];
        }
        end[id] = now;
    }
    inline void build(void)
    {
        head = 1 , tail = 0;
        for (int i = 0; i < 26; i++)
            if ( trans[0][i] ) q[++tail] = trans[0][i];
        while ( head <= tail )
        {
            int x = q[head++];
            for (int i = 0; i < 26; i++)
                if ( !trans[x][i] )
                    trans[x][i] = trans[fail[x]][i];
                else {
                    fail[trans[x][i]] = trans[fail[x]][i];
                    q[++tail] = trans[x][i];
                }
        }
    }
};

序列自動機

肯定性有限狀態自動機,識別且僅識別一個序列的全部子序列。

根據定義,能夠構造一個\(|s|+1\)個狀態的自動機,而後倒序連邊便可,每個狀態均可以做爲終止狀態,時間複雜度\(O(n\Sigma)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6;
struct SequenceAutomaton
{
    int trans[N][26],next[26];
    inline void Build(char *s)
    {
        int n = strlen( s + 1 );
        memset( next , 0 , sizeof next );
        for (int i = n; i >= 1; i--)
        {
            next[ s[i] - 'a' ] = i;
            for (int j = 0; j < 26; j++)
                trans[i-1][j] = next[j];
        }
    }
};

最小表示法

求出一個字符串\(s\)全部循環表示中字典序最小的一個。

能夠用兩個指針\(i,j\)掃描,表示比較\(i,j\)兩個位置開頭的循環同構串,並暴力依次向下比較,直到發現長度\(k\),使得\(s_{i+k}>s_{j+k}\),那麼咱們能夠直接令\(i=i+k+1\),由於對於任意的\(p\in[0,k]\),同構串\(s_{i+p}\)都比同構串\(s_{j+p}\)劣,因此不用再比較。

易知其時間複雜度爲\(O(n)\)

#include <bits/stdc++.h>
using namespace std;
const int N = 3e5 + 20;
int n,s[N<<1];
int main(void)
{
    scanf( "%d" , &n );
    for (int i = 1; i <= n; i++)
        scanf( "%d" , &s[i] ) , s[i+n] = s[i];
    int i = 1 , j = 2 , k;
    while ( i <= n && j <= n )
    {
        for (k = 0; k < n && s[i+k] == s[j+k]; k++);
        if ( k == n ) break;
        if ( s[i+k] > s[j+k] ) ( i += k + 1 ) += ( i == j );
        if ( s[i+k] < s[j+k] ) ( j += k + 1 ) += ( i == j );
    }
    i = min( i , j ) , j = i + n - 1;
    for (int p = i; p <= j; p++) printf( "%d " , s[p] );
    return puts("") , 0;
}

後綴自動機

肯定性有限狀態自動機,識別且僅識別一個字符串的全部後綴。

採用增量法構造,詳見『後綴自動機入門 SuffixAutomaton』

使用靜態數組存轉移邊,時空複雜度\(O(n\Sigma)\),用鏈表能夠將時間複雜度優化到\(O(n)\)。用平衡樹存轉移邊,時間複雜度\(O(n\log \Sigma)\),空間複雜度\(O(n)\)

struct SuffixAutomaton
{
    int trans[N][26],link[N],maxlen[N],tot,last;
    // trans爲轉移函數,link爲後綴連接,maxlen爲狀態內的最長後綴長度
    // tot爲總結點數,last爲終止狀態編號
    SuffixAutomaton () { last = tot = 1; } // 初始化:1號節點爲S
    inline void Extend(int c)
    {
        int cur = ++tot , p;
        maxlen[cur] = maxlen[last] + 1;
        // 建立節點cur
        for ( p = last; p && !trans[p][c]; p = link[p] ) // 遍歷後綴連接路徑
            trans[p][c] = cur; // 沒有字符c轉移邊的連接轉移邊
        if ( p == 0 ) link[cur] = 1; // 狀況1
        else {
            int q = trans[p][c];
            if ( maxlen[q] == maxlen[p] + 1 ) link[cur] = q; // 狀況2
            else {
                int cl = ++tot; maxlen[cl] = maxlen[p] + 1; // 狀況3
                memcpy( trans[cl] , trans[q] , sizeof trans[q] );
                while ( p && trans[p][c] == q )
                    trans[p][c] = cl , p = link[p];
                link[cl] = link[q] , link[q] = link[cur] = cl;
            }
        }
        last = cur;
    }
};

廣義後綴自動機

肯定性有限狀態自動機,識別且僅識別字符串集合\(S\)中全部字符串的全部後綴。

構造方法與狹義後綴自動機相似,只需在轉移邊產生衝突時分裂節點便可。

時空複雜度均與後綴自動機相同。

值得一提的是,廣義後綴自動機若是採用線段樹合併來維護\(\mathrm{endpos}\)集合,則需\(\mathrm{dfs}\)遍歷\(\mathrm{Parent}\)樹來合併,不能夠按照基數排序的拓撲序來合併

struct SuffixAutomaton
{
    int trans[N][26],link[N],maxlen[N],tot;
    SuffixAutomaton () { tot = 1; }
    inline int Extend(int c,int pre)
    {
        if ( trans[pre][c] == 0 )
        {
            int cur = ++tot , p;
            maxlen[cur] = maxlen[pre] + 1;
            for ( p = pre; p && !trans[p][c]; p = link[p] )
                trans[p][c] = cur;
            if ( p == 0 ) link[cur] = 1;
            else {
                int q = trans[p][c];
                if ( maxlen[q] == maxlen[p] + 1 ) link[cur] = q;
                else {
                    int cl = ++tot; maxlen[cl] = maxlen[p] + 1;
                    memcpy( trans[cl] , trans[q] , sizeof trans[q] );
                    while ( p && trans[p][c] == q )
                        trans[p][c] = cl , p = link[p];
                    link[cl] = link[q] , link[q] = link[cur] = cl;
                }
            }
            return cur;
        }
        else {
            int q = trans[pre][c];
            if ( maxlen[q] == maxlen[pre] + 1 ) return q;
            else {
                int cl = ++tot; maxlen[cl] = maxlen[pre] + 1;
                memcpy( trans[cl] , trans[q] , sizeof trans[q] );
                while ( pre && trans[pre][c] == q )
                    trans[pre][c] = cl , pre = link[pre];
                return link[cl] = link[q] , link[q] = cl;
            }
        }
    }
};

後綴樹

將一個字符串\(s\)的全部後綴插入到一個\(\mathrm{Trie}\)樹中,咱們稱這棵\(\mathrm{Trie}\)樹全部葉子節點的虛樹爲這個字符串的後綴樹。

根據\(\mathrm{endpos}\)等價類的定義及性質,容易得知原串倒序插入後綴自動機後的\(\mathrm{Parent}\)樹就是該串的後綴樹,因此能夠用後綴自動機的構造方法求後綴樹。

時間複雜度和後綴自動機的時間複雜度相同,能夠\(O(n)\)順帶求後綴數組。

#include <bits/stdc++.h>
using namespace std;
const int N = 2e5+20;
struct SuffixAutomaton
{
    int trans[N][26],link[N],maxlen[N],tot,last;
    int id[N],flag[N],trie[N][26],sa[N],rk[N],hei[N],cnt;
    // id 表明這個狀態是幾號後綴 , flag 表明這個狀態是否對應了一個真實存在的後綴
    SuffixAutomaton () { tot = last = 1; }
    inline void Extend(int c,int pos)
    {
        int cur = ++tot , p;
        id[cur] = pos , flag[cur] = true;
        maxlen[cur] = maxlen[last] + 1;
        for ( p = last; p && !trans[p][c]; p = link[p] )
            trans[p][c] = cur;
        if ( p == 0 ) link[cur] = 1;
        else {
            int q = trans[p][c];
            if ( maxlen[q] == maxlen[p] + 1 ) link[cur] = q;
            else {
                int cl = ++tot; maxlen[cl] = maxlen[p] + 1;
                memcpy( trans[cl] , trans[q] , sizeof trans[q] );
                while ( p && trans[p][c] == q )
                    trans[p][c] = cl , p = link[p];
                link[cl] = link[q] , id[cl] = id[q] , link[q] = link[cur] = cl;
            }
        }
        last = cur;
    }
    inline void insert(int x,int y,char c) { trie[x][c-'a'] = y; }
    inline void Build(char *s,int n)
    {
        for (int i = n; i >= 1; i--)
            Extend( s[i]-'a' , i );
        for (int i = 2; i <= tot; i++)
            insert( link[i] , i , s[ id[i] + maxlen[link[i]] ] );
    }
    inline void Dfs(int x)
    {
        if ( flag[x] ) sa[ rk[id[x]] = ++cnt ] = id[x];
        for (int i = 0 , y; i < 26; i++)
            if ( y = trie[x][i] ) Dfs(y);
    }
    inline void Calcheight(char *s,int n)
    {
        for (int i = 1 , k = 0 , j; i <= n; i++)
        {
            if (k) --k; j = sa[ rk[i]-1 ];
            while ( s[ i+k ] == s[ j+k ] ) ++k;
            hei[ rk[i] ] = k;
        }
    }
};
SuffixAutomaton T; char s[N];
int main(void)
{
    scanf( "%s" , s+1 );
    int n = strlen( s+1 );
    T.Build( s , n ) , T.Dfs(1);
    T.Calcheight( s , n );
    for (int i = 1; i <= n; i++)
        printf( "%d%c" , T.sa[i] , " \n"[ i == n ] );
    for (int i = 2; i <= n; i++)
        printf( "%d%c" , T.hei[i] , " \n"[ i == n ] );
    return 0;
}

迴文自動機

肯定性有限狀態自動機,識別且僅識別一個字符串\(s\)的全部迴文字串的右半部分

因爲迴文串分奇偶,因此迴文自動機有兩個初始狀態,分別表明奇迴文串和偶迴文串。

可使用數學概括法證實,字符串\(s\)最多隻有\(|s|\)個本質不一樣的迴文字串,因此迴文自動機的一個狀態就表明一個迴文字串。而回文自動機的一條轉移邊就表明在原串的兩邊各加一個字符,這樣轉移後的字符串仍然是迴文串,同時也解釋了爲何迴文自動機只識別迴文串的右半部分。

迴文自動機一樣採用增量法構造。對於每個狀態,咱們額外記錄其最長迴文後綴所對應的狀態,稱爲\(\mathrm{link}\)函數。當咱們在字符串末尾插入一個字符時,咱們從原串最後的狀態開始跳\(\mathrm{link}\),直至能夠構成迴文串,並肯定新的狀態。

對於新的狀態,仍然能夠繼續跳\(\mathrm{link}\),找到其最長迴文後綴。

能夠把迴文自動機看做兩棵樹,也稱爲迴文樹。對於\(\mathrm{link}\)指針,也構成了一棵樹,能夠稱之爲迴文後綴樹。定義勢能函數\(\Phi(p)\)表示狀態\(p\)在迴文後綴樹中的深度,根據構造算法,易知\(\Phi(p)\leq\Phi(\mathrm{link}(p))+1\),而跳\(\mathrm{link}\)則勢函數減少。又由於迴文自動機的狀態數是\(O(n)\)的,迴文後綴樹的最大深度也就是\(n\),能夠得知構造算法的時間複雜度不超過\(O(n)\)

其空間複雜度爲\(O(n\Sigma)\),使用鄰接表存邊,時間複雜度升至\(O(n\Sigma)\),空間複雜度降至\(O(n)\)。若是使用\(\mathrm{Hash}\)表存邊,時空複雜度均降至\(O(n)\)

因爲一個迴文串的最長迴文後綴必然是它的一個\(\mathrm{Border}\),因此迴文樹\(\mathrm{dp}\)可能用到\(\mathrm{Border\ Series}\)的等差性質。迴文自動機中就會額外記錄兩個參量\(\mathrm{dif}\)\(\mathrm{slink}\)\(\mathrm{dif}(x)=\mathrm{len}(x)-\mathrm{len}(\mathrm{link}(x))\)\(\mathrm{slink}(x)\)記錄了迴文後綴樹上\(x\)最深的一個祖先,知足\(\mathrm{dif}(\mathrm{slink}(x))\not=\mathrm{dif}(x)\),這些均可以在構造過程當中順帶維護。

#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 20 , Mod = 1e9 + 7;
struct PalindromesAutomaton
{
    int n,tot,last,link[N],slink[N],trans[N][26],len[N],dif[N],s[N];
    PalindromesAutomaton(void)
    {
        len[ last = 0 ] = 0 , link[0] = 1;
        len[1] = -1 , tot = 1 , s[0] = -1;
    }
    inline void Extend(int c)
    {
        int p = last; s[++n] = c;
        while ( s[n] != s[ n - len[p] - 1 ] ) p = link[p];
        if ( trans[p][c] == 0 )
        {
            int cur = ++tot , q = link[p];
            len[cur] = len[p] + 2;
            while ( s[n] != s[ n - len[q] - 1 ] ) q = link[q];
            link[cur] = trans[q][c] , trans[p][c] = cur;
            dif[cur] = len[cur] - len[ link[cur] ];
            if ( dif[cur] != dif[ link[cur] ] ) slink[cur] = link[cur];
            else slink[cur] = slink[ link[cur] ];
        }
        last = trans[p][c];
    }
};
相關文章
相關標籤/搜索