本文地址:http://blog.csdn.net/freeelinux/article/details/53159929
Given a string S, you are allowed to convert it to a palindrome by adding characters in front of it. Find and return the shortest palindrome you can find by performing this transformation.
For example:
Given "aacecaaa"
, return "aaacecaaa"
.
Given "abcd"
, return "dcbabcd"
.
显然这是一个KMP算法的应用,在我上篇博客已经写过KMP算法了。在这里我们来分析一下:
如果某个字符串是回文的,那么s == reverse(s)。由此,我们可以将s逆转之后拼接在s后面,即ss = s + rev_s。该新字符串ss中首尾相同的部分,即
为s中以s[0]为起始地最长回文字串。
1.如: ss = abcd + dcba = abcddcba,显然在原字符串s中abcd最长回文子串长度就是a,(回文串就是正着读和倒着读都一样的串),所以我们需要给a前 面加上dcba,补齐其他不是回文串的字符,构成dcbabcd,那么dcb就可以通过后面的rev_s - 'a'(没有这种语法,只是为了说明)得到。然后再把dcb和原字符串s拼接即可。
2.再比如:s = aacecaaa,rev_s = aaacecaa,所以ss = aacecaaa + aaacecaa = aacecaaaaaacecaa,可得它的最长回文串是aacecaa,所以 rev_s - aacecaa(同上) = a,然后把a拼接在原字符串的前面即可。这可以通过KMP算法求得,可以参考我上篇博文。KMP算法记录了每个下标所对应的next值,
也就是每个下标前面的字符串前后缀完全相同的个数,同时也是下一步要比较元素的下标。
如图:
我们先忽略中部的#号,它的作用后面会说。
图中的next值由KMP算法即可求得,但是KMP算法求的是当前下标前面字符串相同的前后缀长度,比如下标16,next值为6,说明它前面字符串前后缀相同的最大长度是6,即图中的aaceca,不包括16自己。
那么问题来了,既然不包括自己,那我们怎么算新字符串前后缀的最长的长度呢?图中我们可以看出,结果应该为7,aacecaaa。
有这样一种思路,在新字符串后面加上#号,即添加17号元素,然后用KMP算法求17号元素next值,即可求得新字符串的前后缀相同的最长长度7,我们最后用rev_s的长度8-7=1,说明我们只需添加rev_s前部一个字符a,即可使原字符串构成一个回文串。
但是这种思路是有问题的!如果输入为aaaa呢,那么由我们的KMP算法可得,我们的新元素ss = s + revs = aaaaaaaa#, 按照KMP算法算得前后缀最大长度为7!注意,KMP算法所谓的前后缀长度是可以交叉的。因为它的判断条件if s[k] == s[j] ,next[++j] = ++k,所以只要前面的索引和后面索引对应的字符相等,那么最长前后缀长度即next值就会一直增加,所以得到7这个结果。
然而,rev_sz的长度仅为4,4-7=-3!我们后面用的substr函数在输入区间为负值的情况已,默认输出原函数。也就是说rev_sz.substr(0,-3)输出aaaa,在拼接上aaaa,就是aaaaaaaa了。虽然这仍是回文的,但是对于本来就是回文的字符串来说,做了不必要的运算。
针对aaaa问题,我们可以采取前面图中的方案,在两个字符串中间加个#,用来划分。
我的思路是这样的,先上代码:
class Solution {
public:
string shortestPalindrome(string s) {
string rev_sz = s;
reverse(rev_sz.begin(), rev_sz.end());
string ss = s + '#' + rev_sz;
vector<int> next = get_next(ss, ss.size());
return rev_sz.substr(0, rev_sz.size()-(next[ss.size()-1]+1)) + s;
}
private:
vector<int> get_next(const string& ss, const int length){
vector<int> next(length);
next[0] = -1;
int j = 0, k = -1;
while(j < length -1){
if(k == -1 || ss[k] == ss[j])
next[++j] = ++k;
else
k = next[k];
}
return next;
}
};
求next值不用说,但是由于是在中间加'#‘号,那么我们不能求得next[17]的next值,因为该元素不存在,当然你可以再加个’#'号,不过这就有点得不偿失了。我的方案是,用next[16]+1即可。上图代码已给出。
为什么呢?这是可以证明的。
//备注下面1这个证明不完整,可以跳到2
1.由于我数学水平不够,我采用半数学的证明方式。。。
例如如下图,字符串aacecaaa和它的reverse:
对任意字符串和它的逆串,如果有 ① == ②,①到③的长度和 ③到④的长度相同,且为逆串,可知② == ③,所以必有①==③,③==④。所以任意字符串和它的逆串符合该条件时,后面的③④位必然相等。
2.
对于任意字符串,逆序字符串的最后一位就是原来字符串的第一位,这是毫无疑问的。
从宏观来看,原字符串和逆序字符串只有两种情况:a代表首字符,b代表第二个字符,后面的情况忽略。
(1) s: ab rev_s: ba,这种情况ss即两字符串相加得到的字符串,它的最后一位的next值必为0,如所以下一次匹配,由于逆序,a必然匹配,所以next[++j] = ++k,也可以说是前面的k+1即可。在上面例子中,这个值即为next[17]的值,说明此情况next[17] = next[16] + 1。
(2) s: aa rev_s: aa,这种情况最后一位next值已经为1了,但同样由于逆序关系,最后一位必然匹配,所以next[++16] = next[17] = ++k = 2。
综上所述,一个字符串和它的逆序字符串匹配,假设字符串长度为len,那么必然有next[len] = next[len-1] + 1,得证 。
当然别人还有这样的思路,就是修改next[]数组的计算方法,next的计算方法改为计算当前包括自己在内的最长相同前后缀长度,代码是这样的:
class Solution {
public:
string shortestPalindrome(string s) {
string rev_s = s;
reverse(rev_s.begin(), rev_s.end());
string l = s + "#" + rev_s;
vector<int> p(l.size(), 0);
for (int i = 1; i < l.size(); i++) {
int j = p[i - 1];
while (j > 0 && l[i] != l[j])
j = p[j - 1];
p[i] = (j += l[i] == l[j]);
}
//std::cout<<p[l.size()-1]<<std::endl;
for_each(p.begin(), p.end(), [] (int i) {cout<<i<<' ';});
return rev_s.substr(0, s.size() - p[l.size() - 1]) + s;
}
};