后缀数组

本文深入探讨了后缀数组的基本概念与性质,详细解析了LCP(最长公共前缀)的计算方法,包括其关键性质和公式推导。通过具体代码实现,展示了如何构建后缀数组并计算任意两个后缀的LCP,适用于字符串处理和算法竞赛。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

后缀数组模板


*以下部分代码和部分解释均来自于白书。 *
关于后缀数组的几个性质:

  1. LCP(i,j)=LCP(j,i)LCP(i, j) = LCP(j, i)LCP(i,j)=LCP(j,i)
  2. LCP(i,k)=min(LCP(i,j),LCP(j,k))LCP(i, k) = min(LCP(i, j), LCP(j, k))LCP(i,k)=min(LCP(i,j),LCP(j,k))0≤i≤j≤k&lt;n0 \leq i \leq j \leq k &lt; n0ijk<n
    简单证明:
    LCP(i,j)=p,LCP(j,k)=qLCP(i,j)=p, LCP(j,k)=qLCP(i,j)=p,LCP(j,k)=qposi=sa[i],posj=sa[j],posk=sa[k]pos_i=sa[i],pos_j=sa[j],pos_k=sa[k]posi=sa[i],posj=sa[j],posk=sa[k]
    那么,一定存在 s[posi∼posi+p−1]=s[posj∼posj+p−1]s[pos_i \sim pos_i+p-1]=s[pos_j \sim pos_j+p-1]s[posiposi+p1]=s[posjposj+p1]s[posj∼posj+q−1]=s[posk∼posk+q−1]s[pos_j \sim pos_j+q-1]=s[pos_k \sim pos_k+q-1]s[posjposj+q1]=s[poskposk+q1]
    假设 LCP(i,k)=x&lt;min(p,q)LCP(i,k)=x &lt; min(p,q)LCP(i,k)=x<min(p,q),存在 s[posi+x]≠s[posk+x]s[pos_i+x] \neq s[pos_k+x]s[posi+x]̸=s[posk+x]
    又因为 p,qp,qp,q 均大与 xxx,所以有 s[posi+x]=s[posj+x]s[pos_i+x] = s[pos_j+x]s[posi+x]=s[posj+x]s[posj+x]=s[posk+x]s[pos_j+x]=s[pos_k+x]s[posj+x]=s[posk+x],推出 s[posi+x]=s[posk+x]s[pos_i+x]=s[pos_k+x]s[posi+x]=s[posk+x]。与假设矛盾,所以 LCP(i,k)≥min(p,q)LCP(i,k) \geq min(p,q)LCP(i,k)min(p,q)
    同理可证明出 LCP(i,k)≤min(p,q)LCP(i,k) \leq min(p,q)LCP(i,k)min(p,q),所以 LCP(i,k)=min(p,q)LCP(i,k) = min(p,q)LCP(i,k)=min(p,q)
  3. LCP(i,j)=min{LCP(k,k+1)}LCP(i, j) = min \{ LCP(k, k + 1) \}LCP(i,j)=min{LCP(k,k+1)}i≤k&lt;ji \leq k &lt; jik<j
    可以通过性质 222 推出。
  4. height[rank[i]]≥height[rank[i−1]]−1height[rank[i]] \geq height[rank[i - 1]] - 1height[rank[i]]height[rank[i1]]1
    可以利用这个性质推出任意两个后缀的 LCPLCPLCP

ps: LCP(i,j)LCP(i, j)LCP(i,j) 表示 sa[i]sa[i]sa[i]sa[j]sa[j]sa[j] 两个后缀的最长公共前缀,即排名为 iii 和排名为 jjj 的两个后缀的最长公共前缀。height[i]=LCP(sa[i],sa[i−1])height[i]=LCP(sa[i], sa[i-1])height[i]=LCP(sa[i],sa[i1])

变量意义解释:
sa[i]sa[i]sa[i]:排名为 iii 的后缀在原字符串的位置
rnk[i]rnk[i]rnk[i]:第 iii 个后缀的排名,与 sasasa 数组意义相反
tmp[i]tmp[i]tmp[i]:辅助数组
height[i]height[i]height[i]:表示 sa[i]sa[i]sa[i]sa[i−1]sa[i-1]sa[i1] 两个后缀的 LCPLCPLCP
c[]c[]c[]:基数排序用的
nnn:字符串的长度
mmm:每次需要比较的最大值的大小

代码实现:
pss: 任意位置后缀的 LCPLCPLCP 可以使用 STSTST 表来求,代码中只是根据性质 444 计算了任意位置的 LCPLCPLCP

const int maxn = 1e5 + 5;
string s;

int sa[maxn], rnk[maxn], tmp[maxn], c[maxn], height[maxn];
int n, m;

void build_sa()
{
    m = 128; // 初始时是以字符来比较大小的
    for(int i = 0; i < m; i++) c[i] = 0;
    // 第一次基数排序
    for(int i = 0; i < n; i++) c[rnk[i] = s[i]]++;
    for(int i = 1; i < m; i++) c[i] += c[i - 1];
    for(int i = n - 1; i >= 0; i--) sa[--c[rnk[i]]] = i;
    // 倍增计算
    for(int k = 1; k <= n; k <<= 1){
        int p = 0;
        // 利用sa数组对第二关键词进行排序
        // 第i个后缀的第二关键词就是 位置为i+k,长度为k的串
        // 这里的tmp数组和sa数组意义一样,代表的是第二关键词
        // [n - k, n) 这些位置的后缀没有第二关键词,所以第二关键词为0,此时它们的第二关键词排名是最小的
        for(int i = n - k; i < n; i++) tmp[p++] = i;
        // [0, n - k) 这些位置的后缀有第二关键词,因此可以直接根据sa数组得到第二关键词排名
        // tmp[p++] = sa[i] - k 表示在sa[i]-k位置的后缀的第二关键词的排名为p
        for(int i = 0; i < n; i++) if(sa[i] >= k) tmp[p++] = sa[i] - k;

        // 利用基数排序对第一关键词进行排序
        // 上一次迭代得到的rnk就是当前的第一关键词,rnk[tmp[i]]表示第二关键词排名为i的第一关键词
        for(int i = 0; i < m; i++) c[i] = 0;
        for(int i = 0; i < n; i++) c[rnk[tmp[i]]]++;
        for(int i = 1; i < m; i++) c[i] += c[i - 1];
        for(int i = n - 1; i >= 0; i--) sa[--c[rnk[tmp[i]]]] = tmp[i]; // 记住这里不是等于i

        // 以下是重新计算rnk数组
        swap(rnk, tmp);
        rnk[sa[0]] = 0;
        p = 1;
        // 如果排名为i和i-1的两个串的第一关键词和第二关键词相同,则它们的排名相同;否则,排名增加1
        for(int i = 1; i < n; i++)
            rnk[sa[i]] = (tmp[sa[i]] == tmp[sa[i - 1]] && tmp[sa[i] + k] == tmp[sa[i - 1] + k]) ? p - 1 : p++;
        // 如果没有两个后缀的排名相同,则排序完成
        if(p >= n) break;
        // 下一次迭代需要比较的最大值
        m = p;
    }
}

void get_height()
{
    for(int i = 0, k = 0; i < n; i++){
        if(k) --k; // 性质4在这个体现
        if(rnk[i] == 0) continue;
        int j = sa[rnk[i] - 1];
        while(i + k < n && j + k < n && s[i + k] == s[j + k]) ++k;
        height[rnk[i]] = k;
    }
}

// 计算位置i和j的两个后缀的lcp
int lcp(int i, int j)
{
    if(rnk[i] > rnk[j]) swap(i, j);
    int ret = 0;
    if(i == j) ret = s.length() - i;
    else{
        ret = INT_MAX;
        for(int k = rnk[i] + 1; k <= rnk[j]; k++)
            ret = min(ret, height[k]);
    }
    return ret;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值