字符串算法总结

KMP

  求border

for (int i = 2, j = 0; i <= m; i++)
{
    while (b[i] != b[j + 1] && j) j = nex[j];
    if (b[i] == b[j + 1]) j++;
    nex[i] = j;
}

AC自动机    自动AC的机器 

  相当于$KMP+Trie$,用于求多个模式串的匹配

//构造 
inline void ACbuild()
{
    queue<int> que;
    for (int i = 0; i < 26; i++) if (trie[0][i]) que.push(trie[0][i]);
    while (que.size())
    {
        int u = que.front();
        que.pop();
        for (int i = 0; i < 26; i++)
        {
            if (trie[u][i])
            {
                last[trie[u][i]] = trie[last[u]][i];
                que.push(trie[u][i]);
            }
            else trie[u][i] = trie[last[u]][i];
        }
    }
}

 

后缀数组 SA

  将字符串的$1\leq i\leq n,s[i,n]$按照从小到大排序,用倍增和计数排序

  $O(nlogn)$

int main()
{
    n = strlen(str + 1);

    register int i, w, p, m = 300;
    for (i = 1; i <= n; ++i)
        cnt[rk[i] = str[i]]++;
    for (i = 1; i <= m; ++i)
        cnt[i] += cnt[i - 1];
    for (i = n; i; --i)
        sa[cnt[rk[i]]--] = i;

    for (w = 1;; w <<= 1, m = p)
    {
        for (p = 0, i = n; i > n - w; --i)
            id[++p] = i;
        for (i = 1; i <= n; ++i)
            if (sa[i] > w)
                id[++p] = sa[i] - w;
        memset(cnt, 0, sizeof(cnt));
        for (int i = 1; i <= n; ++i)
            ++cnt[px[i] = rk[id[i]]];
        for (int i = 1; i <= m; i++)
            cnt[i] += cnt[i - 1];
        for (i = n; i; --i)
            sa[cnt[px[i]]--] = id[i];
        memcpy(last, rk, sizeof(rk));
        for (p = 0, i = 1; i <= n; ++i)
            rk[sa[i]] = (last[sa[i]] == last[sa[i - 1]] && last[sa[i] + w] == last[sa[i - 1] + w]) ? p : ++p;
        if (p == n)
        {
            for (int i = 1; i <= n; ++i)
                sa[rk[i]] = i;
            break;
        }
    }
}

 

  用途:

  寻找最小的循环移动位置,例题[JSOI2007]字符加密

  在主串T中在线寻找模式串S,将T后缀数组排序后在里面二分查找S

  从字符串首尾提取字符最小化字典序,例题[USACO07DEC] Best Cow Line

  $height$数组

  $height[i]=lcp(sa[i],sa[i-1])$,第i名的后缀与它前一名的后缀的LCP,$height[1]=0$

  $O(n)$求$height$

for (int i = 1, k = 0; i <= n; i++)
{
    if (k) k--;
    while (s[i + k] == s[sa[rk[i] - 1] + k]) k++;
    height[rk[i]] = k;
}

  $height$数组的用处

  求两子串的最长公共前缀:$LCP(sa[i],sa[j])=min(height[i+1...j])$,之后就可以$RMQ$(区间最大最小值)解决

  比较两子串大小,若要比较的是$A=S[a...b],B=S[c...d]$,若$LCP(a,c)\geq min(\left | A \right |,\left | B \right |),A<B\Leftrightarrow \left | A \right | < \left | B \right |$,否则$A<B \Leftrightarrow rk[a]<rk[b]$

  求不同子串数目,$\frac{n(n+1)}{2}-\sum_{i=2}^{n}height[i]$

  出现至少k次的子串的最大长度,直接看题[USACO06DEC] Milk Patterns

  后缀自动机(SAM)

  详情看Wiki or 后缀自动机详解

  构造模板

inline void add(int c)
{
    int p = last, np = last = ++tot;
    s[tot] = 1;
    d[np].len = d[p].len + 1;
    for (; p && !d[p].ch[c]; p = d[p].fa) d[p].ch[c] = np;

    if (!p) d[np].fa = 1;
    else
    {
        int q = d[p].ch[c];
        if (d[q].len == d[p].len + 1) d[np].fa = q;
        else
        {
            int nq = ++tot;
            d[nq] = d[q];
            d[nq].len = d[p].len + 1;
            d[q].fa = d[np].fa = nq;
            for (; p && d[p].ch[c] == q; p = d[p].fa) d[p].ch[c] = nq;
        }
    }
}

int main()
{
    scanf("%s", str + 1);
    n = strlen(str + 1);
    for (int i = 1; i <= n; i++) add(str[i] - 'a' + 1);
}

  广义后缀自动机

  Wiki

 后缀树(suffixTree)

  将字符串 $S$ 的所有后缀构成的压缩 $Trie$  ,压缩 $Trie$ 指将没有分支的链压缩成一个节点的 $Trie$ 

  详解传送门

  代码来源传送门

struct suffixTree
{
    int n, tot, now, rem; // n 已插入的字符数  tot 后缀树上节点数  now 当前走到的节点  rem 剩余未插入的后缀长度
    int ch[N][30];        // 树上节点的边
    int link[N], len[N], start[N], s[N];
    // link 后缀连接  len 节点所代表字符串长度
    // start 节点所代表字符串的首字母在原串中的下标  s 表示字符串原串
    // 每个节点 v 所代表的字符串即为 str[start[v],start[v]+len[v]-1]
    // 若len[v]=inf 则表示该节点代表从 start[v] 直到最后的字符串

    int new_node(int sta, int l) // 建立新节点
    {
        link[++tot] = 1, len[tot] = l, start[tot] = sta;
        return tot;
    }

    void extend(int x) // 插入 x 字符
    {
        s[++n] = x, rem++;

        for (int last = 1; rem;)
        {
            while (rem > len[ch[now][s[n - rem + 1]]])
                rem -= len[now = ch[now][s[n - rem + 1]]];

            int &v = ch[now][s[n - rem + 1]];
            int c = s[start[v] + rem - 1];

            if (!v || x == c) // 新建一个节点或者该后缀已经存在后缀树中
            {
                link[last] = now;
                last = now;

                if (!v)
                    v = new_node(n, inf);
                else
                    break;
            }
            else // 新节点在一条边中间 需要将边分裂
            {
                int u = new_node(start[v], rem - 1);
                ch[u][c] = v;
                ch[u][x] = new_node(n, inf);
                start[v] += rem - 1, len[v] -= rem - 1;
                link[last] = v = u, last = u;
            }

            if (now == 1) // 插入完成一次 将未插入的串首字符弹出继续插入
                rem--;
            else
                now = link[now];
        }
    }
}
View Code

 

Manacher 马拉车

  计算$str$中最长回文子串的长度

  $d1[i]$表示以$i$为中心的长度为奇数的回文子串的半径,$d2[i]$ 表示 $i$ 为中心的长度为偶数的回文子串的半径

  例:下标以 $1$ 开始的 $str=[a,b,c,b,b,c]$中,$d1[3]=2:[b,c,b]$,$d2[5]=2:[c,b,b,c]$

for (int l = 1, r = -1, i = 1; i <= n; i++)
{
    int k = i > r ? 1 : min(d1[l + r - i], r - i);
    while (0 < i - k && i + k <= n && str[i - k] == str[i + k]) k++;
    d1[i] = k--;
    if (i + k > r) l = i - k, r = i + k;
    ans = max(ans, (d1[i] - 1) * 2 + 1);
}
for (int l = 1, r = -1, i = 1; i <= n; i++)
{
    int k = i > r ? 0 : min(d2[l + r - i + 1], r - i + 1);
    while (0 < i - k - 1 && i + k <= n && str[i - k - 1] == str[i + k]) k++;
    d2[i] = k--;
    if (i + k > r) l = i - k - 1, r = i + k;
    ans = max(ans, d2[i] * 2);
}

 回文自动机

  看wiki吧,解释不清

最小表示法

  若两个字符串 $A,B$ ,有 $A[i~n]+A[1~i-1]=B$ 则称两个字符串循环同构

  最小表示法便是求与字符串循环同构的字典序最小的字符串

inline int get_min(char s[])
{
    int k = 0, i = 0, j = 1;
    while (k < n && i < n && j < n)
    {
        if (s[(i + k) % n] == s[(j + k) % n]) k++;
        else
        {
            if (s[(i + k) % n] > s[(j + k) % n]) i = i + k + 1;
            else j = j + k + 1;
            if (i == j) j++;
            k = 0;
        }
    }
    return min(i, j);
}

 扩展KMP

  求母串的每一个后缀与字串的公共前缀 $LCP$ ,$extend[i]$ 表示母串以 $i$ 开头的后缀与字串的 $LCP$ ,$nex[i]$ 表示字串以 $i$ 开头的后缀与字串的 $LCP$

  模板:【LuoguP5410】

// s母串 t字串
inline void get_nex()
{
    nex[0] = m;
    int dqx = 0;
    while (t[dqx] == t[dqx + 1] && dqx + 1 < m)
        dqx++;
    nex[1] = dqx;

    int p0 = 1;
    for (int i = 2; i < m; i++)
    {
        if (i + nex[i - p0] < nex[p0] + p0)
            nex[i] = nex[i - p0];
        else
        {
            dqx = nex[p0] + p0 - i;
            dqx = max(dqx, 0);
            while (t[dqx] == t[dqx + i] && dqx + i < m)
                dqx++;
            nex[i] = dqx;
            p0 = i;
        }
    }
}

inline void get_exkmp()
{
    get_nex();

    int dqx = 0;
    while (s[dqx] == t[dqx] && dqx < min(n, m))
        dqx++;
    extend[0] = dqx;
    int p0 = 0;
    for (int i = 1; i < n; i++)
    {
        if (i + nex[i - p0] < extend[p0] + p0)
            extend[i] = nex[i - p0];
        else
        {
            dqx = extend[p0] + p0 - i;
            dqx = max(dqx, 0);
            while (t[dqx] == s[dqx + i] && dqx < m && dqx + i < n)
                dqx++;
            extend[i] = dqx;
            p0 = i;
        }
    }
}

 

posted on 2020-12-29 22:26  ArrogHie  阅读(203)  评论(0编辑  收藏  举报