LeetCode5. 最长回文子串(java + 暴力 + 中心扩展 + 动态规划 + 马拉车算法)

最长回文子串

1. 题目描述:

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

示例1:

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

示例2:

输入: "cbbd"
输出: "bb"
2. 题目分析:

leetcode官方题解写得挺好的,以下就参考它的一些做法来写一下。

​ 以下的几种方法多多少少都是利用了回文的对称性

  • 暴力法

    暴力法的思路是比较简单的,主要是提取出每一个子串,类似于冒泡那样提取,然后再看每一个子串是不是回文,如果是回文,那就看它的长度是不是比已知的回文长度要长,是的话就更新回文长度最大值、起始点、终止点。总体来讲还是比较简单的,下面给出检测是否是回文的函数,利用回文的对称性

        public boolean isPlalindrome(String s) {
            int len = s.length();
            for(int i = 0; i < len / 2; i++) {
                if(s.charAt(i) != s.charAt(len - 1 - i)) {
                    return false;
                }
            }
            return true;
        }
    
  • 中心扩展算法

    回文的一个很重要的特性就是对称性,所以回文中心的两边是相等的,从中心扩展开,每一个字符串都可能有2n - 1个中心点,比如“baabcd”以单个字符为中心就有n个,以两个字符为中心就是aa,就有n - 1个。

        //中心扩展函数
    	public int center_expand(String s, int left, int right){
          while(left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)){
                left--;
              //向左边扩展
                right++;
                //向右边扩展
            }
            int len = right - left - 1;
            //记录长度
            return len;
        }
        
        //查找最长回文子串
        public String longestPalindrome(String s) {
            int len = s.length();
            if(len <= 1) return s; 
            //字符串为空或者长度等于1,直接返回
            int max = 0;
            //记录字符串最大值
            String ret = null;
            //存放回文
            for(int i = 0; i < len; i++){
                int len1 = center_expand(s, i, i);
                //以i为中心,i为左,i也为右
                int len2 = center_expand(s, i, i + 1);
                //以(i, i + 1)为中心,i为左,i+1为右
                int len3 = Math.max(len1, len2);
                //取出最大长度
                if(len3 > max){
                    int L = i - (len3 - 1) / 2; 
                    //最大回文长度的左边
                    int R = i + len3 / 2;
                    //最大回文长度的右边
                    max = len3;
                    //回文的最大长度
                    ret = s.substring(L, R + 1);
                    //将回文存放在ret中
                }
            }
            return ret;
        }
    
  • 动态规划算法 (看这个大神写的)

    动态规划算法两个步骤:

    1. 定义“状态”。
    2. 列出状态转换方程。

    1、定义 “状态”,这里 “状态”是一个布尔型二维数组。

    dp[l, r] 表示子串 s[l, r](包括区间左右端点)是否构成回文串,是一个二维布尔型数组。即如果子串 s[l, r] 是回文串,那么 dp[l, r]= true。

    2、状态转移方程:

    dp[l, r] = (s[l] == s[r] and (r - l <= 2 or dp[l + 1, r - 1]))
    

    首先s[l, r]子串是回文的话,字符s[l] 必须等于 s[r],即最左端 == 最右端,利用了回文的对称性

    其次,如果字符串长度比较短,比如aba或者aa,既然s[l] == s[r]都是a,那就不用再多想了,这个一定是回文。这个短字符串满足r - l <= 2

    再者,如果字符串比较长呢,不满足r - l <= 2,比如abba,已知s[l] == s[r] == a还不行,还需要

    dp[l + 1, r - 1] = true成立

    具体该怎么循环,看哪个先有,哪个后有,是先有dp[l + 1, r - 1],再有dp[l, r],所以所以这次的循环是 r 不动,l 由上往下动。两个循环,相当于冒泡的写法,可以遍历每一个子串的起止点,为每一个子串编写状态,看下图。

        public static String longestPalindrome(String s) {
            int len = s.length();
            if(len <= 1) return s;
            //字符串为空或者长度等于1,直接返回
            boolean[][] dp = new boolean[len][len];
            //记录每一个子串的状态,dp[i][j]=true表明,以i为起点,j为终点的子串是回文
            int max = 0;
            //最大回文长度
            String ret = s.substring(0, 1);
            //存放回文,初始化为s的第一个字符,假如该字符串没有回文,那就直接返回字符串的第一个字符
            for(int r = 1; r < len; r++){
                for(int l = 0; l <  r; l++){
                    //这两个循环很关键,
                    if(s.charAt(r) == s.charAt(l) && (r - l <= 2 || dp[l+1][r-1])){
                        dp[l][r] = true;
                        if(max < r - l + 1) {
                            max = r - l + 1;
                            //max值更新
                            ret = new String(s.substring(l, r + 1));
                            //存放新的回文
                        }
                    }
                }
            }
            return ret;
        }
    

    结合代码,以babad为例,分析动态规划实验

    ④的结果建立在③上,⑦的结果建立在⑤上,对角线的关系;所以这次的循环是 r 不动,l 由上往下动
    以字符串“b a b a d”为例的动态规划过程

  • ManACher算法

    ​ 这个算法也叫马拉车算法,额…,是专门用于解决这个问题的,所以…,很强。

    ​ 我是看了公众号CSDN的文章如何找到字符串中的最长回文子串才会的。它的核心主要也是中心扩展算法,为了避免讨论奇偶的情况,可以在字符串的每一个空隙中加上’#‘号,在LeetCode的求解两个有序数组的中位数,也用到了这种预处理。

    ​ 常规的中心扩展算法中,每一个中心点都要进行中心扩展,马拉车算法改进了这一点,并不是每个中心点都需要中心扩展的,假如字符串dabcgcbaf,我已经由d 到 g 都逐个进行了中心扩展,现得出第一个 c 的回文半径是0 (没有以 c 为中心的回文), 而 g 它的回文半径是3 (以 g 为中心,向左走3步,向右走3步,abc g cba),那么还用对第二个 c 进行中心扩展吗?不用了,因为第一个和第二个 c 在以 g 为中心的回文范围内,利用对称性,既然第一个 c 的回文半径是0,那么第二个 c 也是0。

    以下图片来自CSDNManacher算法详解及模板(求解最长回文串)

    • **已知r1R不需要对r进行中心扩展的情况 1:**从左往右开始对每个点进行中心扩展,设r1和R的回文半径已由中心扩展得到,到对 r 进行中心扩展的时候,发现右边界rightSide (R能到的最右端)大于r,根据对称性得leftCenter = 2 * rightSideCenter - i; i 就是 中心点 r ,rightSideCenter就是中心点R,leftCenter就是中心点 r1,得到leftCenter之后,halfLenArr[leftCenter]是中心点r1的回文半径,r 的回文半径halfLenArr[i] = halfLenArr[leftCenter],下图中i + halfLenArr[i] < rightSide即关于 r 的回文半径就已经由halfLenArr[leftCenter]无需进行中心扩展。

      img

    • **已知r1R不需要对r进行中心扩展的情况 2:**跟 1 的情况差不多

      img

    • **已知r1R需要对r需要进行中心扩展:**需要对 r 的黄色部分进行探索

      img

    下面贴上大神写的代码:

        // 预处理字符串,在两个字符之间加上#
        private String preHandleString(String s) {
            StringBuffer sb = new StringBuffer();
            int len = s.length();
            sb.append('#');
            for(int i = 0; i < len; i++) {
                sb.append(s.charAt(i));
                sb.append('#');
            }
            return sb.toString();
        }
        
        public String findLongestPlalindromeString(String s) {
            // 先预处理字符串
            String str = preHandleString(s);
            // 处理后的字串长度
            int len = str.length();
            // 右边界
            int rightSide = 0;
            // 右边界对应的回文串中心
            int rightSideCenter = 0;
            // 保存以每个字符为中心的回文长度一半(向下取整)
            int[] halfLenArr = new int[len];
            // 记录回文中心
            int center = 0;
            // 记录最长回文长度
            int longestHalf = 0;
            for(int i = 0; i < len; i++) {
                // 是否需要中心扩展
                boolean needCalc = true;
                // 如果在右边界的覆盖之内
                if(rightSide > i) {
                    // 根据对称性,计算相对rightSideCenter的对称位置
                    int leftCenter = 2 * rightSideCenter - i;
                    // 根据对称性,得到这两个的回文半径是一样的
                    halfLenArr[i] = halfLenArr[leftCenter];
                    //如果超过或等于右边界,即i + halfLenArr[i] >= rightside都要进行中心扩展
                    if(i + halfLenArr[i] >= rightSide) {
                        halfLenArr[i] = rightSide - i;
                        //先让它暂且等于右边界内的长度,对于这个长度之外部分(上图的黄色部分)再进行扩展
                    }
                    // 如果根据已知条件计算得出的最长回文小于右边界,则不需要扩展了
                    if(i + halfLenArr[leftCenter] < rightSide) {
                        // 直接推出结论
                        needCalc = false;
                    }
                }
                // 中心扩展
                if(needCalc) {
                    while(i - 1 - halfLenArr[i] >= 0 && i + 1 + halfLenArr[i] < len) {
                        if(str.charAt(i + 1 + halfLenArr[i]) == str.charAt(i - 1 - halfLenArr[i])) {
                            halfLenArr[i]++;
                        } else {
                            break;
                        }
                    }
                    // 更新右边界及中心
                    rightSide = i + halfLenArr[i];
                    rightSideCenter = i;
                    // 记录最长回文串
                    if(halfLenArr[i] > longestHalf) {
                        center = i;
                        longestHalf = halfLenArr[i];
                    }
                }
            }
            // 去掉之前添加的#
            StringBuffer sb = new StringBuffer();
            for(int i = center - longestHalf + 1; i <= center + longestHalf; i += 2) {
                sb.append(str.charAt(i));
            }
            return sb.toString();
        }
        
    
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值