后缀数组

36 篇文章 0 订阅
3 篇文章 0 订阅

问题 : 求一个字符串的不可重叠最长重复子串


二分答案 + hash 的思路是比较显然的 , 时间复杂度 O(N(logN)2) , 然而这个算法有可能超时。


字符串的每个子串都是后缀的前缀 , 所以后缀蕴含着很大的信息量

这个时候后缀数组就可以派上用场了~


sa[i] 为排名为 i 的后缀的起始位置 , rank[i] 为以 i 为起始位置的后缀的排名

rank sa 满足这样的关系 : rank[sa[i]]=i , sa[rank[i]]=i

求出 sa[] 之后, 可以直接求出 rank[]

首先我们对字符串 S 的后缀排序

直接排序时间复杂度是 O(N2logN), 显然不能满足需要


利用后缀的性质,我们可以利用倍增的思想排序

假设我们的得到了只考虑后缀最多前 k 位时的排名

那么以 rank[i] 为第一关键字, rank[i+k] 为第二关键字排序
就可以得到考虑后缀最多前 2k 位时的排名

(为了使得排序具有稳定的效率 , 一般采用基数排序)

代码实现有一定技巧

时间复杂度: O(NlogN)


但是只有 sa[] , rank[] 还不够 , 我们还想求出

height[i] : 排名为 i 的后缀与排名为 i1 的后缀的最长公共前缀(LCP)

可以证明 , height[rank[i]]height[rank[i1]]1

时间复杂度: O(N)


证明

定义函数 LCP(x , y) 表示 sa[x ~ y] 的后缀的 LCP
LCP(x , y) = min{LCP(x + t, x + t + 1)} (0 <= t < y - x)

rank[k] = rank[i - 1] - 1

LCP(rank[k], rank[i - 1]) = height[rank[i - 1]]

LCP(rank[k + 1], rank[i]) >= LCP(rank[k], rank[i - 1]) - 1 = height[rank[i - 1]] - 1

此时如果 rank[k + 1] = rank[i] - 1, 结论显然成立

如果 rank[k + 1] <> rank[i] - 1 , 即 rank[k + 1] < rank[i] - 1

显然有
LCP(rank[i] - 1, rank[i])
>= LCP(rank[k + 1], rank[i])
>= height[rank[i - 1]] - 1

结论成立


代码解释

n : 字符串的长度
m : 排名的最大值

c[i] : 关键字的值小于或等于 i 的元素个数
x[i] : 以 i 为起始位置的后缀的排名
y[i] : (按第二关键字排序) 排名为 i 的后缀的起始位置


//Suffix Array

const int maxn = 1e5 + 50;

char s[maxn];
int sa[maxn], t0[maxn], t1[maxn], c[maxn], n;
int rank[maxn], height[maxn];

void build_SA(int m)
{
    int *x = t0, *y = t1;

    for(int i = 0; i < m; i++) c[i] = 0;
    for(int i = 0; i < n; i++) c[x[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[x[i]]] = i;

    for(int k = 1; k <= n; k <<= 1)
    {
        int p = 0;

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

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

        std::swap(x, y);

        p = 1, x[sa[0]] = 0;

        for(int i = 1; i < n; i++)
            x[sa[i]] = y[sa[i-1]] == y[sa[i]] && sa[i-1] + k < n && sa[i] + k < n && y[sa[i-1]+k] == y[sa[i]+k] ? p-1 : p++;
        if(p == n) break;
        m = p;  
    }
}
void build_Height()
{   
    int k = 0;

    for(int i = 0; i < n; i++) rank[sa[i]] = i;
    for(int i = 0; i < n; i++)
    {
        if(k != 0) k--;

        if(!rank[i]) continue;

        int j = sa[rank[i] - 1];

        while(s[i + k] == s[j + k]) k++;
        height[rank[i]] = k;
    }
}

模板提交链接 : uoj #35


回到最开始的问题 , 如何求一个字符串的不可重叠最长重复子串?

先二分答案,把题目变成判定性问题 : 判断是否存在两个长度为 k 的子串, 它们是相同的 , 且不重叠

如果 height[i]k, 我们认为 sa[i] sa[i1] 在同一个集合内

(同一个集合中的点对 (x,y) 都满足 LCP(x,y)k )

然后判断是否存在点对 (x,y) , sa[x] sa[y] 在同一个集合内 且 |sa[x]sa[y]|k

我们只需要对 sa[] 求区间最值即可 , 时间复杂度为 O(N)

总时间复杂度: O(NlogN)


提交链接 : poj 1743

题意 : 将长度为 n 的序列做差分 , 得到一个长度为 n1 的新序列 , 求新序列的最长不重叠重复子串

记最长不重叠重复子串长度为 P , 如果 P+1 5 , 直接输出 0 , 否则输出 P+1


// poj 1743

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <ctime>
#include <iostream>
#include <algorithm>

template<class Num>void read(Num &x)
{
    char c;
    int flag = 1;

    while((c = getchar()) < '0' || c > '9')
        if(c == '-') flag *= -1;
    x = c - '0';
    while((c = getchar()) >= '0' && c <= '9')
        x *= 10, x += c - '0';
    x *= flag;
}
template<class Num>void write(Num x)
{
    if(!x) {putchar('0'); return;}

    static char s[20];
    int sl = 0;

    if(x < 0) putchar('-'), x = -x;

    while(x) s[sl++] = x%10 + '0', x /= 10;
    while(sl) putchar(s[--sl]);
}

const int maxn = 2e4 + 20, base = 100;

int n, s[maxn];
int sa[maxn], rank[maxn], t0[maxn], t1[maxn];
int height[maxn], c[maxn];

void build_sa(int m)
{
    int *x = t0, *y = t1;

    for(int i = 1; i <= m; i++) c[i] = 0;
    for(int i = 1; i <= n; i++) c[x[i] = s[i]]++;
    for(int i = 1; i <= m; i++) c[i] += c[i - 1];
    for(int i = n; i >= 1; i--) sa[c[x[i]]--] = i;

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

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

        std::swap(x, y);
        p = 1, x[sa[1]] = 1;
        for(int i = 2; i <= n; i++)
            x[sa[i]] = y[sa[i]] == y[sa[i - 1]] && sa[i] + k <= n && sa[i - 1] + k <= n && y[sa[i] + k] == y[sa[i - 1] + k] ? p : ++p;
        if(p == n) break;
        m = p;
    }
}
void build_height()
{
    int k = 0;

    for(int i = 1; i <= n; i++) rank[sa[i]] = i;
    for(int i = 1; i <= n; i++)
    {
        if(k) k--;

        if(rank[i] == 1) continue;

        int j = sa[rank[i] - 1];

        while(s[j + k] == s[i + k]) k++;

        height[rank[i]] = k;
    }
}
bool check(int k)
{   
    int e;

    for(int i = 1; i <= n; i = e)
    {
        int min = sa[i], max = sa[i];

        e = i + 1;

        while(e <= n && height[e] >= k)
        {
            min = std::min(sa[e], min);
            max = std::max(sa[e], max);
            e++;
        }
        if(max - min >= k) return true;
    }
    return false;
}
int solve()
{
    int l = 4, r = n;

    if(!check(l)) return -1;
    if(check(r)) return r;

    while(l + 1 != r)
    {
        int mid = (l + r) >> 1;

        if(check(mid)) l = mid;
        else r = mid;
    }

    return l;
}
void init()
{
    for(int i = 1; i <= n; i++) read(s[i]);
    for(int i = 1; i < n; i++)
        s[i] = s[i + 1] - s[i] + base;  
    s[n --] = 0;        
}
int main()
{
#ifndef ONLINE_JUDGE
    freopen("1743.in","r",stdin);
    freopen("1743.out","w",stdout);
#endif

    while(true)
    {
        read(n);
        if(!n) break;

        init();

        build_sa(256);
        build_height();

        write(solve() + 1), puts("");
    }

#ifndef ONLINE_JUDGE
    fclose(stdin);
    fclose(stdout);
#endif
    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值