回文子序列和子串的定义参考:http://www.cnblogs.com/AndyJee/p/4465696.html
子序列:下标可以不连续
子串:下标连续
最长回文子序列:dp[i][j]表示从下标i到j的字符串中,最长回文子序列的长度。
动态规划的状态转移方程为:
设字符串为str,长度为n,dp[i][j]表示第i到第j个字符间的子序列的个数(i<=j),则:
状态初始条件:dp[i][i]=1 (i=0:n-1)
状态转移方程:dp[i][j]=dp[i+1][j-1] + 2 if(str[i]==str[j])
dp[i][j]=max(dp[i+1][j],dp[i][j-1]) if (str[i]!=str[j])
下面代码打印回文子序列最大长度和对应的回文子序列。
#include <iostream> #include <string> #include <cstring> using namespace std; #define N 100 //最长回文子序列的长度 int f[N][N] = { 0 }; int f_cur[N] = { 0 }; int f_pre[N] = { 0 }; int path[N] = { 0 }; //回文子序列的个数 int num[N][N] = { 0 }; int main() { string s; cin >> s; int n = s.length(); for( int i = 0; i < n; i++ ) { f[i][i] = 1; } for( int i = n-1; i >= 0; i-- ) { for( int j = i+1; j < n; j++ ) { if( s[i] == s[j] ) { f[i][j] = f[i+1][j-1] + 2; } else { f[i][j] = max( f[i+1][j], f[i][j-1] ); } //cout << "test: " << "i: " << i << " j: " << j << " f[i][j]: " << f[i][j] << endl; } } //尝试着像背包问题那样降低维度,但行不通 //1.由上面的代码可知,求f[I][j]时,需要f[I+1][j],f[I+1][j-1],这两个是迭代上一个i值(I+1)时得到的结果 //2.除此之外,求f[I][j]还需要f[I][j-1],这个是迭代当前i值(I)时得到的结果。 //如果像背包问题那样降低维度, //由1,j应该按照从大到小的顺序。因为要使用f[I+1][j-1],保证f'[j-1]是原来的,对应上一个i值 //由2,j应该按照从小到大的顺序。因为要使用f[I][j-1],保证f'[j-1]是现在这一次迭代的新值, //所以要有辅助空间,参考 http://blog.csdn.net/u012243115/article/details/41010913,只保存上一个i值的结果 for( int i = n-1; i >= 0; i-- ) { f_cur[i] = 1; for( int j = i+1; j < n; j++ ) { if( s[i] == s[j] ) { if( i+1 > j-1 ) //对应于上面代码中当i+1>j-1时f[i+1][j-1]默认为0,这里需要手动判断一下 //所以最好把子问题表达式写出来,考虑各种情况 { f_cur[j] = 2; } else { f_cur[j] = f_pre[j-1] + 2; } } else { f_cur[j] = max( f_pre[j], f_cur[j-1] ); } } //只保存上一个i值的结果 for( int k = 0; k < n; k++ ) { f_pre[k] = f_cur[k]; } } //输出最长回文子序列的长度, cout << f[0][n-1] << endl; cout << f_cur[n-1] << endl; //如果有多个全部输出,( 如果重复的不输出(set) ) for( int i_ind = 0; i_ind < n; i_ind++ ) { for( int j_ind = i_ind+1; j_ind < n; j_ind++ ) { if( f[i_ind][j_ind] == f[0][n-1] ) { memset( path, 0, sizeof( path ) ); //cstring int i = i_ind, j = j_ind; while( i <= j ) { if( i == j ) { path[i] = 1; break; } //if( f[i][j] == f[i+1][j-1]+2 ) //这样判断不对 if( s[i] == s[j] ) { path[i] = 1; path[j] = 1; i++; j--; } else if( f[i][j] == f[i+1][j] ) { i++; } else if( f[i][j] == f[i][j-1] ) { j--; } } for( int i = 0; i <= n; i++ ) { if( path[i] == 1 ) { cout << s[i] << " "; } } cout << endl; } } }
求回文子序列个数同样是DP思想,num[i][j]代表从下标i到j的字符串中,回文子序列的个数。同样考虑i>j, i=j, i<j的情况。
i<j:如果s[i] != s[j],num[i][j] = num[i+1][j] + num[i][j-1] - num[i+1][j-1];
如果s[i] == s[j],num[i][j] = num[i+1][j] + num[i][j-1] - num[i+1][j-1] + num[i+1][j-1] + 1
= num[i+1][j] + num[i][j-1] + 1;
后面加的这个 num[i+1][j-1] 是 “s[i] + s[i+1]~s[j-1]的回文子序列 + s[j]” 所组成的新回文子序列,最后的1是指还需要加上“s[i]s[j]”这个新回文子序列。
代码如下:
for( int i = n-1; i >= 0; i-- ) { num[i][i] = 1; for( int j = i+1; j < n; j++ ) { if( s[i] != s[j] ) { num[i][j] = num[i+1][j] + num[i][j-1] - num[i+1][j-1]; } else { num[i][j] = num[i+1][j] + num[i][j-1] + 1; } } } //输出回文子序列的个数 cout << num[0][n-1] << endl;
————————————————————————————————————————————————————————————————————————
接下来是最长回文子串的长度。p[i][j]表示从下标i到下标j的字符串是否为回文串,
i>j时,p[i][j] = 0,其实没有用到
i==j时,p[i][j] = 1;
i < j 时,如果s[i]==s[j],且s[i]与s[j]之间的字符串也为回文串,即p[i+1][j-1]==1时,p[i][j]=1;
但当s[i]==s[j],i 与 j 相邻时,无法用上一条规则判断,此时p[i][j]应该为1
当s[i]!=s[j],p[i][j]=0
我们可以用变量将最大长度与起始下标记录下来。(其实也可以利用最长回文子序列的思想,令p[i][j]表示从i到j的字符串中包含的最长回文子串的长度,f[i][j]表示s[i]到s[j]字符串是否为回文串,不过不如以上方式直观)
代码如下:
int v_max = 0; int begin = 0, end = 0; for( int i = n-1; i >= 0; i-- ) { p[i][i] = 1; for( int j = i+1; j < n; j++ ) { if( s[i] == s[j] ) { if( i+1 == j || p[i+1][j-1] > 0 ) { p[i][j] = 1; if( j-i+1 > v_max ) { v_max = j-i+1; begin = i; end = j; } } } else { p[i][j] = 0; } } } //最长回文子串的长度 cout << "---" << v_max << endl; //最长回文子串 for( int i = begin; i <= end; i++ ) { cout << s[i] << " "; }
//回文子串的个数 int cnt = 0; for( int i = 0; i < n; i++ ) { for( int j = i; j<n; j++ ) { if( p[i][j] ) { cnt++; } } }
因为p[i][j]表示从下标i到下标j的字符串是否为回文串,且求p[i][j]只依赖于p[i+1][j-1],这里只用一个一维数度代替p[i][j]。
因为这一轮的p[j]依赖上一轮的p[j-1],所以j要从大到小进行计算。
如果是 这一轮的p[j]依赖这一轮的p[j-1],那就j就要从小到大进行计算。
int p[50]; int find( string &s ) { int slen = 1; //int begin = 0; int end = 0; int n = s.size(); for( int i = n-1; i >= 0; i-- ) { p[i] = 1; for( int j = n-1; j >=i+1; j-- ) { if( s[i] == s[j] ) { if( j == i+1 || p[j-1] == 1 ) { p[j] = 1; } if( slen < j - i + 1 ) { slen = j - i + 1; //begin = i; //end = j; } } else { p[j] = 0; } } } cout << slen << endl; return slen; }
回文子串的个数:
像上面一样,p[i][j]代表从下标i到j的串是否为回文子串。 记录其个数。
代码:
int countSubstrings(string s) { int n = s.length(); bool dp[n][n]; memset( dp, false, n*n*sizeof( bool ) ); int ret = 0; for( int i = n-1; i >= 0; i-- ) { dp[i][i] = true; ret++; for( int j = i+1; j < n; j++ ) { if( s[i] == s[j] ) { if( i+1 == j || dp[i+1][j-1] ) { dp[i][j] = true; ret++; } else dp[i][j] = false; } else { dp[i][j] = false; } } } return ret; }
还有一种马拉车算法,用来求最大回文子串的长度,O(n )
http://www.jianshu.com/p/799bc53d4e3d
http://blog.csdn.net/u012102306/article/details/51353040
代码1:
int getLongestPalindrome(string s, int nn) { int ret = 0; int n = s.length(); //先扩充, #1#1#2#1#1# string ss = "#"; for( int i = 0; i < n; i++ ) { ss += s[i]; ss += "#"; } //求p数组,保存的为以对应下标字符为中心, //最多向右多少个位置(包括中心),使得中心左右这些位置是回文串。 n = ss.length(); int p[n]; //mx为目前为止,最多向右扩充到了哪个下标 //id为对应的中心的下标 int mx = 0, id = 0; for( int i = 1; i < n; i++ ) { //当i小于mx时,可以利用已求得的最长的回文串 if( i < mx ) { //以id为中心,i对应的下标为j int j = 2*id - i; //以p[j]为中心的回文串,位于以id为中心的回文串中时 //以p[i]为中心的回文串,也位于以id为中心的回文串中,不需要继续探索 if( p[j] < mx - i ) { p[i] = p[j]; } //以p[j]为中心的回文串,位于以id为中心的回文串之外时 //以p[i]为中心的回文串,至少会向右扩充到mx,需要继续探索 else if( p[j] > mx - i ) { p[i] = mx - i; while( i+p[i] < n && i-p[i] >= 0 && ss[i+p[i]] == ss[i-p[i]] ) p[i]++; } //以p[j]为中心的回文串,刚好位于以id为中心的回文串之内 //以p[i]为中心的回文串,至少会向右扩充到mx,需要继续探索 else if( p[j] == mx - i ) { p[i] = mx - i; while( i+p[i] < n && i-p[i] >= 0 && ss[i+p[i]] == ss[i-p[i]] ) p[i]++; } } //当i>=mx时 else { p[i] = 1; while( i+p[i] < n && i-p[i] >= 0 && ss[i+p[i]] == ss[i-p[i]] ) p[i]++; } if( i + p[i] -1 > mx ) { mx = i + p[i] -1; id = i; } //p[ id ] - 1:以下标为id对应的字符为中心,最大回文子串的长度 ret = max( ret, p[i] - 1 ); } return ret; }
代码2:
int getLongestPalindrome(string s, int nn) { int ret = 0; int n = nn; //先扩充, #1#1#2#1#1# string ss = "#"; for( int i = 0; i < n; i++ ) { ss += s[i]; ss += "#"; } //求p数组,保存的为以对应下标字符为中心, //最多向右多少个位置(包括中心),使得中心左右这些位置是回文串。 n = ss.length(); int p[n]; //mx为目前为止,最多向右扩充到了哪个下标 //id为对应的中心的下标 int mx = 0, id = 0; for( int i = 1; i < n; i++ ) { //用一行代码表示 p[i] = i < mx ? min( p[2*id-i], mx-i ) : 1; while( i+p[i] < n && i-p[i] >= 0 && ss[i+p[i]] == ss[i-p[i]] ) p[i]++; if( i + p[i] - 1 > mx ) { mx = i + p[i] -1; id = i; } ret = max( ret, p[i] - 1 ); } return ret; }