今天很高兴自己能在后缀数组上有很大的突破;原因是根据自己对后缀数组的理解写出了完全属于自已的代码,可能代码没后缀数组的模板简洁,但个人觉得对于刚接触
后缀数组的acmer们会有更好的理解,看懂了我的代码绝对比模板要简单。因为个人始终不太看好记算法模板;
一 首先解释后缀数组的算法思想,个人语言表达能力不算太好,别批我哈
众所周知,后缀数组处理完某个字符串s后,会得到至少两个重要数据:(后面还能得到其他重要数据,后面再讲哈,呵呵)
1,sa数组,也就是名次列表,sa【i】通俗解释是:s的所有后缀按字典升序,排名为i的后缀首字母在字符串s中的下标; 例如:s = “abcd” ,那么sa【0】肯定是0,因为后缀”abcd“实际上是最小的后缀,首字母“a”在s中的下标是0;
2,rank数组,也就是成绩单,通俗解释是:s的所有后缀按字典升序,首字母在字符串s中的下标为i的后缀在所有后缀中的排名;例如:s = “abcd” ,那么rank【0】肯定是0,因为首字母下标为0的后缀”abcd“实际上是最小的后缀,排名为0;
如何更好的理解这两个结果数组呢,看过别人的论文,我觉得一句话就说得很清楚了,sa说得是"排名i的是谁",rank,说得是“你排第几名”;
二 下面就解析一下后缀数组究竟为什么省事省时的灵魂算法: 倍增算法
想想这样一个问题,我们比较各个后缀的大小的时候,任意找两个后缀,然后从第一个字母依次往后比较,每个人都能想到同样下标的两个字母无数次得重复比较;
比如s=“aaabbb”,后缀“aaabbb” 跟后缀“aabbb”比较的时候,在s中下标 为 0 跟 1 的比较, 1 跟 2 的比较 ,又有后缀“aabbb”跟“abbb”在s中下标 为 1 跟 2 又会进行比较;
如何避免这种重复就是倍增算法要解决的问题了
1——————发现问题
假如我们把首字母在s中的下标为i后缀表示为a【i】,如果a【i】跟a【j】进行比较,在比较到第k个字符的时候,也就是s【i + k】跟s【j + k】进行比较,是否存在后缀a【i+k】,a【j + k】也需要比较这两个字符呢? 那么我们就可以利用这个性质来进行我们的倍增算法设计了
2——————分析问题
那么我们可以在a【i】跟a【j】比较的时候利用a【i+k】跟a【j + k】的比较结果
3——————找到方法
(1)我们先让所有的长度l = 1 的s子串进行比较,得到sa跟rank,这是很容易实现的;
然后l依次倍增
(2)当l = 2的时候,a【i】跟a【j】进行比较,在(1)中我们已经得到了s【i】跟s【j】的大小关系,s【i + 1】跟s【j+ 1】的大小关系也找到了,那么利用这两个不就刚好可以得到长度l = 2的大小比较关系吗
(3)依次l = 4的时候我们有s【i ……i+1】跟s【j……j+1】和s【i + 2……i+3】跟s【j + 2 …… j + 3】,刚好可以得到s【i……i+3】跟s【j……j+3】的大小关系
…………………………‘
(n)当l >strlen(s)的时候,所有的后缀大小关系已经出来了
很重要得有一点需要提醒,我们每次把s末尾加上一个比s中所有字符都要小的值,这是能保证所有的后缀能在不超出范围的时候已经比较出了结果,简洁了我们的代码
三 下面给出我的代码,进一步分析;
int cmp1(int a, int b) // 第一次排序只需要字符间比较久可以了
{
return str[a] < str[b];
}
int init() //长度为1的时候直接算出sa跟rank数组,注意因为可能有并列的排名,所以要在rank中表现出并列;
//rank数组是滚动数组,算下一个rank的时候只要让flag = 1 - flag; flag就能在0 1 之间变换了,因为计算这次rank的时候需要上次的结果
{
int i;
flag = 0;
int index = 0;
for(i = 0; i < len; i ++)
{
sa[i] = i;
}
sort(sa, sa + len, cmp1);
for(i = 0; i < len; i ++)
{
rank[flag][sa[i]] = index;
if(i + 1 < len && str[sa[i]] != str[sa[i + 1]])
index ++;
}
return 0;
}
int getrank(int f) // 利用sa数组求rank数组,注意并列排名,所以我用个index来记录排名,当两个串不一样的时候index才会改变
{
int index = 0, i;
for(i = 0; i < len; i ++)
{
rank[f][sa[i]] = index;
if(rank[1 - f][sa[i]] < rank[1 - f][sa[i + 1]] || (rank[1 - f][sa[i]] == rank[1 - f][sa[i + 1]] && rank[1 - f][sa[i] + k / 2] < rank[1 - f][sa[i + 1] + k / 2]))
{
index ++;
}
}
return 0;
}
int cmp(int a, int b) //利用两个长度为 k / 2的子串结果得到长度为k的结果
{
if(rank[flag][a] != rank[flag][b])
{
return rank[flag][a] < rank[flag][b];
}
if(rank[flag][a + k / 2] != rank[flag][b + k / 2])
return rank[flag][a + k / 2] < rank[flag][b + k / 2];
return 0;
}
int getSa()
{
init();
for(k = 2; k <= len; k *= 2) //k为后缀长度,全局变量
{
sort(sa, sa + len, cmp); //给排名进行再次排序,因为后缀长度加长的时候很多之前并没有分出胜负的还是需要利用上次结果再次比较的
getrank(1 - flag); //获取到这次的rank数组
flag = 1 - flag; //rank数组滚动,避免数据掩盖
}
return 0;
}
至此,sa数组跟rank数组都能得到了,看看这个算法,也就在init的时候字符间才会进行比较,长度l加长之后再也没有字符间的比较了,是不是减少了很多重复的比较
这也就是倍增算法省时的根本原因了
给个经典题目poj2774 两个100000长度字符串的最长公共连续子串
我的代码:
#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<iostream>
using namespace std;
#define MAX 300001
char str[MAX];
int len;
int len1;
int rank[2][MAX], sa[MAX], height[MAX];
int k, flag;
int cmp(int a, int b)
{
if(rank[flag][a] != rank[flag][b])
{
return rank[flag][a] < rank[flag][b];
}
if(rank[flag][a + k / 2] != rank[flag][b + k / 2])
return rank[flag][a + k / 2] < rank[flag][b + k / 2];
return 0;
}
int getrank(int f)
{
int index = 0, i;
for(i = 0; i < len; i ++)
{
rank[f][sa[i]] = index;
if(rank[1 - f][sa[i]] < rank[1 - f][sa[i + 1]] || (rank[1 - f][sa[i]] == rank[1 - f][sa[i + 1]] && rank[1 - f][sa[i] + k / 2] < rank[1 - f][sa[i + 1] + k / 2]))
{
index ++;
}
}
return 0;
}
int cmp1(int a, int b)
{
return str[a] < str[b];
}
int init()
{
int i;
flag = 0;
int index = 0;
for(i = 0; i < len; i ++)
{
sa[i] = i;
}
sort(sa, sa + len, cmp1);
for(i = 0; i < len; i ++)
{
rank[flag][sa[i]] = index;
if(i + 1 < len && str[sa[i]] != str[sa[i + 1]])
index ++;
}
return 0;
}
int getHeight()
{
int i;
int z = 0;
for(i = 0; i < len; i ++)
{
if(rank[flag][i] == len - 1)
continue;
for(z <= 0 ? (z = 0) : (z --); str[i + z] == str[sa[rank[flag][i] + 1] + z]; z ++);
height[rank[flag][i]] = z;
}
return 0;
}
int getSa()
{
init();
for(k = 2; k <= len; k *= 2)
{
sort(sa, sa + len, cmp);
getrank(1 - flag);
flag = 1 - flag;
}
return 0;
}
int Max(int a, int b)
{
return a > b ? a : b;
}
int getresult()
{
int i;
int max = 0;
int f;
if(sa[1] < len1)
f = 1;
else
f = 2;
for(i = 1; i < len - 1; i ++)
{
if(sa[i + 1] >= len1 && sa[i] < len1)
{
max = Max(max, height[i]);
}
else if(sa[i + 1] < len1 && sa[i] >= len1)
max = Max(max, height[i]);
}
return max;
}
int main()
{
while(scanf("%s", str) != EOF)
{
len1 = len = strlen(str);
scanf("%s", str + len);
len = strlen(str);
str[len ++] = 1;
getSa();
getHeight();
printf("%d\n", getresult());
}
return 0;
}
四:height 数组,这就是后缀数组算法得到的解决问题的利器,表示相邻排名的两个后缀的最长前缀
他有个性质:
h[i] = height[rank[i]],h[i] >= h[i - 1] - 1。h[i]表示后缀数组suffix(i)与前面相邻后缀数组的最长公共字符串
也就是后缀a【i】,跟他排名相邻的后缀a【j】,那么与a【i+1】排名相邻的后缀一定是a【j+1】,其实可以反证法证明的,也其实一眼能看出来的,实在不想写证明过程,累啊啊啊
那么height【i + 1】最多比height【i】少1,因为a【i】就只是比a【i + 1】多最前面一个字符,a【j】也比a【j + 1】多最前面的一个字符,没有这个字符,后面的不都一样么?自己多想想吧
这就是我自己的理解,别的论文都是各种证明啊,头大啊,痛啊,各位兄弟们,你们不想头痛吧,那就多想想,别看那些乱七八糟的证明, 式子等等了
我的分析就此为止了,实在哪儿有没叙述清楚的,可以给我留言,尽快给你们回复。。。。。。