题目
给你一个字符串 s,找到 s 中最长的回文子串。
示例 1:
输入:s = "babad" 输出:"bab" 解释:"aba" 同样是符合题意的答案。 示例 2:
输入:s = "cbbd" 输出:"bb" 示例 3:
输入:s = "a" 输出:"a" 示例 4:
输入:s = "ac" 输出:"a"
提示: 1 <= s.length <= 1000 s 仅由数字和英文字母(大写和/或小写)组成
动态规划
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Main main = new Main();
main.longestPalindrome("abc");
}
// DP table(自底向上记录)
Map<Integer, List<int[]>> dpTable = new HashMap<>();
public String longestPalindrome(String s) {
int length = s.length();
// 从短字符串开始找起, 回文串长度为1, 当长度为2, 长度为3
for (int i = 1; i <= 1000; i ++) {
if (i > 2) {
// 根据长度为1和长度为2的回文字符串得到
int pre = i - 2;
if (dpTable.containsKey(pre)) {
List<int []> tempList = dpTable.get(pre);
for (int [] index : tempList) {
int before = index[0] - 1;
int after = index[1];
if (before >= 0 && after < length && s.charAt(before) == s.charAt(after)) {
// 添加
addMap(before, after + 1, i);
}
}
}
} else {
for (int j = 0; j <= s.length() - i; j ++) {
String temp = s.substring(j, i + j);
if (isPalindrome(temp)) {
addMap(j, i + j, i);
}
}
}
}
Set<Integer> set = dpTable.keySet();
Object[] obj = set.toArray();
Arrays.sort(obj);
int maxKey = Integer.parseInt(obj[obj.length - 1].toString());
int [] maxIndex = dpTable.get(maxKey).get(0);
return s.substring(maxIndex[0], maxIndex[1]);
}
public boolean isPalindrome(String tempString) {
if (tempString.length() == 1) {
return Boolean.TRUE;
}
char [] charArray = tempString.toCharArray();
for (int i = 0; i < (tempString.length() / 2); i ++) {
if (tempString.charAt(i) != tempString.charAt(tempString.length() - 1 - i)) {
return Boolean.FALSE;
}
}
return Boolean.TRUE;
}
public void addMap(int start, int end, int key) {
List<int[]> temp = new ArrayList<>();
if (dpTable.containsKey(key)) {
temp = dpTable.get(key);
} else {
}
temp.add(new int[]{start, end});
dpTable.put(key, temp);
}
}
基本思路
-
首先根据题目分析, 是否是最优子结构, 能否找到状态转移方程, 发现当长度大于2当时候, 例如等于3的时候, 是通过对于长度为1的回文子字符串两头添加一个相同的字符, 就可以得到一个回文串, 长度为4的时候可以由长度为2的回文子字符串得到. 因此类推, 5由3得到, 6由4得到. 因此求解时, 只需要根据上一次的结果, 判断守首尾的两个字符即可, 同时如果没有3, 就肯定没5, 后续的判断非常简化.
-
因此假定回文字符串长度为1, 开始递增求取, 直到结束, 需要用Dp table记录不同长度回文子字符串的索引下标情况
-
状态转移方程: 1. dp(i -2)存在, 且在原字符串中索引下标为m, n, 然后s.charAt(m -1) == s.charAt(n + 1) 条件是 i > 2 dp(i) = 2. 直接判断长度为1, 长度为2的回文子字符串有哪些
-
需要假定回文串长度为多少的结束条件可以是题目要求, 也可以是原是字符串的长度
缺点
-
当前的dp table记录了所有长度的回文字符串的索引, 但是题目只需要最长的, 也就是说假如一个长度为2的回文字符串索引是(2, 3), 然后我在计算4的时候, 我判断(2, 3)存在, 同时判断索引1和4字符相同, 这个时候记录(1, 4)即可, 无需通过list来存储, 直接用二维数组即可
-
找最长的索引的时候, 方式也比较愚蠢
优化后的动态规划
public class Main {
public static void main(String[] args) {
Main main = new Main();
main.longestPalindrome("cbbd");
}
public String longestPalindrome(String s) {
int length = s.length();
String result = "";
boolean [][] dpTable = new boolean[length][length];
// l为每次期望的回文子字符串的长度
for (int l = 0; l < s.length(); l ++) {
// i代表每个长度从索引0开始遍历
int ss = 0;
for (int i = 0; i < s.length() - l; i ++) {
// i代表长度为l + 1子字符串的开始索引, j代表结束索引
int j = l + i;
if (l == 0) {
// l为0, 实际长度为1的, 任意位置都是回文子字符串
dpTable[i][j] = Boolean.TRUE;
} else if(l == 1) {
// l为1, 实际长度为2的, 只需满足i和j的字符相等即可
dpTable[i][j] = (s.charAt(i) == s.charAt(j));
} else {
// 长度大于2的, 需要dp[i + 1][j - 1]为true
// 即长度差2, 索引各差1
dpTable[i][j] = (s.charAt(i) == s.charAt(j)) && dpTable[i + 1][j - 1];
}
// 如果得到更长的子字符串, 刷新结果即可, 无需存储旧有的字符串
if (dpTable[i][j] && j - i + 1> result.length()) {
result = s.substring(i, j + 1);
}
}
}
return result;
}
}
改进
-
同样是发现当长度大于2时的子回文字符串和l -2 的有关, 但是全程的判断都是通过字符串中的索引来完成, 更加高效
-
dp table采用二维数组的方式来记录, 更加高效
-
将之前的判断一个回文子字符串两头的字符是否相同, 转化为判断当前的两个字符是否相同, 变成去掉当前两个字符, 所包含的字符串是否是回文子字符串, 本质一样, 但是这样判断省去了边界判断.
-
最终结果的存储变成了一旦有更长的就替代原来的, 无需存储太多字符串