codility上的问题之四 Gamma 2011

时间:2021-11-14 00:01:03

       这个题目比较难,给定一个全部由字母组成的字符串,长度为N,求其中长度大于1的回文子串的个数。其实它求的是下标对的个数 (x,y)满足y<y 并且x到y(包含)之间的字符串是回文的。

       输入范围:长度 [0..20000]

       输出: 长度大于1的回文子串个数,如果个数大于10^8,输出-1。


      要求复杂度: 时间复杂度 O(N),空间复杂度 O(N) 。

解答:

      其实我们可以套用最大回文子串的方法。 首先给字符串相邻字符之间加上一个特殊符号,它没有出现过,这样我们就统一了奇数长度和偶数长度的回文串,都变为奇数长度了。例如aba变为了#a#b#a#。

       一个回文串必然是以某个字符为中心的,那么我们定义p[i]表示以i为中心向“右”延伸的距离,使得[i - p[i]..i + p[i]] 这个子串是回文串。其实在变换后的字符串中,这个长度恰好就是原来回文串的长度。 那么如果我们能线性时间求出p这个数组,那么就可以了。求总的个数时,只要把p[i] / 2相加就可以了。例如,一个长度为5的回文串实际上包含了长度为5的和长度为3的回文串。

        那么如何线性时间求p呢? 当前求p[i],我们已经求出前面所有0..i - 1的p值了。对于某个x,我们定义,从子串[x - p[x], x + p[x]]是回文的。我们定义这样的东西叫做一个窗口。我们考虑尽可能靠右的窗口,假设它的中心是center, 子串[center - p[center], center + p[center]] 是回文的。 我们假设i <= center + p[center], 我们考虑它关于c的对称点i' = center * 2 - i。 因为对称性,i'向左的字符和i向右的字符是一样的,我们看一下p[i'], 我们试图把i也向右延伸,但是不能超过这个窗口的右边界。 也就是在这个窗口内部,i'和i向左和右延伸是一样的。那么我们可以让p[i] = min(p[i'], right - i)。当然超出有边界的部分,我们还是要不比较更新。这个算法复杂度是 O(N)的,因为right只增不减,每次比较,如果p[i] = p[i'],没有额外的开销,否则额外比较开销正好在更新right上,而right最多更新N次。这就是最大回文子串的O(N)算法。网上右很多画图讲解的,也非常清晰。但是我觉得,莫过于自己想清楚这个问题。只要在那个限定的窗口里,以i为中心的长度,可以用以i'为中心的长度所部分确定,只有超出部分才需要比较,这就是本算法的核心了。


代码:

    

// you can also use includes, for example:
// #include <algorithm>
#include <vector>

int solution(const string &S) {
// write your code here...
if (S.empty()) {
return 0;
}
string s = "#";
int i;
for (i = 0; i < S.length(); ++i) {
s += S[i];
s += "#";
}
int n = s.length();
int center = 0, right = 0,answer = 0;
vector<int> p;
p.resize(n);
for (i = 0; i < n; ++i) {
int ii = (center << 1) - i;
for (p[i] = (i < right)?min(p[ii], right - i):0; (i - p[i] - 1 >= 0) && (i + p[i] + 1 < n) && (s[i - p[i] - 1] == s[i + p[i] + 1]); ++p[i])
;
if ((answer += (p[i] >> 1)) >= 100000000) {
return -1;
}
if (i + p[i] > right) {
right = i + p[i];
center = i;
}
}
return answer;
}