给定一个字符串 s,你可以通过在字符串前面添加字符将其转换为回文串。找到并返回可以用这种方式转换的最短回文串。
示例 1:
输入: “aacecaaa”
输出: “aaacecaaa”
示例 2:
输入: “abcd”
输出: “dcbabcd”
又到了每周困难一题的时间,不过这周的还好吧。
首先考虑,如果我知道答案的长度,构造答案的字符串是非常简单的。只需要从s中倒着往一个StringBuilder bulider里头加字母,直到sbuilder和s的长度之和是需要的长度,再拼接起来即可。接下来的问题是答案的长度如何获得。可以从一半开始,尝试用不同的字母(或者字母间隙)为轴,从中间往左边依次尝试。至于判断能否为轴…先来个暴力的吧:
class Solution {
private boolean isValid(String s, int l, int r) {
while(l >= 0) {
if(s.charAt(l) != s.charAt(r))
return false;
--l;
++r;
}
return true;
}
public String shortestPalindrome(String s) {
//先来个简单一点的?
if(s.length() % 2 == 1 && isValid(s, s.length() / 2 - 1, s.length() / 2 + 1))
return s;
int pov = s.length() / 2 - 1;
int length = -1;
while(true) {
if(isValid(s, pov, pov + 1)) {
length = 2 * (s.length() - pov - 1);
break;
}
if(isValid(s, pov - 1, pov + 1)) {
length = (s.length() - pov) * 2 - 1;
break;
}
--pov;
}
length = length - s.length();
StringBuilder builder = new StringBuilder(s.substring(s.length() - length));
builder.reverse();
return builder.toString() + s;
}
}
实现上比较值得在意的是字符串长度为奇数和偶数的时候第一个轴的选择有所不懂。如果字符串长度为奇数,比如abc,需要先考虑b,再考虑ab间隙,再往左考虑。如果是偶数abcd,要先考虑bc间隙,再考虑b,再往前考虑。实现中先对长度为奇数的情况判断了一次能否以正中的字母为轴,即可保持奇偶的迭代过程一致。
387ms,过了。看看题解,嗯,提到了Rabin-Karp哈希算法。似乎能够用来给我的算法进行优化,想着实现起来不会很困难但是整的实现还是有很多蛋疼的地方。
首先,由于这里需要对轴两边的字符串反着进行匹配,左右在计算哈希值的时候需要反着来。这本来没什么,但是这里利用已有的哈希值计算新的哈希值时,左边和右边的字符串是需要从不同的方向删除字符的。以12345678为例。计算了1234和8765的哈希值,下一个需要用到是123和765的哈希值。如果定义两边为高位的话,8765变成765只需要减去8 * 1000再对mod取余即可,但是1234变成123就不知道怎么做了。没有mod的事的话自然只需要除以10取整,但是取整运算在取模意义下是等价的吗…问了问度娘,她只说
没说除法成立…所以我还是想别的辙吧…最后想到,需要计算哈希值的左边子串实际上是递增的,以上头为例分别是1 12 123 1234。那么我可以在算法开始的时候依次把这些的哈希值算出来存起来,后面再用就好。
然后是右边,右边的哈希值有两种更新方式:8765->765->654。刚才虽然说减去1000*8非常轻巧,但是这个乘方值每次都要循环乘法的话就失去哈希的意义了 …不过想到这里已经解决了左边哈希值的问题,所以考虑也在初始化的时候存一下各乘方值就好。
最后对于奇数的第一各特殊值的判断,也可以在构造第一组哈希值的时候顺便算出来:
class Solution {
private static final int base = 131;
private static final int mod = 10000007;
private boolean isValid(String s, int l, int r) {
while(l >= 0) {
if(s.charAt(l) != s.charAt(r))
return false;
--l;
++r;
}
return true;
}
public String shortestPalindrome(String s) {
//先来个简单一点的?
//用 Rabin-Karp 字符串哈希来加快匹配速度
if(s.length() <= 1)
return s;
int pov = s.length() / 2 - 1;
int length = -1;
int rValue = s.charAt(2 * pov + 1);//两头高位 中间低位
int rr = s.charAt(s.length() - 1);//先判断一下当前字符串是不是回文子串,顺便对奇数的情况作了判断
//左右的可以区别对待
int[] lValues = new int[pov + 1];
int[] pow = new int[pov + 1];
lValues[0] = s.charAt(0) % mod;
pow[0] = 1;
for(int i = 1; i <= pov; ++i) {
pow[i] = (pow[i - 1] * base) % mod;
rValue = (rValue * base + s.charAt(2 * pov + 1 - i)) % mod;
rr = (rr * base + s.charAt(s.length() - 1 - i)) % mod;
lValues[i] = (lValues[i - 1] * base + s.charAt(i)) % mod;
}
if(rr == lValues[pov] && isValid(s, pov, pov + 2))
return s;
while(true) {
if(lValues[pov] == rValue && isValid(s, pov, pov + 1)) {
length = 2 * (s.length() - pov - 1);
break;
}
rValue -= s.charAt(2 * pov + 1) * pow[pov];
while(rValue < 0)
rValue += mod;
if(pov == 0 || lValues[pov - 1] == rValue && isValid(s, pov - 1, pov + 1)) {
length = (s.length() - pov) * 2 - 1;
break;
}
rValue -= s.charAt(2 * pov) * pow[pov - 1];
while(rValue < 0)
rValue += mod;
rValue = (base * rValue + s.charAt(pov)) % mod;
--pov;
}
length = length - s.length();
StringBuilder builder = new StringBuilder(s.substring(s.length() - length));
builder.reverse();
return builder.toString() + s;
}
}
4ms,老夫可以瞑目了。
搬运一下题解
class Solution {
public String shortestPalindrome(String s) {
int n = s.length();
int base = 131, mod = 1000000007;
int left = 0, right = 0, mul = 1;
int best = -1;
for (int i = 0; i < n; ++i) {
left = (int) (((long) left * base + s.charAt(i)) % mod);
right = (int) ((right + (long) mul * s.charAt(i)) % mod);
if (left == right) {
best = i;
}
mul = (int) ((long) mul * base % mod);
}
String add = (best == n - 1 ? "" : s.substring(best + 1));
StringBuffer ans = new StringBuffer(add).reverse();
ans.append(s);
return ans.toString();
}
}
直接寻找s中最长的回文前缀,得到的答案会由坏到好。这里耍了个小聪明,如果当前的正反哈希值相同就认为是回文前缀了。如果要认真的话应该再进一步判断一下的防止哈希冲突。我写的里头加了放=防哈希冲突的判断。
class Solution {
public String shortestPalindrome(String s) {
int n = s.length();
int[] fail = new int[n];
Arrays.fill(fail, -1);
for (int i = 1; i < n; ++i) {
int j = fail[i - 1];
while (j != -1 && s.charAt(j + 1) != s.charAt(i)) {
j = fail[j];
}
if (s.charAt(j + 1) == s.charAt(i)) {
fail[i] = j + 1;
}
}
int best = -1;
for (int i = n - 1; i >= 0; --i) {
while (best != -1 && s.charAt(best + 1) != s.charAt(i)) {
best = fail[best];
}
if (s.charAt(best + 1) == s.charAt(i)) {
++best;
}
}
String add = (best == n - 1 ? "" : s.substring(best + 1));
StringBuffer ans = new StringBuffer(add).reverse();
ans.append(s);
return ans.toString();
}
}
和上一个的原理相同但是巧妙地运用了KMP法。设s’是s的反序,寻找s’的后缀和s的前缀重合的最长长度,而s‘的后缀其实就是反过来的s的前缀。KMP法我属于勉强能看懂但是不能灵活运用的程度…玩不了题解这么花啊TAT