1. 最长回文串
一般用后缀数组或者后缀树可以解决,
用此方法:http://blog.csdn.net/v_july_v/article/details/6897097
1.1 是不是想到一种求s和s'最长公共子串(注意不是最长公共子序列)?
这个方法在有时候不work,
S = “abacdfgdcaba”, S’ = “abacdgfdcaba”.
The longest common substring between S and S’ is “abacd”.明显错误。
1.2
依次遍历字符串S中可能的中心点,注意,中心点可能在字符,也可能在两个字符之间
//从中心向两端扩展 string expandAroundCenter(string s,int c1,int c2) { int l=c1; int r=c2; int n=s.length(); while (l>=0 && r<=n-1 && s[l]==s[r]) { l--; r++; } return s.substr(l+1,r-l-1); } //方法3:从中心向两端扩展 string IsPalindrome3(string str) { int n=str.length(); if (n==0) { return ""; } string longest=str.substr(0,1); for (int i=0;i<n-1;i++) { string p1=expandAroundCenter(str,i,i); if (p1.length()>longest.length()) { longest=p1; } string p2=expandAroundCenter(str,i,i+1); if (p2.length()>longest.length()) { longest=p2; } } return longest; }
Define P[ i, j ] ← true iff the substring Si … Sj is a palindrome, otherwise false.
P[ i, j ] ← ( P[ i+1, j-1 ] and Si = Sj )
This yields a straight forward DP solution, which we first initialize the one and two letters palindromes, and work our way up finding all three letters palindromes, and so on…
//方法2:动态规划 string IsPalindrome2(string str) { if (str=="") { return ""; } int n=str.length(); int maxIndex=0; int maxLength=1; bool IsPal[1000][1000]={false}; for (int i=0;i<n;i++) { IsPal[i][i]=true; } for (int i=0;i<n-1;i++) { if (str[i]==str[i+1]) { IsPal[i][i+1]=true; maxIndex=i; maxLength=2; } } for (int len=3;len<=n;len++) { for (int i=0;i<=n-len;i++) { int j=i+len-1; if (IsPal[i+1][j-1] && str[i]==str[j]) { IsPal[i][j]=true; maxIndex=i; maxLength=len; } } } return str.substr(maxIndex,maxLength); }
但是有一种巧妙的方法,不构建后缀树在o(n)完成
转自:http://www.felix021.com/blog/read.php?2040
首先用一个非常巧妙的方式,将所有可能的奇数/偶数长度的回文子串都转换成了奇数长度:在每个字符的两边都插入一个特殊的符号。比如 abba 变成 #a#b#b#a#, aba变成 #a#b#a#。 为了进一步减少编码的复杂度,可以在字符串的开始加入另一个特殊字符,这样就不用特殊处理越界问题,比如$#a#b#a#。
下面以字符串12212321为例,经过上一步,变成了 S[] = "$#1#2#2#1#2#3#2#1#";
然后用一个数组 P[i] 来记录以字符S[i]为中心的最长回文子串向左/右扩张的长度(包括S[i]),比如S和P的对应关系:
int Palindrome(char *str) { int len = strlen(str); char *Str = new char[len+len+3]; memset(Str,0,len+len+3);// Str[0] = '$'; int Len = len+len+2; int *scale = new int[Len]; memset(scale,0,sizeof(int)*Len); int j=0; for (int i=1;i<Len;++i) { if(i%2==0) Str[i] = str[j++]; else Str[i] = '#'; } Str[Len] = '\0'; int id = 0;//在i之前的回文段,这个回文段的中心点,这个回文段的右边的边界最大 int mx = 0;//在i之前的回文段,这个回文段的右边的边界最大 for (int i=1;i<Len;++i) { if (mx >i)//如果之前的回文边界超过i,要找到i关于回文中心点id的对称点j { int j = 2*id - i; int rightSpan = mx - i; if(scale[j]>rightSpan) scale[i] = rightSpan; else scale[i] = scale[j]; }else scale[i] = 1; int right = i + scale[i] +1; int left = i - scale[i] -1; while ( (right<Len) && (Str[right]==Str[left]) ) { ++scale[i]; ++right; --left; } if (i+scale[i]>mx) { mx = scale[i]; id = i; } } int maxValue = 0; //cout<<"Len "<<Len<<endl; for (int i=0;i<Len;++i) { //cout<<i<<endl; if(scale[i]>maxValue) maxValue = scale[i]; } //cout<<"out"<<endl; delete []Str; delete []scale; return maxValue ; }
第四题可以看做是longest common substring,可以就是说要求最长公共字串必须是连续的,用后缀树或者后缀数组的时间复杂度是o(n+m)
当然还可以用动态规划实现,o(n*m):(为了避免边界检查,f[][]都是从1开始计算的,但是f[i][j],表示的是a[i-1]b[j-1])
char a[]="acbedf"; char b[]="ecbedfacabe"; int f[50][50]; memset(f,0,sizeof(f)); int maxL=0,ai=0; for (int i=1;i<=sizeof(a);++i) { for(int j=1;j<=sizeof(b);++j) { if(a[i-1]==b[j-1]) { f[i][j] =f[i-1][j-1] +1; if (f[i][j]>maxL) { maxL = f[i][j]; ai = i-1; } } } } char c[50]; memset(c,0,sizeof(c)); strncpy(c,a+ai-maxL+1,maxL); cout<<maxL<<endl<<c<<endl;
以上代码主要参考:http://sagittarix.blogspot.com/2008/11/blog-post_06.html
\ |
e |
d |
c |
e |
d |
n |
n |
0 |
0 |
0 |
0 |
0 |
1 |
c |
0 |
0 |
1 |
0 |
0 |
0 |
e |
1 |
0 |
0 |
1 |
0 |
0 |
d |
0 |
1 |
0 |
0 |
1 |
0 |
t |
0 |
0 |
0 |
0 |
0 |
0 |
我们把两个字符串看作矩阵的x-y轴:首先遍历字符串时,把两个字符相同的位置标记成1,不相同的地方标记成0。很容易证明,如果能形成公共子序列,则最长子序列一定是从某个位置开始的对角线的长度,所以我们需要做的就是统计对角线的长度。
例如str1=”ncedt”与str2=”edcedn”生成的矩阵如左矩阵,遍历找到最长的对角线即可。
e |
d |
c |
e |
d |
n |
|
n |
0 |
0 |
0 |
0 |
0 |
1 |
c |
0 |
0 |
1 |
0 |
0 |
0 |
e |
1 |
0 |
0 |
2 |
0 |
0 |
d |
0 |
2 |
0 |
0 |
3 |
0 |
t |
0 |
0 |
0 |
0 |
0 |
0 |
其实这里还可以优化,即在生成表格的时候,加上对角线上比它前面的元素,这样一遍遍历就可以生成左图所示矩阵,中间记下最大值便可,省下最后一趟比较,降低时间复杂度;空间复杂度也可降低,其实只要一维的数组便可以了,因为每次更新只用到上面一行(或左边一列,看你程序怎么写了,都可以)。
这个直观上很好理解,但拆分到子问题的思想表达起来比较绕,就转一下别人的文字:
---------------------------------------------------------------------------------------------------------------------
定义f(m, n)为串Xm和Yn之间最长的子字符串的长度并且该子字符串结束于Xm 和 Yn。因为要求是连续的,所以定义f的时候多了一个要求,即字符串结束于Xm 和 Yn。
于是有f(m, 0) = f(0, m) = 0
如果xm != yn, 则f(m, n) = 0
如果xm = yn, 则f(m, n) = f(m-1, n-1) + 1
因为最长字符串不一定结束于Xm 和 Yn末尾,所以这里必须求得所有可能的f(p, q) | 0 <>最大的f(p, q)就是解。依照公式用Bottom-up DP可解。
代码就不写了,简单。但是它与其它的DP填表又有所不同,不是直接得出最大的填到表格最后一格中,而是找出格子中的最大值,其中差别,细细体会
还有一个问题是 longest common subsquence ,要求最长公共序列不一定是连续的,但是有序的,一般只能用动态规划实现,o(n*m)
算法导论讲的很透彻,这个博客也不错:http://blog.csdn.net/orbit/article/details/6717125
现在来分析一下本问题的最优子结构。首先定义问题,假设有字符串str1长度为m,字符串str2长度为n,可以把子问题描述为:求字符串str1<1..m>中从第1个到第i(i <= m)个字符组成的子串str1<1…i>和字符串str2<1..n>中从第1个到第j(j <= n)个字符组成的子串str2<1…j>的最长公共序列,子问题的最长公共序列可以描述为d[i,j] = { Z1,Z2, … Zk },其中Z1-Zk为当前子问题已经匹配到的最长公共子序列的字符。子问题定义以后,还要找出子问题的最优序列d[i,j]的递推关系。分析d[i,j]的递推关系要从str1[i]和str2[j]的关系入手,如果str1[i]和str2[j]相同,则d[i,j]就是d[i-1,j-1]的最长公共序列+1,Zk=str1[i]=str2[j];如果str1[i]和str2[j]不相同,则d[i,j]就是d[i-1,j]的最长公共序列和d[i,j-1]的最长公共序列中较大的那一个。
最后是确定d[i,j]的边界值,当字符串str1为空或字符串str2为空时,其最长公共子串应该是0,也就是说当i=0或j=0时,d[i,j]就是0。d[i,j]的完整递推关系如下:
d[i,j]表示,对于序列str1[0,i]和str2[0,j]的lcs
构建一颗后缀树的编码难度较大,一般解题应该用后缀数组:http://dongxicheng.org/structure/suffix-array/
有时间再研究下后缀数组吧。。。