Leetcode——回文子串 / 最长回文子串 / 最长回文子序列

1. 回文子串

在这里插入图片描述

(1)中心扩展

  • 比如对一个字符串 ababa,选择最中间的 a 作为中心点,往两边扩散,第一次扩散发现 left 指向的是 b,right 指向的也是 b,所以是回文串,继续扩散,同理 ababa 也是回文串。
  • 这个是确定了一个中心点后的寻找的路径,然后我们只要寻找到所有的中心点,问题就解决了。
  • 中心点一共有多少个呢?看起来像是和字符串长度相等,但你会发现,如果是这样,上面的例子永远也搜不到 abab
  • 中心点不能只有单个字符构成,还要包括两个字符,比如上面这个子串 abab,就可以有中心点 ba 扩展一次得到,所以最终的中心点由 2 * len - 1 个,分别是 len 个单字符和 len - 1 个双字符。
class Solution{
    public int countSubstrings(String s) {

        int ans = 0;
        for (int center = 0; center < 2 * s.length() - 1; center++) {
            // left和right指针和中心点的关系是?
            // 首先是left,有一个很明显的2倍关系的存在,其次是right,可能和left指向同一个(偶数时),也可能往后移动一个(奇数)
            // 大致的关系出来了,可以选择带两个特殊例子进去看看是否满足。
            int left = center / 2;
            int right = left + center % 2;

            while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
                ans++;
                left--;
                right++;
            }
        }
        return ans;
    }
}

这个解法也同样适用于下面一个题:最长回文子串

(2)动规

  • 状态:dp[i][j] 表示字符串s在[i,j]区间的子串是否是一个回文串。
  • 状态转移方程:当 s[i] == s[j] && (j - i < 2 || dp[i + 1][j - 1]) 时,dp[i][j]=true,否则为false

解释一下:

  • 当只有一个字符时,比如 a 自然是一个回文串。
  • 当有两个字符时,如果是相等的,比如 aa,也是一个回文串。
  • 当有三个及以上字符时,比如 ababa 这个字符记作串 1,把两边的 a 去掉,也就是 bab 记作串 2,可以看出只要串2是一个回文串,那么左右各多了一个 a 的串 1 必定也是回文串。所以当 s[i]==s[j] 时,自然要看 dp[i+1][j-1] 是不是一个回文串。
class Solution {
    public int countSubstrings(String s) {
        // 动态规划法
        boolean[][] dp = new boolean[s.length()][s.length()];
        int ans = 0;

        for (int j = 0; j < s.length(); j++) {
            for (int i = 0; i <= j; i++) {
                //dp[i + 1][j - 1] : 向里面缩
                if (s.charAt(i) == s.charAt(j) && (j - i < 2 || dp[i + 1][j - 1])) {
                    dp[i][j] = true;
                    ans++;
                }
            }
        }
        return ans;
    }
}

2. 最长回文子串

给你一个字符串 s,找到 s 中最长的回文子串。

示例 1:
输入:s = “babad”
输出:“bab”
解释:“aba” 同样是符合题意的答案。

示例 2:
输入:s = “cbbd”
输出:“bb”

示例 3:
输入:s = “a”
输出:“a”

示例 4:
输入:s = “ac”
输出:“a”

提示:

1 <= s.length <= 1000
s 仅由数字和英文字母(大写和/或小写)组成

(1)暴力法

class Solution {
    public String longestPalindrome(String s) {
        String ans = "";
        int max = 0;
        int len = s.length();
        for (int i = 0; i < len; i++)
            for (int j = i + 1; j <= len; j++) {
                String test = s.substring(i, j);
                if (isPalindromic(test) && test.length() > max) {
                    ans = s.substring(i, j);
                    max = Math.max(max, ans.length());
                }
            }
        return ans;
    }


    public boolean isPalindromic(String s) {
		int len = s.length();
		for (int i = 0; i < len / 2; i++) {
			if (s.charAt(i) != s.charAt(len - i - 1)) {
				return false;
			}
		}
		return true;
	}
}

(2)动态规划

dp[i][i]=1; //单个字符是回文串
dp[i][i+1]=1 if s[i]=s[i+1]; //连续两个相同字符是回文串

class Solution {
    // 动态规划法
    public String longestPalindrome(String s) {
        int len = s.length();
        if (len < 2) {
            return s;
        }
        int maxLen = 1;
        int begin = 0;
        char[] cs = s.toCharArray();

        // dp[i][j]:表示s[i][j]是否是回文串
        boolean[][] dp = new boolean[len][len];

        // 初始化:单独一个字符肯定是回文子串
        for (int i = 0; i < len; i++) {
            dp[i][i] = true;
        }
        
        // 经验:dp区域是正方形的话,通常左下角区域无效不需要再填,因为走过的区域不用再走
        for (int j = 1; j < len; j++) { // 上三角区域,按列从上到下填
            for (int i = 0; i < j; i++) {
                // 首尾不相等时,必不是回文串
                if (cs[i] != cs[j]) {
                    dp[i][j] = false;
                } else {
                    // 首尾相等时,有2种情况
                    // 情况1:s[i...j]长度不超过3,不用检查必为回文串
                    // 情况2:s[i...j]长度大于3,由s[i+1...j-1]来判断
                    dp[i][j] = j - i + 1 <= 3 || dp[i + 1][j - 1];
                }
                // 更新max和begin
                if (dp[i][j] && j - i + 1 > maxLen) {
                    maxLen = j - i + 1;
                    begin = i;
                }
            }
        }
        return s.substring(begin, begin + maxLen);
    }
}

这样写或许更容易理解(更清晰):

class Solution {
    public String longestPalindrome(String s) {
        int len = s.length();
        if (len < 2)
            return s;
        int maxStart = 0;  //最长回文串的起点
        int maxEnd = 0;    //最长回文串的终点
        int maxLen = 1;  //最长回文串的长度
        char[] cs = s.toCharArray();

        // dp[i][j]:表示s[i][j]是否是回文串
        boolean[][] dp = new boolean[len][len];
        
        for(int right = 1; right < len; right++) {
            for(int left = 0; left < right; left++) {               
                if (cs[left] == cs[right] && (right - left <= 2 || dp[left + 1][right - 1])) {
                    dp[left][right] = true;
                    // 更新max和begin
                    if (right - left + 1 > maxLen) {
                        maxLen = right - left + 1;
                        maxStart = left;
                        maxEnd = right;
                    }
                }
                
            } 
        }
        return s.substring(maxStart, maxEnd + 1);
    }
}

(3)中心扩展法

回文中心的两侧互为镜像。因此,回文可以从他的中心展开,并且只有 2n-1 个这样的中心(一个元素为中心的情况有 n 个,两个元素为中心的情况有 n-1 个)

class Solution {
    public String longestPalindrome(String s) {
        if (s == null || s.length() < 1){
            return "";
        }

        // 初始化最大回文子串的起点,最大回文子串长度
        int start = 0;
        int maxLen = 1;
        // 遍历每个位置,当做中心位
        for (int i = 0; i < s.length(); i++) {
            // 分别拿到奇数偶数的回文子串长度
            int len_odd = expandCenter(s,i,i);
            int len_even = expandCenter(s,i,i + 1);
            // 对比最大的长度
            int len = Math.max(len_odd,len_even);
			
			// i为当前回文的中心:奇数回文为中心下标,偶数回文为一条虚线(i,i + 1)
            // 计算对应最大回文子串的起点和终点,最长子串长度
            //如果长度比原本记录的长度大了,说明有更长的回文出现了,所以要记录一下回文的下标
            if (len > maxLen){
            	// 根据i和maxLen算start下标
                // 奇数:i-maxLen/2
                // 偶数:i-maxLen/2+1
                // 统一:i-(maxLen-1)/2
                //这里为什么要i-1?,这里说明一下,因为for循环是从0开始的,
				//如果是奇数回文,假设有个回文是3个,那么len=3,此时中心i是下标1(从0开始),那么(len-1)/2和len/2的结果都是1,因为整型会向下取整
				//但是如果是偶数回文,假设有个回文是4个,那么len=4,此时的中心是一条虚线,但是i的位置却在1,(因为S是从左向右遍历的,如果从右向左,
				//i的位置就会在2.)这时候,(len-1)/2=1,len/2=2.很明显为了保证下标正确,我们需要的是(len-1)/2.原因其实是i在中心线的左边一位,
				//所以要少减个1.
                maxLen = len;
                start = i - (len - 1)/2;
            }
        }
        
        return s.substring(start, start + maxLen);		//s.substring(3,5) 切割 [3,5),左闭右开
    }
    
    private int expandCenter(String s,int left,int right){
        // left = right 的时候,此时回文中心是一个字符,回文串的长度是奇数
        // right = left + 1 的时候,此时回文中心是一个空隙,回文串的长度是偶数
        // 跳出循环的时候恰好满足 s.charAt(left) != s.charAt(right)
        while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)){
            left--;
            right++;
        }
        //这里其实是right-left+1-2,意思就是right-left+1是本来的长度,但是由于上面最后一次判断肯定false,所以最后一次left--和right++
		//其实不属于回文的一部分,所以要减去2
        return right - left - 1;
    }
}

简化一下代码:

class Solution {
    public String longestPalindrome(String s) {
        if (s == null || s.length() < 1){
            return "";
        }
        String result = "";
        int len = s.length();

        for (int center = 0; center < len * 2 - 1; center++) {
            int left = center / 2;
            int right = left + center % 2;
            while (left >= 0 && right < len && s.charAt(left) == s.charAt(right)) {
                String tmp = s.substring(left, right + 1);
                if (tmp.length() > result.length())
                    result = tmp;
                left--;
                right++;
            }
        }
        return result;
    }
}

3. 最长回文子序列

在这里插入图片描述

(1)动规

状态:

  • dp[i][j] 表示 s 的第 i 个字符到第 j 个字符组成的子串中,最长的回文序列长度是多少。

转移方程:

  • 如果 s 的第 i 个字符和第 j 个字符相同的话: dp[i][j] = dp[i + 1][j - 1] + 2
  • 如果 s 的第 i 个字符和第 j 个字符不同的话: dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])
  • 然后注意遍历顺序,i 从最后一个字符开始往前遍历,j 从 i + 1 开始往后遍历,这样可以保证每个子问题都已经算好了。
    • 从递推公式dp[i][j] = dp[i + 1][j - 1] + 2 和 dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]) 可以看出,dp[i][j]是依赖于dp[i + 1][j - 1] 和 dp[i + 1][j],
    • 如果我们想求dp[i][j],那么其他3个必须都是已知的,很明显从上往下遍历是不行的,我们只能让i从最后一个字符往前遍历,j从i的下一个开始遍历,最后只需要返回dp[0][length - 1]即可。
      在这里插入图片描述

初始化:

  • dp[i][i] = 1 单个字符的最长回文序列是 1
class Solution {
    public int longestPalindromeSubseq(String s) {
        int len = s.length();
        int[][] dp = new int[len][len];

        //这里i要从最后一个开始遍历
        for(int i = len-1; i >= 0; --i) {
            for(int j = i; j < len; ++j) {
                //单个字符也是一个回文串
                if(i == j) {
                    dp[i][j] = 1;
                    continue;
                } 
                //j从i的下一个开始
                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];
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Yawn__

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值