代码随想录算法训练营第五七天 | 回文子串、最长回文子序列

LeetCode 647. 回文子串
LeetCode 516.最长回文子序列

回文子串

给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。

回文字符串 是正着读和倒过来读一样的字符串。

子字符串 是字符串中的由连续字符组成的一个序列。

具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

  • 判断字符串s 是否是回文,如果知道 s[1],s[2],s[3] 这个子串是回文的,那么只需要比较 s[0]和s[4]这两个元素是否相同,如果相同的话,这个字符串s 就是回文串。

  • 找到递归关系:判断一个子字符串(字符串的下表范围[i,j])是否回文,依赖于,子字符串(下表范围[i + 1, j - 1])) 是否是回文。

  • 布尔类型的dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。

  • 递推公式

    • s[i] != s[j] dp[i][j]一定是false。
    • s[i] == s[j]
      • 下标i 与 j相同,同一个字符例如a,当然是回文子串
      • 下标i 与 j相差为1,例如aa,也是回文子串
      • 下标:i 与 j相差大于1的时候,例如cabac,此时s[i]与s[j]已经相同了,我们看i到j区间是不是回文子串就看aba是不是回文就可以了,那么aba的区间就是 i+1 与 j-1区间,这个区间是不是回文就看dp[i + 1][j - 1]是否为true。
  • 遍历顺序

在这里插入图片描述

从下到上,从左到右遍历,这样保证dp[i + 1][j - 1]都是经过计算的。

class Solution {
    // 递归关系:判断一个子字符串(下标范围[i,j])是否回文
    // 依赖于 子字符串(下标范围[i-1,j-1])是否回文
    // dp[i][j] 表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。
    // s[i] != s[j]   false
    // s[i] == s[j]  
    // i == j  true
    // j - i == 1 true
    // j - i > 1 dp[i+1][j-1]是否为true。
    // 要从下到上,从左到右遍历,这样保证dp[i + 1][j - 1]都是经过计算的。
    public int countSubstrings(String s) {
        char[] chars = s.toCharArray();
        int len = chars.length;

        boolean[][] dp = new boolean[len][len];
        int result = 0;

        for (int i = len - 1; i >= 0; i--) {
            for (int j = i; j < len; j++) {
                if (chars[i] == chars[j]) {
                    if (j - i <= 1) {
                        result++;
                        dp[i][j] = true;
                    } else if (dp[i+1][j-1]) {
                        result++;
                        dp[i][j] = true;
                    }
                }
            }
        }
        return result;
    }
}
  • 中心拓展
  • 计算有多少个回文子串的最朴素方法就是枚举出所有的回文子串,而枚举出所有的回文字串又有两种思路,分别是:
    • 枚举出所有的子串,然后再判断这些子串是否是回文;
    • 枚举每一个可能的回文中心,然后用两个指针分别向左右两边拓展,当两个指针指向的元素相同的时候就拓展,否则停止拓展。
  • 假设字符串的长度为 n。我们可以看出前者会用 O ( n 2 ) O(n^2) O(n2) 的时间枚举出所有的子串 s [ l i . . . r i ] s[li ... ri] s[li...ri],然后再用 O ( r i − l i + 1 ) O(ri−li+1) O(rili+1) 的时间检测当前的子串是否是回文,整个算法的时间复杂度是 O ( n 3 ) O(n^3) O(n3)。而后者枚举回文中心的是 O ( n ) O(n) O(n) 的,对于每个回文中心拓展的次数也是 O ( n ) O(n) O(n) 的,所以时间复杂度是 O ( n 2 ) O(n^2) O(n2) 。所以我们选择第二种方法来枚举所有的回文子串。
  • 在实现的时候,我们需要处理一个问题,即如何有序地枚举所有可能的回文中心,我们需要考虑回文长度是奇数和回文长度是偶数的两种情况。如果回文长度是奇数,那么回文中心是一个字符;如果回文长度是偶数,那么中心是两个字符。当然你可以做两次循环来分别枚举奇数长度和偶数长度的回文,但是我们也可以用一个循环搞定。我们不妨写一组出来观察观察,假设 n = 4 n = 4 n=4,我们可以把可能的回文中心列出来:
编号 i i i回文中心左起始位置 l i li li回文中心右起始位置 r i ri ri
000
101
211
312
422
523
633
  • 长度为 n n n 的字符串会生成 2 n − 1 2^n−1 2n1 组回文中心 [ l i , r i ] [li, ri] [li,ri]
  • int left = i / 2, right = left + i % 2;
  • 这样我们只要从 0 到 2 n − 2 2^n−2 2n2遍历 i,就可以得到所有可能的回文中心,这样就把奇数长度和偶数长度两种情况统一起来了。
class Solution {
    public int countSubstrings(String s) {
        int len = s.length();
        int result = 0;
        if (s == null || len < 1) return 0;
        // 总共有 2 * len - 1个中心点
        for (int i = 0; i < 2 * len - 1; i++) {
            // 通过遍历每个回文中心,向两边扩散,并判断是否回文字串
            // 有两种情况,left == right,right = left + 1,这两种回文中心是不一样的
            int left = i / 2, right = left + i % 2;
            while (left >= 0 && right < len && s.charAt(left) == s.charAt(right)) {
                result++;
                left--;
                right++;
            }
        }
        return result;
    }
}

最长回文子序列

class Solution {
    // dp[i][j]  字符串 s 在 [i, j] 范围内最长的回文子序列长度
    // s[i] == s[j] dp[i][j] = dp[i+1][j-1] + 2
    // s[i] != s[j] dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1])
    // 首先要考虑当i 和j 相同的情况,从递推公式:dp[i][j] = dp[i + 1][j - 1] + 2; 
    // 可以看出 递推公式是计算不到 i 和j相同时候的情况。
    // 手动初始化一下,当i与j相同,那么dp[i][j]一定是等于1的
    // 从下到上遍历,这样才能保证下一行的数据是经过计算的。
    public int longestPalindromeSubseq(String s) {
        int len = s.length();
        int[][] dp = new int[len][len];

        for (int i = len - 1; i >= 0; i--) {
            dp[i][i] = 1; //  初始化
            for (int j = i + 1; j < len; j++) {
                if (s.charAt(i) == s.charAt(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][len-1];
    }
}
  • 17
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
最长回文子串是指在一个字符串中最长回文子序列回文是指正着读和倒着读都一样的字符串。动态规划是解决最长回文子串问题的一种常用方法。动态规划的思想是将问题分解成子问题,通过求解子问题的最优解来得到原问题的最优解。在最长回文子串问题中,我们可以使用一个二维数组dp[i][j]来表示从i到j的子串是否为回文子串。如果dp[i][j]为true,则表示从i到j的子串回文子串,否则不是。我们可以通过以下步骤来求解最长回文子串: 1. 初始化dp数组,将所有dp[i][i]都设置为true,表示单个字符是回文子串。 2. 遍历字符串s,从长度为2的子串开始,依次判断每个子串是否为回文子串。如果是,则将dp[i][j]设置为true。 3. 在遍历的过程中,记录最长回文子串的长度和起始位置。 4. 最后,通过起始位置和长度来截取最长回文子串。 下面是一个示例代码,可以帮助你更好地理解动态规划求解最长回文子串的过程: class Solution { public: string longestPalindrome(string s) { int len=s.size(); if(len<2) return s; bool dp[len][len];//布尔型,dp[i][j]表示从i到j是否构成回文 int max_count=1;//最大字串的长度 int start=0;//最长字串的起始位置 for(int j=0;j<len;j++) { for(int i=0;i<j;i++) { if(s[i]!=s[j]) dp[i][j]=false; else if((j-i)<3)//(j-1)-(i+1)+1<2表示dp[i][j]的最大字串长度为1 dp[i][j]=true; else { dp[i][j]=dp[i+1][j-1]; } if((j-i+1)>max_count&&dp[i][j]) { max_count=j-i+1; start=i; } } } return s.substr(start,max_count);//截取字符串 } };
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值