后缀数组的应用

想要了解后缀数组,可以在网上搜索罗同学的论文,这里的代码更容易理解些。利用倍增算法对后缀数组按字典序排序并求这些数组的最长公共前缀是基本操作,就用途来说没有意义,一般都是利用这个结果进行其它操作,它在很多OJ题或面试题里的字符串操作中会用到,下面是一些例子:

1. 求一个字符串所有不同的子串个数(子串意味着是连续的)。比如“abaaba”,它的字串包括:a, b, aa, ab, ba, aba, baa,  aab, abaa, baab, aaba, abaab, baaba, abaaba。这个就可以用后缀数组的结果求解。

    首先,我们知道sa数组,里面是排好序的所有后缀,运用后缀的想法就把子串进行了第一次划分。

    然后,对于每一个后缀串,找出他们所有的前缀串,这就进行了第二次划分,所有的子串就出来了,假设一个后缀串长度为n,则它一共有n个前缀串。所有的后缀串的前缀个数相加,结果就出来了。这里面,因为有些后缀之间有公共前缀,公共前缀部分构成的串就重复了,要减掉的个数正好是最长公共前缀的长度,也就是上面网页里的height数组。所以,结果很容易就出来了。

	int total = 0;
	for (int i = 0; i < n; i++)
		total += n - i - height[sa[i]];
2. 求一个字符串中所有不同子串中,按字典序的第k个子串。

    这个也可以用后缀数组求,因为sa数组就是按字典序排列好的。对于某个字符串sa[i]来说,按字典序,它的所有前缀肯定比它本身要小的,去掉最长公共前缀部分,顺次往下找就行,跳一个字符就是一个排名。简略代码如下:

	int total = 0;
	int i, end;
	for (i = 0; i < n; i++)
	{
		int acc = n - sa[i] - height[i]; //acc就是后缀sa[i]所包含的除去重复子串后的子串个数
		if (total + acc >= k)            //可知要找的k肯定在后缀sa[i]里面。
		{
			end = k - total + height[i] - 1; //主串中下标从sa[i]到sa[i]+end 的部分就是要求的子串
			break;
		}
		total += acc;
	}

3. 求所有子串,和2类似。

4. 求可重叠最长重复子串。重复子串就是两个子串完全相同,这两个子串是可以有相交的部分的。最长的就是公共前缀值最大的,也就是height数组的最大值。

5.  求不可重叠最长重复子串。这里两个子串不能有相交部分。试想一下,这两个子串还是必须包含在两个后缀串里面,有公共的前缀,那才能相等啊。只是这两个后缀串在原字符串中的位置之间要有一定距离,这个距离要大于或等于二者公共前缀长度,二者才不会重复。代码如下:

	int maxlen = 0, beg=0;
	for (int i = 0; i < n; i++)
	{
		if ( height[i] > maxlen && abs(sa[i]-sa[i-1])>=height[i] ) //每次和最maxlen比较,小于等于的直接不考虑了
		{
			maxlen = height[i];
			beg = sa[i];
		}
	}
	int end = maxlen + beg;
	for (int j = beg; j < end; ++j)
		cout << ch[j] <<" ";
	cout << endl;
6.  给定一个字符串str,已知这个字符串是由某个字符串s重复r 次而得到的, 求r 的最大值。(pku2406)。首先,如果str里不存在题目中所要求的串,那整个str是一个s,此时r=1。设str长度为n, 串s的长度为len,len取值为[1,n],遍历枚举。要找到题目要求的串,必须满足:1. n%len=0,即要能整除;2. 串s在原串str中第一次出现处的后缀数组和第二次出现处的后缀数组...第r次出现处的后缀数组,最长公共前缀就是这个串s。这里我只进行第一次和第二次公共前缀判断。代码如下:
int r=0;
	for (int len = 1; len < n; ++len)
	{
		if (n%len == 0 && height[rank[len]]==len)
		{
			r = n / len;
		}
	}
	cout << r << endl;
这是有更简单,耗时少的解法,用KMP算法的next数组。对整个字符串str求next数组,如果有重复,str最后一个字符的next值就是前一个串s的结尾。串s的长就是n-next[n-1]。
 int len=n-next[n-1];
 if(n%len==0) 
      cout<<n/len<<endl;
 else 
      cout<<1<<endl;
后来我自己测试,上面的代码无论如何也通过不了。仔细想想以前自己对KMP算法的理解,上面的结果是得不出那个结论的,后来在网上重新看了下KMP算法,发现它的实现版本太多。有下面版本:
void getNext(char *ch, int len, int *next)
{
	int i = 0, k = -1;
	next[0] = -1;
	while (ch[i] != '\0')
	{
		if (k == -1 || ch[i] == ch[k])
		{
			++i; ++k;
			if (ch[i] == ch[k])
			{
				next[i] = next[k];
			}
			else {
				next[i] = k;
			}
		}
		else
			k = next[k];
	}
}

这个版本和严蔚敏老师教材上的基本一样,只是一个从-1开始,一个从0开始。它是经过优化之后 的,用这个版本解决我们这里的问题肯定是不行的。还有一个没优化版本,严老师的书上也有一个没优化的版本,这是可以解决我们上面问题的:

void makeNext(const char pattern[], int next[])
{
 int q, k;
 int m = strlen(pattern);
 next[0] = 0;
 for (q = 1, k = 0; q < m; ++q)
 {
  while (k > 0 && pattern[q] != pattern[k])
  k = next[k - 1];
  if (pattern[q] == pattern[k])
  {
    k++;
  }
  next[q] = k;
 }
}
注意这里next的含义与上面代码就不一样了。它是 长度为1到n的字符串pattern的前后缀的最大公共长度。比如"abaabaaba"第一字符a它没有前后缀,也就没有公共部分,所以next[0]为0。前二字符"ab",前缀为a后缀为b, 二者也没公共部分...所以,next[1]也为0。详细的解释看这里吧,又学习了一种新的解释KMP算法的方法,不得不说算法到处都是坑啊。像前面说的,这个是没优化的,优化的原因可以参考严老师教材,也可以看这里。还有一个现象:在未优化版本中,求next值的模式串是可以有重叠部分的,(优化后的版本没这种情况),所以,如果一个串str由m个s串重复组成,则最后一个字符的next值正好是串str除去第一个s串的长度,所以本题上面的解法就显而易见了。具体实现时下标0, -1, +1,-1什么的要注意。





你可能感兴趣的:(后缀数组的应用)