文章目录
最长回文子串
给你一个字符串 s,找到 s 中最长的回文子串。
示例 1:
输入:s = “babad”
输出:“bab”
解释:“aba” 同样是符合题意的答案。
示例 2:
输入:s = “cbbd”
输出:“bb”
示例 3:
输入:s = “a”
输出:“a”
示例 4:
输入:s = “ac”
输出:“a”
暴力匹配
class Solution {
public String longestPalindrome(String s) {
int length = s.length();
int max = 0;
int start = 0;
for (int i = 0; i < length; ++i) {
for (int j = 0; j < length - i; ++j) {
if (s.charAt(i) == s.charAt(length - j - 1)) {
String sub = s.substring(i, length - j);
int num = sub.length();
if (num <= max) {
break;
}
if (isPalindrome(sub)) {
if (num > max) {
start = i;
max = num;
}
break;
}
}
}
}
return s.substring(start, start + max);
}
private boolean isPalindrome(String s) {
String reverse = new StringBuffer(s).reverse().toString();
if (s.equals(reverse)) {
return true;
}
return false;
}
}
扩展中心法
找一个或两个相同字符作为中心,向两边检测是否为相同字符
class Solution {
public String longestPalindrome(String s) {
int start = 0, max = 0;
int length = s.length();
for (int i = 0; i < length; i++) {
int len1 = expandAroundCenter(s, i, i, length);
int len2 = expandAroundCenter(s, i, i + 1, length);
int len = Math.max(len1, len2);
if (len > max) {
start = i - (len - 1) / 2;
max = len;
}
}
return s.substring(start, start + max);
}
private int expandAroundCenter(String s, int left, int right, int length) {
while (left >= 0 && right < length && s.charAt(left) == s.charAt(right)) {
left--;
right++;
}
return right - left - 1;
}
}
动态规范法
class Solution {
public String longestPalindrome(String s) {
int length = s.length();
int start = 0, max = 0;
boolean[][] dp = new boolean[length][length];
for (int i = length - 1; i >= 0; i--) {
for (int j = i; j < length; j++) {
dp[i][j] = (s.charAt(i) == s.charAt(j)) && (j - i < 2 || dp[i + 1][j - 1]);
if (dp[i][j] && j - i + 1 > max) {
start = i;
max = j + 1 - i;
}
}
}
return s.substring(start, start + max);
}
}
最长有效括号
给你一个只包含 ‘(’ 和 ‘)’ 的字符串,找出最长有效(格式正确且连续)括号子串的长度。
示例 1:
输入:s = “(()”
输出:2
解释:最长有效括号子串是 “()”
示例 2:
输入:s = “)()())”
输出:4
解释:最长有效括号子串是 “()()”
示例 3:
输入:s = “”
输出:0
贪心法
从左到右和从右到左两次遍历
class Solution {
public int longestValidParentheses(String s) {
int len = s.length();
int max = 0;
int left = 0, right = 0;
for (int i = 0; i < len; i++) {
if (s.charAt(i) == '(')
left++;
if (s.charAt(i) == ')')
right++;
if (left == right)
max = Math.max(max, left + right);
if (left < right) {
left = right = 0;
}
}
left = right = 0;
for (int i = len - 1; i >= 0; i--) {
if (s.charAt(i) == '(')
left++;
if (s.charAt(i) == ')')
right++;
if (left == right)
max = Math.max(max, left + right);
if (left > right) {
left = right = 0;
}
}
return max;
}
}
中心扩展法
找到一组“()”作为中心,向两侧扩展窗口,碰到上一个窗口则两窗口合并
class Solution {
public int longestValidParentheses(String s) {
int len = s.length();
int max = 0;
int left = 0, right = 0;
Map<Integer, Integer> map = new HashMap<>();
while (left >= 0 && right < len) {
left = s.indexOf("()", right);
right = left + 1;
if (left < 0 || right >= len)
break;
int[] li = expand(s, left, right);
left = li[0];
right = li[1];
while (map.containsKey(left - 1)) {
li = expand(s, map.get(left - 1), right);
left = li[0];
right = li[1];
}
map.put(right, left);
max = Math.max(max, right - left + 1);
}
return max;
}
private int[] expand(String s, int left, int right) {
while (left > 0 && right < s.length() - 1 && s.charAt(left - 1) == '(' && s.charAt(right + 1) == ')') {
left--;
right++;
}
return new int[]{left, right};
}
}
动态规划
定义 d p [ i ] dp[i] dp[i] 表示以下标 ii 字符结尾的最长有效括号的长度。
- s[i]=‘)’ 且 s[i - 1] = ‘(’,也就是字符串形如 “……()”,我们可以推出:dp[i]=dp[i−2]+2。
我们可以进行这样的转移,是因为结束部分的 “()” 是一个有效子字符串,并且将之前有效子字符串的长度增加了 2 。 - s[i]=‘)’ 且 s[i−1]=‘)’,也就是字符串形如 “……))”,我们可以推出:如果 s[ i−dp[i−1]−1 ]=‘(’,那么 dp[i] = dp[i−1] + dp[ i−dp[i−1]−2 ]+2
class Solution {
public int longestValidParentheses(String s) {
int len = s.length();
int max = 0;
int[] dp = new int[len];
for (int i = 1; i < len; i++) {
if (s.charAt(i) == ')') {
if (s.charAt(i - 1) == '(') {
dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2;
} else if (i - dp[i - 1] > 0 && s.charAt(i - dp[i - 1] - 1) == '(') {
dp[i] = dp[i - 1] + ((i - dp[i - 1]) >= 2 ? dp[i - dp[i - 1] - 2] : 0) + 2;
}
max = Math.max(max, dp[i]);
}
}
return max;
}
}
辅助栈法
用一个数组 valid 来记录匹配括号对的位置,在 valid 中找到连续最长的 1 序列。
class Solution {
public int longestValidParentheses(String s) {
int len = s.length();
boolean[] valid = new boolean[len];
LinkedList<Integer> stack = new LinkedList<>();
for (int i = 0; i < len; i++) {
if (s.charAt(i) == '(') {
stack.push(i);
} else {
if (!stack.isEmpty()) {
valid[i] = true;
valid[stack.pop()] = true;
}
}
}
int max = 0, count = 0;
for (int i = 0; i < len; i++) {
count = valid[i] ? count + 1 : 0;
max = Math.max(count, max);
}
return max;
}
}
最短回文串
给定一个字符串 s,你可以通过在字符串前面添加字符将其转换为回文串。找到并返回可以用这种方式转换的最短回文串。
示例 1:
输入:s = “aacecaaa”
输出:“aaacecaaa”
示例 2:
输入:s = “abcd”
输出:“dcbabcd”
暴力匹配
class Solution {
public String shortestPalindrome(String s) {
String r = new StringBuilder(s).reverse().toString();
int len = s.length();
for (int i = 0; i < len; i++) {
if (s.substring(0, len - i).equals(r.substring(i))) {
StringBuilder result = new StringBuilder(s.substring(len - i)).reverse();
return result.append(s).toString();
}
}
return new StringBuilder(s).reverse().append(s).toString();
}
}
中心扩展法
class Solution {
public String shortestPalindrome(String s) {
int len = s.length();
int start = 0;
for (int i = (len - 1) / 2; i >= 0; i--) {
int right1 = expandAroundCenter(s, i, i, len);
int right2 = expandAroundCenter(s, i, i + 1, len);
if (right1 != -1 || right2 != -1) {
start = Math.max(right1, right2);
break;
}
}
StringBuilder sb = new StringBuilder(s);
for (int i = start; i < len; i++)
sb.insert(0, s.charAt(i));
return sb.toString();
}
private int expandAroundCenter(String s, int left, int right, int length) {
while (left >= 0 && right < length && s.charAt(left) == s.charAt(right)) {
left--;
right++;
}
if (left == -1)
return right;
return -1;
}
}
双指针递归法
用两个指针查询从头开始的最长回文串。只要 j 进入了最长回文子串,一定会使得 i 走出最长回文子串,因此用递归法不断在字符串开头加入前缀,直至找到从头开始的回文串。
class Solution {
public String shortestPalindrome(String s) {
int i = 0;
char[] charArray = s.toCharArray();
for (int j = s.length() - 1; j >= 0; j--) {
if (charArray[i] == charArray[j])
i++;
}
if (i == s.length())
return s;
String suffix = s.substring(i);
String reverse = new StringBuilder(suffix).reverse().toString();
return reverse + shortestPalindrome(s.substring(0, i)) + suffix;
}
}
字符串哈希
一个字符串是回文串,当且仅当该字符串与它的反序相同。因此,我们仍然暴力地枚举
s
1
s_1
s1的结束位置,并计算
s
1
s_1
s1与
s
1
s_1
s1反序的哈希值。如果这两个哈希值相等,说明我们找到了一个
s
s
s 的前缀回文串。在枚举
s
1
s_1
s1的结束位置时,我们可以从小到大地进行枚举,这样就可以很容易地维护
s
1
s_1
s1与
s
1
s_1
s1反序的哈希值:
设当前枚举到的结束位置为 i,对应的
s
1
s_1
s1 记为
s
1
i
s_1^i
s1i,其反序记为
s
^
1
i
\hat{s}_1^i
s^1i。我们可以通过递推的方式,在 O(1) 的时间通过
s
1
i
−
1
s_1^{i-1}
s1i−1和
s
^
1
i
−
1
\hat{s}_1^{i-1}
s^1i−1的哈希值得到
s
1
i
s_1^i
s1i和
s
^
1
i
\hat{s}_1^i
s^1i的哈希值:
hash
(
s
1
i
)
=
hash
(
s
1
i
−
1
)
×
base
+
ASCII
(
s
[
i
]
)
\text{hash}(s_1^i) = \text{hash}(s_1^{i-1}) \times \textit{base} + \text{ASCII}(s[i])
hash(s1i)=hash(s1i−1)×base+ASCII(s[i])
hash
(
s
^
1
i
)
=
hash
(
s
^
1
i
−
1
)
+
ASCII
(
s
[
i
]
)
×
base
i
\text{hash}(\hat{s}_1^i) = \text{hash}(\hat{s}_1^{i-1}) + \text{ASCII}(s[i]) \times \text{base}^i
hash(s^1i)=hash(s^1i−1)+ASCII(s[i])×basei
即,
hash
(
s
1
i
)
=
base
0
×
ASCII
(
s
[
0
]
)
+
base
1
×
ASCII
(
s
[
1
]
)
+
base
2
×
ASCII
(
s
[
2
]
)
+
.
.
.
\text{hash}(s_1^i) = \text{base}^0 \times \text{ASCII}(s[0]) + \text{base}^1 \times \text{ASCII}(s[1]) + \text{base}^2 \times \text{ASCII}(s[2]) +...
hash(s1i)=base0×ASCII(s[0])+base1×ASCII(s[1])+base2×ASCII(s[2])+...
hash
(
s
^
1
i
)
=
base
i
×
ASCII
(
s
[
0
]
)
+
base
i
−
1
×
ASCII
(
s
[
1
]
)
+
base
i
−
2
×
ASCII
(
s
[
2
]
)
+
.
.
.
\text{hash}(\hat{s}_1^i) = \text{base}^i \times \text{ASCII}(s[0]) + \text{base}^{i-1} \times \text{ASCII}(s[1]) + \text{base}^{i-2} \times \text{ASCII}(s[2]) +...
hash(s^1i)=basei×ASCII(s[0])+basei−1×ASCII(s[1])+basei−2×ASCII(s[2])+...
class Solution {
public String shortestPalindrome(String s) {
int len = s.length();
int base = 131, mod = 1000000007;
int left = 0, right = 0, mul = 1;
int best = -1;
char[] ascii = s.toCharArray();
for (int i = 0; i < len; ++i) {
left = (int) (((long) left * base + ascii[i]) % mod);
right = (int) ((right + (long) mul * ascii[i]) % mod);
if (left == right)
best = i;
mul = (int) ((long) mul * base % mod);
}
String add = best == len - 1 ? "" : s.substring(best + 1);
StringBuilder prefix = new StringBuilder(add).reverse();
return prefix.append(s).toString();
}
}
KMP
构造的字符串后缀就是原字符串的倒置,前缀后缀相等时,也就意味着当前前缀是一个回文串,而 next 数组是寻求最长的前缀,我们也就找到了开头开始的最长回文串。
class Solution {
public String shortestPalindrome(String s) {
String str = s + '#' + new StringBuilder(s).reverse();
int max = getLastNext(str);
return new StringBuilder(s.substring(max)).reverse() + s;
}
public int getLastNext(String s) {
int len = s.length();
char[] charArray = s.toCharArray();
int[] next = new int[len + 1];
next[0] = -1;
next[1] = 0;
int k = 0, i = 2;
while (i <= len) {
if (k == -1 || charArray[i - 1] == charArray[k]) {
next[i] = k + 1;
k++;
i++;
} else {
k = next[k];
}
}
return next[len];
}
}
马拉车算法
class Solution {
public String preProcess(String s) {
int len = s.length();
if (len == 0)
return "^$";
StringBuilder str = new StringBuilder("^");
for (int i = 0; i < len; i++)
str.append("#").append(s.charAt(i));
str.append("#$");
return str.toString();
}
public String shortestPalindrome(String s) {
String str = preProcess(s);
int len = str.length();
int[] p = new int[len];
int center = 0, max_right = 0;
for (int i = 1; i < len - 1; i++) {
int i_mirror = 2 * center - i;
if (max_right > i)
p[i] = Math.min(max_right - i, p[i_mirror]);
else
p[i] = 0;
while (str.charAt(i + 1 + p[i]) == str.charAt(i - 1 - p[i]))
p[i]++;
if (i + p[i] > max_right) {
center = i;
max_right = i + p[i];
}
}
int max_length = 0;
int centerIndex = 0;
for (int i = 1; i < len - 1; i++) {
int start = (i - p[i]) / 2;
if (start == 0)
max_length = Math.max(p[i], max_length);
}
StringBuilder prefix = new StringBuilder(s.substring(max_length)).reverse();
String result = prefix.append(s).toString();
return result;
}
}
分割回文串1
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
回文串 是正着读和反着读都一样的字符串。
示例 1:
输入:s = “aab”
输出:[[“a”,“a”,“b”],[“aa”,“b”]]
示例 2:
输入:s = “a”
输出:[[“a”]]
回溯
class Solution {
public List<List<String>> partition(String s) {
int len = s.length();
List<List<String>> result = new ArrayList<>();
if (len != 0) {
char[] charArray = s.toCharArray();
backtrack(charArray, len, 0, new ArrayList<String>(), result);
}
return result;
}
private void backtrack(char[] charArray, int len, int start, List<String> path, List<List<String>> res) {
if (len == start) {
res.add(new ArrayList<String>(path));
return;
}
for (int i = start; i < len; i++) {
if (isPalindrome(charArray, start, i)) {
path.add(new String(charArray, start, i + 1 - start));
backtrack(charArray, len, i + 1, path, res);
path.remove(path.size() - 1);
}
}
}
private boolean isPalindrome(char[] charArray, int start, int end) {
for (int i = start, j = end; i < j; i++, j--) {
if (charArray[i] != charArray[j])
return false;
}
return true;
}
}
动态规划+回溯
class Solution {
private List<List<String>> result = new ArrayList<>();
private boolean[][] dp;
public List<List<String>> partition(String s) {
int len = s.length();
if (len == 0)
return result;
dp = new boolean[len][len];
char[] charArray = s.toCharArray();
for (int right = 0; right < len; right++) {
for (int left = 0; left <= right; left++) {
if (charArray[left] == charArray[right] && (right - left <= 2 || dp[left + 1][right - 1])) {
dp[left][right] = true;
}
}
}
backtrack(charArray, len, 0, new ArrayList<String>());
return result;
}
private void backtrack(char[] charArray, int len, int start, List<String> path) {
if (len == start) {
result.add(new ArrayList<String>(path));
return;
}
for (int i = start; i < len; i++) {
if (dp[start][i]) {
path.add(new String(charArray, start, i + 1 - start));
backtrack(charArray, len, i + 1, path);
path.remove(path.size() - 1);
}
}
}
}
分割回文串2
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文。
返回符合要求的 最少分割次数 。
示例 1:
输入:s = “aab”
输出:1
解释:只需一次分割就可将 s 分割成 [“aa”,“b”] 这样两个回文子串。
示例 2:
输入:s = “a”
输出:0
示例 3:
输入:s = “ab”
输出:1
动态规划
class Solution {
public int minCut(String s) {
int len = s.length();
char[] charArray = s.toCharArray();
boolean[][] dp = new boolean[len][len];
for (int i = len - 1; i >= 0; i--) {
for (int j = i; j < len; j++) {
if (charArray[i] == charArray[j] && (j - i <= 1 || dp[i + 1][j - 1]))
dp[i][j] = true;
}
}
int[] split_num = new int[len];
Arrays.fill(split_num, Integer.MAX_VALUE);
for (int i = 0; i < len; ++i) {
if (dp[0][i]) {
split_num[i] = 0;
} else {
for (int j = 0; j < i; ++j) {
if (dp[j + 1][i])
split_num[i] = Math.min(split_num[i], split_num[j] + 1);
}
}
}
return split_num[len - 1];
}
}