• [Hot100]回文子串 与 最长回文子串


    647. 回文子串
    中等题
    回文字符串 是正着读和倒过来读一样的字符串。
    子字符串 是字符串中的由连续字符组成的一个序列。

    ①动态规划
    状态:dp[i][j] 表示字符串s在[i,j]区间的子串是否是一个回文串。
    状态转移方程:当 s[i] == s[j] && (j - i < 2 || dp[i + 1][j - 1]) 时,dp[i][j] = true,否则为dp[i][j] = 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 res = 0;
            for(int i=s.length()-1;i>=0;i--){
                for(int j = i;j<s.length();j++){
                    if(s.charAt(i) == s.charAt(j) && (j-i<=1 || dp[i+1][j-1]==true)){
                        res++;
                        dp[i][j] = true;
                    }
                }
            }
            return res;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    ②中心扩展法
    这是一个比较巧妙的方法,实质的思路和动态规划的思路类似。

    比如对一个字符串 ababa,选择最中间的 a 作为中心点,往两边扩散,第一次扩散发现 left 指向的是 b,right 指向的也是 b,所以是回文串,继续扩散,同理 ababa 也是回文串。

    这个是确定了一个中心点后的寻找的路径,然后我们只要寻找到所有的中心点,问题就解决了。

    中心点一共有多少个呢?
    看起来像是和字符串长度相等,但你会发现,如果是这样,上面的例子永远也搜不到 abab,想象一下单个字符的哪个中心点扩展可以得到这个子串?似乎不可能。所以中心点不能只有单个字符构成,还要包括两个字符,比如上面这个子串 abab,就可以有中心点 ba 扩展一次得到,分别是 len 个单字符和 len - 1 个双字符,所以最终的中心点有 (len + len -1) = 2 * len - 1 个。

    如果上面看不太懂的话,还可以看看下面几个问题:

    为什么有 2 * len - 1 个中心点?

    • aba 有5个中心点,分别是 a、b、a、ab、ba
    • abba 有7个中心点,分别是 a、b、b、a、ab、bb、ba
      什么是中心点?
      中心点即 left 指针和 right 指针初始化指向的地方,可能是一个也可能是两个
      为什么不可能是三个或者更多?
      因为 3 个可以由 1 个扩展一次得到,4 个可以由两个扩展一次得到
    class Solution {
        public int countSubstrings(String s) {
            int n = s.length();
            int count = 0;
            //遍历每个位置
            for(int i=0;i<n;i++){
                //中心可能是1个字符 也可能是2个字符
                for(int j=0;j<=1;j++){
                    int l = i;
                    int r = i + j;
                    while(l >= 0 && r < n && s.charAt(l) == s.charAt(r)){
                        l--;
                        r++;
                        count++;
                    }
                }
            }
            return count;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    5. 最长回文子串

    中等题

    ①动态规划

    我们用 P(i,j)表示字符串 s 的第 i 到 j 个字母组成的串(下文表示成 s[i:j])是否为回文串:
    在这里插入图片描述
    这里的「其它情况」包含两种可能性:

    s[i, j]本身不是一个回文串;
    i > j,此时 s[i, j]、 本身不合法。

    状态转移方程:
    P ( i , j ) = P ( i + 1 , j − 1 ) 且 ( S i = = S j ) P(i,j) = P(i+1,j-1) 且 (S_i==S_j) P(i,j)=P(i+1,j1)(Si==Sj)

    只有 s[i+1:j-1] 是回文串,并且 s 的第 i 和 j 个字母相同时,s[i:j] 才会是回文串。

    • 对于长度为 1 的子串,它显然是个回文串;
    • 对于长度为 2 的子串,只要它的两个字母相同,它就是一个回文串。

    因此我们就可以写出动态规划的边界条件:
    在这里插入图片描述
    最终的答案即为所有 P(i,j)=truej−i+1(即子串长度)的最大值。

    注意:在状态转移方程中,我们是从长度较短的字符串向长度较长的字符串进行转移的,因此一定要注意动态规划的循环顺序。

    public class Solution {
    
        public String longestPalindrome(String s) {
            int len = s.length();
            if (len < 2) {
                return s;
            }
    
            int maxLen = 1;
            int begin = 0;
            // dp[i][j] 表示 s[i..j] 是否是回文串
            boolean[][] dp = new boolean[len][len];
            // 初始化:所有长度为 1 的子串都是回文串
            for (int i = 0; i < len; i++) {
                dp[i][i] = true;
            }
    
            char[] charArray = s.toCharArray();
            // 递推开始
            // 先枚举子串长度
            for (int L = 2; L <= len; L++) {
                // 枚举左边界,左边界的上限设置可以宽松一些
                for (int i = 0; i < len; i++) {
                    // 由 L 和 i 可以确定右边界,即 j - i + 1 = L 得
                    int j = L + i - 1;
                    // 如果右边界越界,就可以退出当前循环
                    if (j >= len) {
                        break;
                    }
    
                    if (charArray[i] != charArray[j]) {
                        dp[i][j] = false;
                    } else {
                        if (j - i < 3) {
                            dp[i][j] = true;
                        } else {
                            dp[i][j] = dp[i + 1][j - 1];
                        }
                    }
    
                    // 只要 dp[i][L] == true 成立,就表示子串 s[i..L] 是回文,此时记录回文长度和起始位置
                    if (dp[i][j] && j - i + 1 > maxLen) {
                        maxLen = j - i + 1;
                        begin = i;
                    }
                }
            }
            return s.substring(begin, begin + maxLen);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    ②中心扩展法:

    状态转移链:

    P ( i , j ) ← P ( i + 1 , j − 1 ) ← P ( i + 2 , j − 2 ) ← ⋯ ← 某 一 边 界 情 况 P(i,j)←P(i+1,j−1)←P(i+2,j−2)←⋯←某一边界情况 P(i,j)P(i+1,j1)P(i+2,j2)
    所有的状态在转移的时候的可能性都是唯一的。也就是说,我们可以从每一种边界情况开始「扩展」,也可以得出所有的状态对应的答案。
    边界情况即为子串长度为 1或 2 的情况。
    本质:枚举所有的「回文中心」并尝试「扩展」,直到无法扩展为止,此时的回文串长度即为此「回文中心」下的最长回文串长度。我们对所有的长度求出最大值,即可得到最终的答案。

    class Solution {
        public String longestPalindrome(String s) {
            if (s == null || s.length() < 1) {
                return "";
            }
            int start = 0, end = 0;
            for (int i = 0; i < s.length(); i++) {
                int len1 = expandAroundCenter(s, i, i);
                int len2 = expandAroundCenter(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);
        }
    
        public int expandAroundCenter(String s, int left, int right) {
            while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
                --left;
                ++right;
            }
            return right - left - 1;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    参考:https://leetcode.cn/problems/longest-palindromic-substring/solution/zhong-xin-kuo-san-dong-tai-gui-hua-by-liweiwei1419/

    public class Solution {
    
        public String longestPalindrome(String s) {
            int len = s.length();
            if(len < 2) return s;
            
            int maxLen = 0;
            // 数组第一位记录起始位置,第二位记录长度
            int[] res = new int[2];
            for (int i = 0; i < s.length() - 1; i++) {
                int[] odd = centerSpread(s, i, i);
                int[] even = centerSpread(s, i, i + 1);
                int[] max = odd[1] > even[1] ? odd : even;
                if (max[1] > maxLen) {
                    res = max;
                    maxLen = max[1];
                }
            }
            return s.substring(res[0], res[0] + res[1]);
        }
    
        private int[] centerSpread(String s, int left, int right) {
            int len = s.length();
            while (left >= 0 && right < len) {
                if (s.charAt(left) == s.charAt(right)) {
                    left--;
                    right++;
                } else {
                    break;
                }
            }
            return new int[]{left + 1, right - left - 1};
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    中心扩散的方法,其实做了很多重复计算。动态规划就是为了减少重复计算的问题。动态规划听起来很高大上。其实说白了就是空间换时间,将计算结果暂存起来,避免重复计算。
    作用和工程中用 redis 做缓存有异曲同工之妙。

  • 相关阅读:
    x264编译
    SAP-MM-查找采购订单的创建和修改日期
    npm更换成淘宝镜像源及cnpm使用
    计算文本相似度的几种方法以及实现原理
    27岁了,目前从事软件测试,听说测试前途是IT里最差的,是这样吗?
    使用 Tailwind CSS 自定义基础样式层
    Android自定义控件(一) 可滑动的进度条
    【一周安全资讯1014】交通运输部发布《公路工程设施支持自动驾驶技术指南》;多地网信办对违反数据安全法规企业作出行政处罚
    2022年湖北天门中级工程师职称评审要求是什么呢?如何评审呢?甘建二
    基于java的图书馆座位系统的设计与实现
  • 原文地址:https://blog.csdn.net/m0_58058653/article/details/125503124