【问题描述】[第5题][最长回文子串][中等]
给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。示例 1:输入: "babad"
输出: "bab"
注意: "aba" 也是一个有效答案。
【解答思路】
1. 中心扩展法
时间复杂度:O(N^2) 空间复杂度:O(1)
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,不能取 jreturn s.substring(i + 1, j);}
}
2.暴力
- 根据回文子串的定义,枚举所有长度大于等于 22 的子串,依次判断它们是否是回文;
- 在具体实现时,可以只针对大于“当前得到的最长回文子串长度”的子串进行“回文验证”;
- 在记录最长回文子串的时候,可以只记录“当前子串的起始位置”和“子串长度”
时间复杂度:O(N^3) 空间复杂度:O(1)
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;}
}
3. 动态规划
时间复杂度:O(N^2) 空间复杂度:O(N)
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];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);}
}
【总结】
1. 想法优先暴力 逐渐优化 不要东想西想
2.动态规划
1、思考状态(重点)
状态的定义,先尝试「题目问什么,就把什么设置为状态」;
然后思考「状态如何转移」,如果「状态转移方程」不容易得到,尝试修改定义,目的依然是为了方便得到「状态转移方程」。
「状态转移方程」是原始问题的不同规模的子问题的联系。即大问题的最优解如何由小问题的最优解得到。
2、思考状态转移方程(核心、难点)
状态转移方程是非常重要的,是动态规划的核心,也是难点;
常见的推导技巧是:分类讨论。即:对状态空间进行分类;
归纳「状态转移方程」是一个很灵活的事情,通常是具体问题具体分析;
除了掌握经典的动态规划问题以外,还需要多做题;
如果是针对面试,请自行把握难度。掌握常见问题的动态规划解法,理解动态规划解决问题,是从一个小规模问题出发,逐步得到大问题的解,并记录中间过程;
「动态规划」方法依然是「空间换时间」思想的体现,常见的解决问题的过程很像在「填表」。
3、思考初始化
初始化是非常重要的,一步错,步步错。初始化状态一定要设置对,才可能得到正确的结果。
角度 1:直接从状态的语义出发;
角度 2:如果状态的语义不好思考,就考虑「状态转移方程」的边界需要什么样初始化的条件;
角度 3:从「状态转移方程」方程的下标看是否需要多设置一行、一列表示「哨兵」(sentinel),这样可以避免一些特殊情况的讨论。
4、思考输出
有些时候是最后一个状态,有些时候可能会综合之前所有计算过的状态。
5、思考优化空间(也可以叫做表格复用)
「优化空间」会使得代码难于理解,且是的「状态」丢失原来的语义,初学的时候可以不一步到位。先把代码写正确是更重要;
「优化空间」在有一种情况下是很有必要的,那就是状态空间非常庞大的时候(处理海量数据),此时空间不够用,就必须「优化空间」;
非常经典的「优化空间」的典型问题是「0-1 背包」问题和「完全背包」问题。
3. 动态规划思考
- 边界问题考虑清楚(第二第三步)
- 动态就是做表格 想清楚方向
- 自底向上 子问题 学基础 再解决问题 通识教育
- 自顶向下 一般解决问题思路
转载链接:https://leetcode-cn.com/problems/longest-palindromic-substring/solution/zhong-xin-kuo-san-dong-tai-gui-hua-by-liweiwei1419/