字符串算法题
1.最长回文子字符串(Manacher!)
给定一个字符串S=A1A2...An,要求找出其最长回文子串(Longest Palindromic Substring)。所谓回文子串就是S的某个子串Ai...Aj为回文。例如,对字符串S=abcdcbeba,它的回文子串有:bcdcb,cdc,beb,满足题目要求的最长回文子串为bcdcb。
分析:
*回文可能由奇数个字符组成,也可能由偶数个字符组成。
解决方案:在字符边界添加特殊符号。例如,对字符串aba,预处理后变成#a#b#a#;对字符串abba,预处理后变成#a#b#b#a#。可以看出,不管是奇数回文,还是偶数回文,在与处理后都变成奇数回文。在找出与预处理后字符串的最长回文后,只需要去除所有的#即为源字符串的最长回文。其实就是添加b和b之前的中心为#。
*穷举->o(n^3)
*可以以源字符串的每个字符为中心,依次寻找出最长回文子串P0, P1,...,Pn。这些最长回文子串中的最长串Pi = max(P1, P2,...,Pn)即为所求。时间o(n^2),空间o(n)。
*对字符串S=abcdcba而言,最长回文子串是以d为中心,半径为3的子串。当我们采用上面的做法分别求出以S[1]=a, S[2]=b, S[3]=c, S[4]=d为中心的最长回文子串后,对S[5]=c,S[6]=b...还需要一一进行扩展求吗?答案是NO。因为我们已经找到以d为中心,半径为3的回文了,S[5]与S[3],S[6]与S[2]...,以S[4]为对称中心。因此,在以S[5],S[6]为中心扩展找回文串时,可以利用已经找到的S[3],S[2]的相关信息直接进行一定步长的偏移,这样就减少了比较的次数(回想一下KMP中next数组的思想)。这就是Manacher算法。
id为已找出的最大回文子串的中心。mx为最大回文子串的尾节点。i为当前需要计算的节点中心。P[]为保存的以中心为下标的回文半径。
算法的核心思想就是利用对称和动态规划。
关键步骤就是以下两步:
if(i < mx)
P[i] = MIN(P[2 * id - i], mx - i);
当mx-i>P[j]时,
图中最下面的红色线条是之前求得的索引号i之前的那个使得回文子串最右面的字符的索引号最大的那个回文子字符串。j点是i关于id的对称点,由于红的字符串是回文字符串,所以关于j对称的回文子串和关于i对称的回文子串是完全一样的(图中两段绿色的线条),而满足mx-i>P[j]时说明此时j的回文子串半径小于j到mx关于j对称的左端点的差,此时可以初始化P[i]=P[j]。
当mx-i<=P[j]:
图中最下面的红色线条仍然是之前求得的索引号i之前的那个使得回文子串最右面的字符的索引号最大的那个回文子字符串。j点是i关于id的对称点,由于红的字符串是回文字符串,所以关于j对称的回文子串和关于i对称的在mx和mx的对称点之间的回文子串是完全一样的(图中两段绿色的线条),而满足mx-i<=P[j]时说明此时j的回文子串半径大于或等于j到mx关于j对称的左端点的差,此时可以初始化P[i]=mx-i,再对P[i]的回文子串半径进行进一步的增大。
此算法的关键思想还是在于利于规则来进行动态规划(根据前面步骤得出的结果计算后面的值)。
代码:
1 package nothing; 2 3 /** 4 * 给定一个字符串S=A1A2...An,要求找出其最长回文子串(Longest Palindromic Substring)。 5 * 所谓回文子串就是S的某个子串Ai...Aj为回文。例如,对字符串S=abcdcbeba, 6 * 它的回文子串有:bcdcb,cdc,beb,满足题目要求的最长回文子串为bcdcb。 7 * @author hasee 8 * 9 */ 10 public class Manacher { 11 public static void main(String[] args) { 12 char[] arr = "156566665".toCharArray(); 13 char[] subArr = getMax(arr); 14 for (char c : subArr) { 15 System.out.print(c); 16 } 17 System.out.println(); 18 } 19 20 private static char[] getMax(char[] arr) { 21 if(arr == null) 22 return null; 23 char[] inter = interpolate(arr); //内插'*',使长度变成2*length+1,变成奇数长度 24 int[] P = new int[inter.length]; //半径结果数组 25 int id=-1,mx=-1; //id最长子串中心,mx最长子串尾 26 for (int i = 0; i < inter.length; i++) { 27 if(i<mx) //2*id-i为i关于id的镜像,由对称性可进行动态规划 28 P[i] = P[2*id-i]<mx-i ? P[2*id-i] : mx-i; 29 else 30 P[i] = 0; 31 //注意检查是否越界,利用&&的贪婪性,扩展i的半径值 32 while(i-P[i]-1>0&&i+P[i]+1<inter.length&&inter[i-P[i]-1]==inter[i+P[i]+1]) 33 P[i]++; 34 if (P[i]>mx-id) { //更新最大半径 35 id=i; 36 mx=id+P[i]; 37 } 38 } 39 if(mx-id==0) 40 return null; 41 char[] subArr = new char[mx-id]; 42 int index=0; 43 for (int i = 2*id-mx; i <= mx; i++) if(inter[i]!='*') subArr[index++]=inter[i]; 44 return subArr; 45 } 46 private static char[] interpolate(char[] arr) { 47 char[] newArr = new char[(arr.length<<1)+1]; 48 for (int i = 0; i < arr.length; i++) { 49 newArr[i<<1] = '*'; 50 newArr[(i<<1)+1] = arr[i]; 51 } 52 newArr[arr.length<<1] = '*'; 53 return newArr; 54 } 55 }
参考:http://my.oschina.net/pathenon/blog/63575
http://blog.csdn.net/pi9nc/article/details/9251455