• 最长回文子串


    • 以下解法中「暴力算法」是基础,「动态规划」必须掌握,「中心扩散」方法要会写;

    方法一:暴力匹配 (Brute Force)
    根据回文子串的定义,枚举所有长度大于等于 22 的子串,依次判断它们是否是回文;
    在具体实现时,可以只针对大于“当前得到的最长回文子串长度”的子串进行“回文验证”;
    在记录最长回文子串的时候,可以只记录“当前子串的起始位置”和“子串长度”,不必做截取。这一步我们放在后面的方法中实现。
    说明:暴力解法时间复杂度高,但是思路清晰、编写简单。由于编写正确性的可能性很大,可以使用暴力匹配算法检验我们编写的其它算法是否正确。优化的解法在很多时候,是基于“暴力解法”,以空间换时间得到的,因此思考清楚暴力解法,分析其缺点,很多时候能为我们打开思路。

    参考代码 1:Java 代码正常运行,C++ 代码超出内存限制,Python 代码超时。

    public class Solution {
    
        public String longestPalindrome(String s) {
            int len = s.length();
            if (len < 2) {
                return s;
            }
    
            int maxLen = 1;
            int begin = 0;
            // s.charAt(i) 每次都会检查数组下标越界,因此先转换成字符数组
            char[] charArray = s.toCharArray();
    
            // 枚举所有长度大于 1 的子串 charArray[i..j]
            for (int i = 0; i < len - 1; i++) {
                for (int j = i + 1; j < len; j++) {
                    if (j - i + 1 > maxLen && validPalindromic(charArray, i, j)) {
                        maxLen = j - i + 1;
                        begin = i;
                    }
                }
            }
            return s.substring(begin, begin + maxLen);
        }
    
        /**
         * 验证子串 s[left..right] 是否为回文串
         */
        private boolean validPalindromic(char[] charArray, int left, int right) {
            while (left < right) {
                if (charArray[left] != charArray[right]) {
                    return false;
                }
                left++;
                right--;
            }
            return true;
        }
    }

    复杂度分析:时间复杂度:O(N^3)O(N3),这里 NN 是字符串的长度,枚举字符串的左边界、右边界,然后继续验证子串是否是回文子串,这三种操作都与 NN 相关;

    空间复杂度:O(1)O(1),只使用到常数个临时变量,与字符串长度无关。

    方法三:中心扩散法
    暴力法采用双指针两边夹,验证是否是回文子串。

    除了枚举字符串的左右边界以外,比较容易想到的是枚举可能出现的回文子串的“中心位置”,从“中心位置”尝试尽可能扩散出去,得到一个回文串。

    因此中心扩散法的思路是:遍历每一个索引,以这个索引为中心,利用“回文串”中心对称的特点,往两边扩散,看最多能扩散多远。

    枚举“中心位置”时间复杂度为 O(N)O(N),从“中心位置”扩散得到“回文子串”的时间复杂度为 O(N)O(N),因此时间复杂度可以降到 O(N^2)O(N
    2
    )。

    在这里要注意一个细节:回文串在长度为奇数和偶数的时候,“回文中心”的形式是不一样的。

    奇数回文串的“中心”是一个具体的字符,例如:回文串 "aba" 的中心是字符 "b";
    偶数回文串的“中心”是位于中间的两个字符的“空隙”,例如:回文串串 "abba" 的中心是两个 "b" 中间的那个“空隙”。

    我们看一下一个字符串可能的回文子串的中心在哪里?

     

    方法二:动态规划
    下面是「动态规划』问题的思考路径,供大家参考。

    特别说明:

    以下「动态规划」的解释只帮助大家了解「动态规划」问题的基本思想;
    「动态规划」问题可以难到非常难,在学习的时候建议不要钻到特别难的问题中去;
    掌握经典的动态规划问题的解法,理解状态的定义的由来、会列出状态转移方程;
    然后再配合适当难度的问题的练习;
    有时间和感兴趣的话可以做一些不太常见的类型的问题,拓宽视野;
    「动态规划」讲得比较好的经典书籍是《算法导论》。
    提示:右键「在新便签页打开图片」可查看大图。

    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    我们可以设计一个方法,兼容以上两种情况:

    1、如果传入重合的索引编码,进行中心扩散,此时得到的回文子串的长度是奇数;

    2、如果传入相邻的索引编码,进行中心扩散,此时得到的回文子串的长度是偶数。

    具体编码细节在以下的代码的注释中体现。

    public class Solution {
    
        public String longestPalindrome(String s) {
            int len = s.length();
            if (len < 2) {
                return s;
            }
            int maxLen = 1;
            String res = s.substring(0, 1);
            // 中心位置枚举到 len - 2 即可
            for (int i = 0; i < len - 1; i++) {
                String oddStr = centerSpread(s, i, i);
                String evenStr = centerSpread(s, i, i + 1);
                String maxLenStr = oddStr.length() > evenStr.length() ? oddStr : evenStr;
                if (maxLenStr.length() > maxLen) {
                    maxLen = maxLenStr.length();
                    res = maxLenStr;
                }
            }
            return res;
        }
    
        private String centerSpread(String s, int left, int right) {
            // left = right 的时候,此时回文中心是一个字符,回文串的长度是奇数
            // right = left + 1 的时候,此时回文中心是一个空隙,回文串的长度是偶数
            int len = s.length();
            int i = left;
            int j = right;
            while (i >= 0 && j < len) {
                if (s.charAt(i) == s.charAt(j)) {
                    i--;
                    j++;
                } else {
                    break;
                }
            }
            // 这里要小心,跳出 while 循环时,恰好满足 s.charAt(i) != s.charAt(j),因此不能取 i,不能取 j
            return s.substring(i + 1, j);
        }
    }

    1、思考状态(重点)

    状态的定义,先尝试「题目问什么,就把什么设置为状态」;
    然后思考「状态如何转移」,如果「状态转移方程」不容易得到,尝试修改定义,目的依然是为了方便得到「状态转移方程」。
    「状态转移方程」是原始问题的不同规模的子问题的联系。即大问题的最优解如何由小问题的最优解得到。

    2、思考状态转移方程(核心、难点)

    状态转移方程是非常重要的,是动态规划的核心,也是难点;

    常见的推导技巧是:分类讨论。即:对状态空间进行分类;

    归纳「状态转移方程」是一个很灵活的事情,通常是具体问题具体分析;

    除了掌握经典的动态规划问题以外,还需要多做题;

    如果是针对面试,请自行把握难度。掌握常见问题的动态规划解法,理解动态规划解决问题,是从一个小规模问题出发,逐步得到大问题的解,并记录中间过程;

    「动态规划」方法依然是「空间换时间」思想的体现,常见的解决问题的过程很像在「填表」。

    3、思考初始化

    初始化是非常重要的,一步错,步步错。初始化状态一定要设置对,才可能得到正确的结果。

    角度 1:直接从状态的语义出发;

    角度 2:如果状态的语义不好思考,就考虑「状态转移方程」的边界需要什么样初始化的条件;

    角度 3:从「状态转移方程」方程的下标看是否需要多设置一行、一列表示「哨兵」(sentinel),这样可以避免一些特殊情况的讨论。

    4、思考输出

    有些时候是最后一个状态,有些时候可能会综合之前所有计算过的状态。

    5、思考优化空间(也可以叫做表格复用)

    「优化空间」会使得代码难于理解,且是的「状态」丢失原来的语义,初学的时候可以不一步到位。先把代码写正确是更重要;
    「优化空间」在有一种情况下是很有必要的,那就是状态空间非常庞大的时候(处理海量数据),此时空间不够用,就必须「优化空间」;
    非常经典的「优化空间」的典型问题是「0-1 背包」问题和「完全背包」问题。
    (下面是这道问题「动态规划」方法的分析)

    这道题比较烦人的是判断回文子串。因此需要一种能够快速判断原字符串的所有子串是否是回文子串的方法,于是想到了「动态规划」。

    「动态规划」的一个关键的步骤是想清楚「状态如何转移」。事实上,「回文」天然具有「状态转移」性质。

    一个回文去掉两头以后,剩下的部分依然是回文(这里暂不讨论边界情况);
    依然从回文串的定义展开讨论:

    如果一个字符串的头尾两个字符都不相等,那么这个字符串一定不是回文串;
    如果一个字符串的头尾两个字符相等,才有必要继续判断下去。
    如果里面的子串是回文,整体就是回文串;
    如果里面的子串不是回文串,整体就不是回文串。
    即:在头尾字符相等的情况下,里面子串的回文性质据定了整个子串的回文性质,这就是状态转移。因此可以把「状态」定义为原字符串的一个子串是否为回文子串。

    第 1 步:定义状态
    dp[i][j] 表示子串 s[i..j] 是否为回文子串,这里子串 s[i..j] 定义为左闭右闭区间,可以取到 s[i] 和 s[j]。

    第 2 步:思考状态转移方程
    在这一步分类讨论(根据头尾字符是否相等),根据上面的分析得到:


    dp[i][j] = (s[i] == s[j]) and dp[i + 1][j - 1]
    说明:

    「动态规划」事实上是在填一张二维表格,由于构成子串,因此 i 和 j 的关系是 i <= j ,因此,只需要填这张表格对角线以上的部分。

    看到 dp[i + 1][j - 1] 就得考虑边界情况。

    边界条件是:表达式 [i + 1, j - 1] 不构成区间,即长度严格小于 2,即 j - 1 - (i + 1) + 1 < 2 ,整理得 j - i < 3。

    这个结论很显然:j - i < 3 等价于 j - i + 1 < 4,即当子串 s[i..j] 的长度等于 2 或者等于 3 的时候,其实只需要判断一下头尾两个字符是否相等就可以直接下结论了。

    如果子串 s[i + 1..j - 1] 只有 1 个字符,即去掉两头,剩下中间部分只有 11 个字符,显然是回文;
    如果子串 s[i + 1..j - 1] 为空串,那么子串 s[i, j] 一定是回文子串。
    因此,在 s[i] == s[j] 成立和 j - i < 3 的前提下,直接可以下结论,dp[i][j] = true,否则才执行状态转移。

    第 3 步:考虑初始化
    初始化的时候,单个字符一定是回文串,因此把对角线先初始化为 true,即 dp[i][i] = true 。

    事实上,初始化的部分都可以省去。因为只有一个字符的时候一定是回文,dp[i][i] 根本不会被其它状态值所参考。

    第 4 步:考虑输出
    只要一得到 dp[i][j] = true,就记录子串的长度和起始位置,没有必要截取,这是因为截取字符串也要消耗性能,记录此时的回文子串的「起始位置」和「回文长度」即可。

    第 5 步:考虑优化空间
    因为在填表的过程中,只参考了左下方的数值。事实上可以优化,但是增加了代码编写和理解的难度,丢失可读和可解释性。在这里不优化空间。

    注意事项:总是先得到小子串的回文判定,然后大子串才能参考小子串的判断结果,即填表顺序很重要。

    大家能够可以自己动手,画一下表格,相信会对「动态规划」作为一种「表格法」有一个更好的理解。

    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];
            char[] charArray = s.toCharArray();
    
            for (int i = 0; i < len; i++)
            {
                dp[i][i] = true;
            }
    
            for (int j = 1; j < len; j++)
            {
                for (int i = 0; i < j; i++)
                {
                    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][j] = true成立,就表示子串s[i...j]是回文,此时记录回文长度和起始位置
                    if (dp[i][j] && j-i+1 > maxLen)
                    {
                        maxLen = j-i+1;
                        begin = i;
                    }
                }
            }
            return s.substring(begin,begin+maxLen);
        }
    }
  • 相关阅读:
    眼睛的颜色 博弈
    codevs1281 矩阵乘法 快速幂 !!!手写乘法取模!!! 练习struct的构造函数和成员函数
    10 25日考试 数学题目练习 斐波拉契 打表
    线段树 模板
    榨取kkksc03 luogu1855 dp 裸二维费用背包
    低价购买 洛谷1108 codevs4748 dp
    [转] 经典排序算法
    [USACO08DEC] Trick or Treat on the Farm
    [NOIP2009] 靶形数独(搜索+剪枝)
    各种蒟蒻模板【如此简单】
  • 原文地址:https://www.cnblogs.com/ziytong/p/13126851.html
Copyright © 2020-2023  润新知