LeetCode之旅(C/C++):5. 最长回文子串

#PS:不明之处,请君留言,以期共同进步!


##1、题目描述
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为1000。

###示例 1:

输入: “babad”
输出: “bab”
注意: "aba"也是一个有效答案。
###示例 2:

输入: “cbbd”
输出: “bb”
##2、代码实现
###2.1 C++语言
####2.1.1动态规划
首先分析问题:
一个字符的串肯定是一个回文串;
两个字符只要相同,它们在一起也构成一个回文串;
三个字符的串,只要首尾两个字符相同,那它也是一个回文串;
以此类推,如果一个字符串,处于它的首尾两个字符之间的子串是回文串,并且它的首尾字符相同,则它就是一个回文串。
总结,当一个串有三个字符时,我们可以用中间一个字符的情况加上它的首尾字符是否相同推出来;当一个串有四个字符时,我们可以用中间两个字符的情况加上它的首尾字符是否相同推出来,按照这个规律递推下去,就能判断所有长度的子串(包括相同长度的不同子串)是否是回文串。
因此,我们需要一个二维数组 bool dp[len][len] 来缓存一个串是否是回文串的结果,第一维是字符串的首字符的下标,第二维是字符串的尾字符的下标,用 dp[i][j] 的值表示str[i…j]这个串是否是回文串,true代表是,false代表否。这样就可以缓存每个串是否是回文串的结果,当我们枚举所有串来判断它们是否是回文串时,就可以借助缓存的结果(当前串去掉首尾字符的子串是不是回文串),用O(1)的时间复杂度快速地对当前串做出判断。
我们定义:

dp[i][j] = true(当串str[i…j]是回文串)
dp[i][j] = false(当串str[i…j]不是回文串)

并且可以得到以下的状态转换方程:

dp[i][j] = (dp[i + 1][j - 1] && str[i] == str[j])

具体解法如下:
当串长度为以 1 时,dp[i, i] = true(0 <= i < len);
当串长度为 2 时,dp[i, i+1] = (str[i] == str[i+1]) (0 <= i < len - 1),前两种情况很简单,我们直接判断并初始化dp数组;
当串长度大于等于3时,我们使用状态转换方程进行递推。

动态规划的过程如下图:
LeetCode之旅(C/C++):5. 最长回文子串_第1张图片
#####动态规划的第一种代码

class Solution {
public:
    //动态规划,O(n^2)
    //根据len大小创建dp数组,涉及到创建、初始化和销毁的过程,必然是消耗时间节省空间
    //如果牺牲空间节省时间,可以这样定义二维数组: bool dp[1000][1000] = {false};
    string longestPalindrome(string s) 
    {
        int start = 0;	//最大回文子串起始地址
        int maxSubLen = 0;	//最大回文子串长度
        int subLen = 0;	//当前回文子串长度
        const int len = s.length();	//母串长度

        //创建二维数组dp
        //bool(*dp)[len] = new bool[len][len];//不行,要求len是常量
        bool **dp = new bool *[len];//指针数组,包含len个bool指针
        for (int i = 0; i < len; ++i)
        {
            dp[i] = new bool[len];//每个bool指针指向一个包含len个bool元素的一维数组
        }
        
        //初始化二维数组dp
        for (int i = 0; i < len; ++i)
        {
            for (int j = 0; j < len; ++j)
            {
                dp[i][j] = false;
            }
        }
        
        // 子串长度为1和为2的初始化
        for (int i = 0; i < len; ++i) 
        {
            dp[i][i] = true;
            subLen = 1;
            if (subLen >= maxSubLen)
            {
                start = i;
                maxSubLen = subLen;
            }
            if (i < len - 1 && s.at(i) == s.at(i + 1)) 
            {
                dp[i][i + 1] = true;
                subLen = 2;
                if (subLen >= maxSubLen)
                {
                    start = i;
                    maxSubLen = subLen;
                }
            }
        }

        // 使用上述结果可以dp出子串长度为3~len的子串
        for (int strlen = 3; strlen <= len; ++strlen) //子串长度
        {
            for (int i = 0; i <= len - strlen; ++i) //子串起始地址
            {
                int j = i + strlen - 1; // 子串结束地址
                if (dp[i + 1][j - 1] && s.at(i) == s.at(j)) 
                {
                    dp[i][j] = true;
                    start = i;
                    maxSubLen = strlen;
                }
            }
        }

        //销毁二维数组dp
        for (int i = 0; i < len; ++i)
        {
            delete[] dp[i];
        }
        delete[]dp;

        //返回最长回文子串
        return s.substr(start, maxSubLen);
    }
};

#####动态规划的第二种代码

class Solution {
public:
    //动态规划O(n^2)
    //二维数组dp的第一维是子串中首字符下标,第二维是尾字符下标
    //dp[i][j]代表以i,j为起止下标的子串是否是回文串,true代表是,false代表否
    //如果dp[i+1][j-1]是true,且str[i]=str[j],则dp[i][j]也是true
    //用dp[i][j]缓存str[i...j]是否是回文串,可以在枚举所有子串的时候,根据之前缓存的结果快速判断当前子串是否是回文串
    string longestPalindrome(string s) 
    {
        int len = s.length();//母串长度
        int start = 0, end = 0;//最长回文子串的起止地址

        //创建二维数组dp,用于动态规划
        bool **dp = new bool *[len];
        for (int i = 0; i < len; ++i)
        {
            dp[i] = new bool[len];
        }

        //初始化二维数组dp,若不初始化,默认初始化为true
        for (int i = 0; i < len; ++i)
        {
            for (int j = 0; j < len; ++j)
            {
                dp[i][j] = false;
            }
        }

        //查找最长回文子串
        for (int i = 0; i < len; ++i)//子串首尾字符的间距,最小是0,最大是len - 1
        {
            for (int j = 0; j < len - i; ++j)//j + i是子串尾字符下标,要小于len,不能越界
            {
                //对子串分情况讨论
                if (i == 0)//子串长度为1
                {
                    dp[j][j + i] = true;
                }
                else if (i == 1)//子串长度为2
                {
                    if (s.at(j) == s.at(j + i))
                    {
                        dp[j][j + i] = true;
                    }
                }
                else//子串长度为3~len,此时大子串去掉首尾字符后还是小子串
                {
                    if (dp[j + 1][j + i - 1] && s.at(j) == s.at(j + i))
                    {
                        dp[j][j + i] = true;
                    }
                }
                //子串str[j...j+i]是回文串
                //最长回文子串一定是所有回文子串中最后被枚举到的
                if (dp[j][j + i] == true)
                {
                    start = j;
                    end = j + i;
                }
            }
        }

        //销毁二维数组dp
        for (int i = 0; i < len; ++i)
        {
            delete[]dp[i];
        }
        delete[]dp;

        //返回最长回文子串
        return s.substr(start, end - start + 1);
    }
};

####2.1.2 Manacher算法(马拉车算法)
参考文档:【回文串-Manacher】

class Solution {
public:
    //O(n)
    string longestPalindrome(string s)
    {
        //s为空直接返回
	    if (s.empty())
	    {
		    return s;
	    }
        
        //加入特殊字符,前:babab,后:(#b#a#b#a#b#)
        string tmp = s;
        int len = tmp.length();
        for (int i = 0, j = 0; i <= len; ++i)
        {
            tmp.insert(j, "#");
            j = j + 2;
        }
        tmp.insert(0, "(");//防止越界
        tmp.push_back(')');

        //创建并求解辅助数组rad[]
        //rad[i]是以s[i]为中心字符的最长回文子串的半径,即它的最右字符和中心字符的下标之差
        len = len * 2 + 3;
        int *rad = new int[len];
        //i用来遍历tmp串,并有k跳跃;j是回文半径,k是从1到rad[i]的值,用来求rad[i+1]到rad[i+rad[i]]的值
        for (int i = 1, j = 0, k; i < len - 1; i += k)
        {
            //判断以 tmp[i] 为中心,j+1 为半径的子串是否是回文串
            while (tmp.at(i - j - 1) == tmp.at(i + j + 1))
                ++j;//回文半径加 1,继续判断
            rad[i] = j;//找到以s[i]为中心的最大回文子串,用回文半径j初始化rad[i]
            //镜像,遇到rad[i-k]=rad[i]-k停止,这时不用从j=1开始比较
            for (k = 1; k <= rad[i]  && rad[i - k] != rad[i] - k; ++k)
                rad[i + k] = min(rad[i - k], rad[i] - k);
            j = max(j - k, 0);//更新j
        }
        
        int maxLen = 0;//最大回文子串的长度
        int start;//最大回文子串的起始地址
        //遍历rad数组找到最大回文子串的长度和起始地址
        for (int i = 1; i < len - 1; ++i)
        {
            if (rad[i] > maxLen)
            {
                maxLen = rad[i];
                start = (i - maxLen + 1) / 2 - 1;
            }
        }
        return s.substr(start, maxLen);//返回最大回文子串
    }
};

你可能感兴趣的:(LeetCode之旅)