字符串

马拉车(manacher)算法

马拉车算法是用来 查找一个字符串的最长回文子串的线性方法

code
const int N = 2e5 + 10;

int n;
char a[N], s[N];
int p[N];

void init()
{
    int k = 0;
    s[k ++ ] = '$', s[k ++ ] = '#';
    for (int i = 0; i < n; i ++ )
    	s[k ++ ] = a[i], s[k ++ ] = '#';
    s[k ++ ] = '^';
    n = k;
}

void manacher()
{
    int mr = 0, mid;
    for (int i = 0; i < n; i ++ )
    {
        if (i < mr) p[i] = min(p[mid * 2 - i], mr - i);
        else p[i] = 1;
        while (s[i - p[i]] == s[i + p[i]])
            p[i] ++ ;
        if (i + p[i] > mr) 
        {
            mr = i + p[i];
            mid = i;
        }
    }
}

后缀数组

后缀数组算法,就是求出后缀次序的算法,也就是对所有后缀排个序的高效算法。

\(height\)数组

LCP(最长公共前缀)
两个字符串 \(S\)\(T\)\(LCP\) 就是最大的\(x(x\le \min(|S|, |T|))\) 使得 \(S_i=T_i\ (\forall\ 1\le i\le x)\)

height 数组的定义
\(height[i]=lcp(sa[i],sa[i-1])\),即第 \(i\) 名的后缀与它前一名的后缀的最长公共前缀。
\(height[1]\) 可以视作 \(0\)

O(n) 求 height 数组需要的一个引理
\(height[rk[i]]\ge height[rk[i-1]]-1\)

\(height\) 数组的应用

两子串最长公共前缀
\(lcp(sa[i],sa[j])=\min\{height[i+1..j]\}\)

比较一个字符串的两个子串的大小关系
假设需要比较的是 \(A=S[a..b] 和 B=S[c..d]\) 的大小关系。

\(lcp(a, c)\ge\min(|A|, |B|),A<B\iff |A|<|B|\)

否则,\(A<B\iff rk[a]< rk[c]\)

不同子串的数目
子串就是后缀的前缀,所以可以枚举每个后缀,计算前缀总数,再减掉重复。

「前缀总数」其实就是子串个数,为 \(n(n+1)/2\)

如果按后缀排序的顺序枚举后缀,每次新增的子串就是除了与上一个后缀的 \(LCP\) 剩下的前缀。这些前缀一定是新增的,否则会破坏 \(lcp(sa[i],sa[j])=\min\{height[i+1..j]\}\) 的性质。只有这些前缀是新增的,因为 \(LCP\) 部分在枚举上一个前缀时计算过了。

所以答案为:

\(\frac{n(n+1)}{2}-\sum\limits_{i=2}^nheight[i]\)

code(有注释)
const int N = 1e6 + 10;

int n, m;
char s[N];
int sa[N];//排名为i的是第几个后缀
int x[N], y[N], c[N];
int rk[N];//第i个后缀的排名是多少
int height[N];//sa[i]与sa[i-1]的最长公共前缀

void get_sa()
{
    //按第一个字母进行基数排序
    for (int i = 1; i <= n; i ++ ) c[x[i] = s[i]] ++ ;//统计每个字符的个数
    for (int i = 2; i <= m; i ++ ) c[i] += c[i - 1];//前缀和求小于当前字符的字符个数
    for (int i = n; i; i -- ) sa[c[x[i]] -- ] = i;//逆序使得相同字母的相对位置不变
    for (int k = 1; k <= n; k <<= 1)//倍增
    {
        //分成长度为k的两段,以第一段为第一关键字,第二段为第二关键字进行排序
        int num = 0;

        //按第二关键字排序,y为排序结果
        for (int i = n - k + 1; i <= n; i ++ ) y[ ++ num] = i;
        //第二关键字为空,直接加入排序
        for (int i = 1; i <= n; i ++ )
            if (sa[i] > k) y[ ++ num] = sa[i] - k;  
        //i开头的第2段为i+k开头的第1段

        //基数排序
        for (int i = 1; i <= m; i ++ ) c[i] = 0;
        for (int i = 1; i <= n; i ++ ) c[x[i]] ++ ;
        for (int i = 2; i <= m; i ++ ) c[i] += c[i - 1];
        for (int i = n; i; i -- )
            sa[c[x[y[i]]] -- ] = y[i], y[i] = 0;
        swap(x, y);

        //离散化
        x[sa[1]] = 1, num = 1;
        for (int 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;
        //判断排名为i和i-1的后缀的两段是否分别相等

        if (num == n) break;
        m = num; 
    }
}

void get_height()
{
    for (int i = 1; i <= n; i ++ ) rk[sa[i]] = i;
    //排名为i的后缀的排名为i
    for (int i = 1, k = 0; i <= n; i ++ )
    {
        if (rk[i] == 1) continue;
        if (k) k -- ;
        int j = sa[rk[i] - 1];//比当前后缀排名小1的后缀
        while (i + k <= n && j + k <= n && s[i + k] == s[j + k]) k ++ ;
        height[rk[i]] = k;
    }
}
code(无注释)
const int N = 1e6 + 10;

int n, m;
char s[N];
int sa[N], x[N], y[N], c[N], rk[N], height[N];

void get_sa()
{
    for (int i = 1; i <= n; i ++ ) c[x[i] = s[i]] ++ ;
    for (int i = 2; i <= m; i ++ ) c[i] += c[i - 1];
    for (int i = n; i; i -- ) sa[c[x[i]] -- ] = i;
    for (int k = 1; k <= n; k <<= 1)
    {
        int num = 0;

        for (int i = n - k + 1; i <= n; i ++ ) y[ ++ num] = i;
        for (int i = 1; i <= n; i ++ )
            if (sa[i] > k) y[ ++ num] = sa[i] - k;  

        for (int i = 1; i <= m; i ++ ) c[i] = 0;
        for (int i = 1; i <= n; i ++ ) c[x[i]] ++ ;
        for (int i = 2; i <= m; i ++ ) c[i] += c[i - 1];
        for (int i = n; i; i -- )
            sa[c[x[y[i]]] -- ] = y[i], y[i] = 0;
        swap(x, y);

        x[sa[1]] = 1, num = 1;
        for (int 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;

        if (num == n) break;
        m = num; 
    }
}

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

Z函数(EXKMP)

  • 约定:字符串下标以 \(1\) 为起点。
  • 定义:对于个长度为 \(n\) 的字符串 \(s\)。定义函数 \(z[i]\) 表示 \(s\)\(s[i,n]\)(即以 \(s[i]\) 开头的后缀)的最长公共前缀(LCP)的长度。\(z\) 被称为 \(s\) 的 Z 函数。
code
void getZ()
{
    z[1] = n;
    for (int i = 2, l = 0, r = 0; i <= n; i ++ )
    {
        if (i <= r) z[i] = min(z[i - l + 1], r - i + 1);
        while (i + z[i] <= n && str[i + z[i]] == str[1 + z[i]]) z[i] ++ ;
        if (i + z[i] - 1 > r) l = i, r = i + z[i] - 1;
    }
}
posted @ 2023-03-24 10:32  kroyosh  阅读(35)  评论(0)    收藏  举报