最长回文子序列、串 回文子序列、串个数

回文子序列和子串的定义参考: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;
}
原文地址:https://www.cnblogs.com/wangzhiyi/p/6670701.html