后缀数组 详解

字符串前缀:从字符串开头到字符串某个位置
字符串后缀:从字符串某个位置到字符串结尾
( 原串 和 空字符串 也是 前缀(后缀))

后缀数组:将所有后缀按字典序排序后,得到的数组

① 方法

如果我们直接将每个后缀字符串直接进行比较

复杂度为 O( n * n * logn )
代码如下:

string S;
int sa[105], rk[105];
bool cmp(int a, int b)
{
    return S.substr(a) < S.substr(b);
}
void construct_sa()
{
    for(int i = 0; i <= S.length(); i++)
    {
        sa[i] = i;
    }
    sort(sa, sa + S.length() + 1, cmp);
    rk[sa[0]] = 0;
    for(int i = 1; i <= S.length(); i++)
    {
        rk[sa[i]] = rk[sa[i-1]] + 1;
    }
}

字符串 S 为 abeacadabea
sa: 11 10 7 0 3 5 8 1 4 6 9 2
rk: 3 7 11 4 8 5 9 2 6 10 1 0

② 方法

字符之间的比较所消耗的复杂度为 O( n ),而数字之间的比较复杂度为 O( 1 ),如果我们能将字符之间比较转化为数字之间比较,是否可以降低复杂度呢

思路:我们用倍增思想,先比较长度为 2 的子串,再利用该结果比较长度为 4 的子串,再利用该结果比较长度为 8 的子串 … ( 长度为 1 的子串不用比较,因为自身的 ASCII 值就已经相当于比较了)
( 如果 剩余字符 不足 长度 2^k ,则表示到 字符串S的末尾 )
我们应该怎么利用上次比较的结果呢,比如长度为 k 的子串我们已经比较好了,那么长度为 2 * k 的子串,我们就只用看 i 开头长度为 k 的子串排序序号,和 i + k 开头长度为 k 的子串排序序号,然后比较序号之间就行,这样就把字符串之间的比较,转换成了数字比较

复杂度为 O( n * log n * log n )
代码如下:

string S;
int sa[105], rk[105], tmp[105];
int k;
bool cmp(int a, int b)
{
    if(rk[a] == rk[b])
    {
        int i = a + k <= S.length() ? rk[a + k] : -1;
        int j = b + k <= S.length() ? rk[b + k] : -1;
        return i < j;
    }
    return rk[a] < rk[b];
}
void construct_sa()
{
    for(int i = 0; i <= S.length(); i++)
    {
        sa[i] = i;
        rk[i] = i < S.length() ? S[i] - 'a' + 1 : 0;
    }
    for(int i = 1; i <= S.length(); i = i * 2)
    {
        k = i;
        sort(sa, sa + S.length() + 1, cmp);
        tmp[sa[0]] = 0;
        for(int i = 1; i <= S.length(); i++)
        {
            tmp[sa[i]] = tmp[sa[i-1]] + (cmp(sa[i-1], sa[i]) ? 1 : 0);
        }
        for(int i = 0; i <= S.length(); i++)
        {
            rk[i] = tmp[i];
        }
    }

}

字符串 S 为 abeacadabea
sa: 11 10 7 0 3 5 8 1 4 6 9 2
rk: 3 7 11 4 8 5 9 2 6 10 1 0

感觉注释写代码里看不清(注释颜色太浅),我就在这解释下:

  1. tmp 是一个临时的数组,因为如果我们直接更新 rk 结果数组,这样我们在判断长度为 k 的字符子串时,可能会有已经更新过的 长度为 2k 的字符子串混入,会影响判断结果
  2. 我们对于每次的排序都需要再处理一次,因为当两个字符子串大小相同时,我们需要让排序序号相同,不然会影响下次的判断

③ 方法

我们是否可以用字符串匹配的思想,把每个后缀列用数字表达出来,然后排个序
例:字符串 S 为 abeacadabea 时,部分后缀为:
11个空字符
a + 10 个空字符
ea + 9 个空字符
bea + 8 个空字符
abea + 7 个空字符

如果给每种字符编上号,例如 空字符 等于 0,a 等于 1,b 等于 2 …
那我们只用比较这几个数的大小就可以了( 当最大种数小于 10 的时候 )
00000000000
10000000000
51000000000
25100000000
12510000000

但这种十进制的表达只有字符最大种数小于等于 10 的情况才能用,那如果情况不为 10 种的情况,我们可以将 十进制 改为 n进制 就行,这样每一位仍然可以表达字符是什么
但是这个很不靠谱,因为我们比较的是大小,所以我们不能取模,这就导致很容易就出现一个巨大无比的数,很容易就无法判断了

复杂度为 O( n * logn ),但也就数据很小很小的时候可以用用,大多数没用
代码如下:

const ll B = 10;
string S;
pair<ll, int> sa[105];
int rk[105];
int k;
void construct_sa()
{
    ll T = 1;
    for(int i = 0; i < S.length(); i++)
    {
        T = T * B;
    }
    ll t = 0;
    sa[S.length()] = make_pair(t, S.length());
    for(int i = S.length() - 1; i >= 0; i--)
    {
        t = t / B + (S[i] - 'a' + 1) * T;
        sa[i] = make_pair(t, i);
    }
    sort(sa, sa + S.length() + 1);
    rk[sa[0].second] = 0;
    for(int i = 1; i <= S.length(); i++)
    {
        rk[sa[i].second] = rk[sa[i-1].second] + 1;
    }
}

字符串 S 为 abeacadabea
sa: 11 10 7 0 3 5 8 1 4 6 9 2
rk: 3 7 11 4 8 5 9 2 6 10 1 0

后缀数组的用处

例如可以应用于字符串匹配 ( 假设已经算好了 字符串S 的后缀数组 )
在 字符串S 中找 字符串T ,字符串S 的长度为 n,字符串T 的长度为 m,则复杂度为 O( m log n )
如果用 Rabin-Karp算法写的话复杂度为 O( n + m )(上篇文章写了)
在 n 较大的时候,后缀数组的优势更大,所以当我们对同一个 字符串S 找多个不同字符串 Ti 时,我们可以用这个后缀数组处理

bool contain(string S, string T)
{
    int l = 0, r = S.length(), mid = (l + r) / 2;
    while(l < r - 1)
    {
        if(S.compare(sa[mid], T.length(), T) == -1)
        {
            l = mid;
            mid = (l + r) / 2;
        }
        else
        {
            r = mid;
            mid = (l + r) / 2;
        }
    }
    return S.compare(sa[r], T.length(), T) == 0;
}

注释放下面:

  1. compare函数,比较两个字符串大小,若相同返回 0,若小于返回 -1,若大于返回 1
    如果只有一个参数,则两个字符串直接比较 s1.compare(s2)
    若有三个参数,则比较 s1 从 下标a 开始 b个字符 组成的子字符串和 s2 比较 s1.compare(a,b,s2)
    若有五个参数,则比较 s1 从下标a 开始 b个字符 组成的子字符串和 s2 从下标c 开始 d个字符组成的子字符串 s1.compare(a,b,s2,c,d)
  2. 这个二分是 左开右闭 ( l , r ] ,因为不可能是 0,那是空串
  • 9
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
KMP算法是一种字符串匹配算法,用于在一个文本串S内查找一个模式串P的出现位置。其中,KMP算法的关键在于求解模式串P的next数组。下面详细介绍next数组的含义、求解方法以及应用。 1. 含义 next数组是一个长度为m(m为模式串P的长度)的数组,其中next[i]表示P[0:i]这个子串中,最长的既是其前缀又是其后缀的字符串的长度。特别地,next[0]=-1,next[1]=0。例如,当P="abab"时,其next数组为[-1,0,0,1]。 2. 求解 next数组的求解可以通过动态规划的方式实现。具体来说,在求解next[i]时,假设已知next[0:i-1]的值,我们需要找到一个最长的既是P[0:i-1]的前缀,也是P[1:i]的后缀的字符串。这个字符串可以通过比较P[0:j-1]和P[i-j:i-1]来得到,其中j=next[i-1]+1。 如果P[j]==P[i],那么next[i]=j;否则,我们需要找到一个更短的字符串。此时,我们可以利用next数组的性质,从next[j]开始向前查找,直到找到一个P[k]等于P[i]为止,然后令next[i]=k。如果一直找到k=-1还没有找到,那么next[i]=0。 3. 应用 有了next数组之后,我们就可以利用KMP算法在文本串S中查找模式串P的出现位置。具体来说,我们维护两个指针i和j,分别指向S和P的当前位置。如果P[j]==S[i],那么i和j都向后移动一位;否则,我们利用next数组来决定j的下一步移动位置。具体来说,如果next[j]=-1,或者next[j]<i,则令j=0,i不变;否则,令j=next[j]。这样,我们可以在O(n+m)的时间复杂度内完成匹配。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值