最长回文子串与最长回文子序列

最长回文子串与最长回文子序列

最长回文子串

LeetCode 链接: 5. 最长回文子串

题目描述

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

示例 1:

输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。

示例 2:

输入: "cbbd"
输出: "bb"
dp解法

如果我们已经知道 “bab” 是回文子串,那么很明显 “ababa” 一定是回文子串,因为这两个左右端字母是相同的。假设 S 是给定的字符串, S i S_i Si 表示 S 在位置 i 的字符

定义状态:

d p ( i , j ) = { t r u e , 当 S i . . . S j 是回文子串 f a l s e , 其它情况 dp(i,j)=\begin{cases} true, & \text {当} S_i...S_j \text{是回文子串} \\ false, & \text {其它情况} \end{cases} dp(i,j)={true,false,Si...Sj是回文子串其它情况

初始状态:

d p ( i , i ) = t r u e dp(i,i)=true dp(i,i)=true

d p ( i , i + 1 ) = ( S i = = S j ) dp(i,i+1)=(S_i==S_j) dp(i,i+1)=(Si==Sj)

转移方程:

d p ( i , j ) = { S i = = S j   & &   d p ( i + 1 , j − 1 ) , j − i > 2 S i = = S j , 1 < = j − i < = 2 t r u e j − i = 0 dp(i,j) = \begin{cases} S_i==S_j \ \&\& \ dp(i+1,j-1), & j-i>2 \\ S_i==S_j, & 1<=j-i<=2 \\ true & j-i=0 \end{cases} dp(i,j)=Si==Sj && dp(i+1,j1),Si==Sj,trueji>21<=ji<=2ji=0

需要注意的是,求 dp(i+1,j-1) 时要保证 j-i > 2,防止出现越界错误

public String longestPalindrome(String s) {
    if (s == null || s.length() == 0)
        return s;
    int n = s.length();
    char[] str = s.toCharArray();

    String res = null;
    boolean[][] dp = new boolean[n][n];

    for (int i = n - 1; i >= 0; i--) {
        for (int j = i; j < n; j++) {
            dp[i][j] = str[i] == str[j] && (j - i <= 2 || dp[i + 1][j - 1]);
            if (dp[i][j] && (res == null || j - i + 1 > res.length())) {
                res = s.substring(i, j + 1);
            }
        }
    }
    return res;
}

时间复杂度: O ( n 2 ) O(n^2) O(n2)

空间复杂度: O ( n 2 ) O(n^2) O(n2)

n 是字符串长度

从中心扩展

考虑到回文子串是关于中心对称的,因此一个回文子串可以从中心向两边扩展。实际就是暴力搜索的思想,从回文子串的中心向两边展开搜索匹配。另外回文子串的长度可能是奇数也可能是偶数,从而,不仅要以每个字符为中心搜索(此时回文子串长度为奇数),而且还要以相邻字符之间的位置为中心搜索(此时回文子串长度为偶数),因此我们需要搜索 2 n − 1 2n-1 2n1 个中心。 例如,对于字符串 aba,中心是字符 b,对于字符串 abba,中心是两个 b 之间的位置。

public String longestPalindrome(String s) {
    if (s == null || s.length() == 0)
        return s;
    // 记录当前最长回文子串开始字符和结束字符的位置
    int start = 0, end = 0;
    for (int i = 0; i < s.length(); ++i) {
        // 回文中心是字符
        int len1 = searchFromCenter(s, i, i);
        // 回文中心在字符之间
        int len2 = searchFromCenter(s, i, i + 1);
        int len = Math.max(len1, len2);
        // 出现更长的回文子串,更新位置信息
        if (len > end - start) {
            // 统一处理成开始字符和结束字符的位置
            start = i - (len - 1) / 2;
            end = i + len / 2;
        }
    }
    return s.substring(start, end + 1);
}

private int searchFromCenter(String s, int left, int right) {
    int l = left, r = right;
    while (l >= 0 && r < s.length() && s.charAt(l) == s.charAt(r)) {
        l--;
        r++;
    }
    return r - l - 1;
}

时间复杂度: O ( n 2 ) O(n^2) O(n2)

空间复杂度: O ( 1 ) O(1) O(1)

n 是字符串长度

马拉车算法

关于马拉车算法,单独记录了一下自己的理解:

理解 Manacher’s Algorithm(马拉车算法)——最长回文子串问题

public String longestPalindrome(String s) {
    /**
    字符串处理,例如 S = "babad",处理后 T = "@#b#a#b#a#d#$"
    要在每个字符之间以及字符串首部和尾部添加'#',T 的长度变为2*s.length()+1,另外又要添加'@'和'$'防止越界,因此最终 T 的长度为2*s.length()+3
    **/
    char[] T = new char[2*s.length()+3];
    T[0]='@'; // 避免越左边界,因为'@'肯定和右边的字符不一样,匹配到'@'循环终止
    T[1]='#';
    T[T.length-1]='$'; // 避免越右边界,因为'$'肯定和左边的字符不一样,匹配到'$'循环终止
    int t = 2; // 在首部添加特殊字符后,从下标 2 开始
    for(char c : s.toCharArray()){
        T[t++]=c;
        T[t++]='#';
    }

    // maxLen 记录 T 中最长回文子串的长度,maxCenter为最长回文子串的中心
    int maxLen=0, maxCenter=0;    

    // center为上一个最长回文串的中心,right为其最右端字符的位置
    int[] R = new int[T.length];
    int center = 0, right = 0;
    
    // Manacher's algorithm
    for (int i = 1; i < R.length - 1; ++i) {
        //算法核心部分
        if (i < right)
            R[i] = Math.min(right - i, R[2 * center - i]);
        // 从T[i]依次向两边匹配
        while (T[i + R[i] + 1] == T[i - R[i] - 1])
            R[i]++;
        // 更新回文串中心和最右端位置
        if (i + R[i] > right) {
            center = i;
            right = i + R[i];
        }
        // 如果出现更长的回文子串,更新 maxLen 和 maxCenter
        if(maxLen < R[i]){
            maxLen = R[i];
            maxCenter = i;
        } 
    }    

    // 计算最长回文子串在S中的起始位置
    int index = (maxCenter - 1 - maxLen) / 2;
    //返回S中的最长回文子串
    return s.substring(index, index + maxLen);
}

时间复杂度: O ( n ) O(n) O(n)

空间复杂度: O ( n ) O(n) O(n)

n 是字符串长度

其他解法:字符串哈希。掌握后再补充

最长回文子序列

LeetCode 链接: 516. 最长回文子序列

题目描述

给定一个字符串s,找到其中最长的回文子序列。可以假设s的最大长度为1000

示例 1:
输入:

"bbbab"

输出:

4

一个可能的最长回文子序列为 “bbbb”。

示例 2:
输入:

"cbbd"

输出:

2

一个可能的最长回文子序列为 “bb”。

dp 解法

定义状态: dp(i,j) 表示字符串 s 中从位置 i 到 位置 j 的最长子序列长度,其中 i <= j

转移方程:

d p ( i , j ) = { 1 , 当i=j d p ( i + 1 , j − 1 ) + 2 , 当 s . c h a r A t ( i ) = = s . c h a r A t ( j ) m a x { d p ( i + 1 , j ) , d p ( i , j − 1 ) } , 其他情况 dp(i,j)=\begin{cases} 1, & \text {当i=j} \\ dp(i+1, j-1) + 2, & \text {当}s.charAt(i) == s.charAt(j) \\ max \{ dp(i+1, j), dp(i,j-1) \}, & \text {其他情况} \end{cases} dp(i,j)=1,dp(i+1,j1)+2,max{dp(i+1,j),dp(i,j1)},i=js.charAt(i)==s.charAt(j)其他情况

这里有一点需要注意,i = j - 1 时,如果有 s.charAt(i) == s.charAt(j),那么这时候应该是 dp(i,j) = 2,但因为初始化时有

d p ( i , j ) = { 1 , 当i=j 0 , 当 i ≠ j dp(i,j)=\begin{cases} 1, & \text {当i=j} \\ 0, & \text {当} i \neq j \end{cases} dp(i,j)={1,0,i=ji=j

所以这时候 dp(i,j) = dp(i+1, j-1) + 2 同样满足转移方程,

public int longestPalindromeSubseq(String s) {
    if(s==null || s.length()==0)
        return 0;
    int n = s.length();
    char[] str = s.toCharArray();
    int[][] dp = new int[n][n];  
    for(int i = 0; i < n; i++){
        dp[i][i] = 1;
    }
    for(int j = 1; j < n; ++j){
        for(int i = j - 1; i >= 0; --i){
            if(str[i] == str[j]){
                if(i == j - 1){
                    dp[i][j] = 2;
                }else{
                    dp[i][j] = dp[i + 1][j - 1] + 2;
                }
            }else{
                dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
            }
        }
    }
    return dp[0][n-1];
}

时间复杂度: O ( n 2 ) O(n^2) O(n2)

空间复杂度: O ( n 2 ) O(n^2) O(n2)

n 是字符串长度

简省版:

public int longestPalindromeSubseq(String s) {
    if (s == null || s.length() == 0)
        return 0;
    int n = s.length();
    char[] str = s.toCharArray();
    int[][] dp = new int[n][n];
    for (int i = n - 1; i >= 0; --i) {
        dp[i][i] = 1;
        for (int j = i + 1; j < n; ++j) {
            if (str[i] == str[j]) {
                dp[i][j] = dp[i + 1][j - 1] + 2;
            } else {
                dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]);
            }
        }
    }
    return dp[0][n - 1];
}
递归思想

递归思想的解法跟 dp 解法 几乎相同,

  • s.charAt(i) == s.charAt(j) 时,因为要找回文子序列,所以 s.charAt(i)s.charAt(j) 就属于结果的回文子序列中,继续递归 s.charAt(i+1)s.charAt(j-1) 之间的子串;
  • s.charAt(i) != s.charAt(j) 时,就递归考虑 s.charAt(i+1)s.charAt(j) 之间的子串以及s.charAt(i)s.charAt(j-1) 之间的子串

其中 i <= j,否则直接返回 0;另外防止重复计算,如果 memo[i][j] 已经计算过,直接返回

public int longestPalindromeSubseq(String s) {
        if (s == null || s.length() == 0)
            return 0;
        int n = s.length();
        int[][] memo = new int[n][n];
        return helper(s, 0, n - 1, memo);
    }

private int helper(String s, int i, int j, int[][] memo) {
    if (memo[i][j] != 0) {
        return memo[i][j];
    }
    if (i > j) return 0;
    if (i == j) return 1;

    if (s.charAt(i) == s.charAt(j)) {
        memo[i][j] = helper(s, i + 1, j - 1, memo) + 2;
    } else {
        memo[i][j] = Math.max(helper(s, i + 1, j, memo), helper(s, i, j - 1, memo));
    }
    return memo[i][j];
}

时间复杂度: O ( n 2 ) O(n^2) O(n2)

最好情况下 O ( n ) O(n) O(n),最坏情况下 O ( n 2 ) O(n^2) O(n2)

空间复杂度: O ( n 2 ) O(n^2) O(n2)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值