leetcode刷题记录
题目 最长回文子串
题目描述
给你一个字符串 s,找到 s 中最长的回文子串。
示例 1:
输入:s = “babad”
输出:“bab”
解释:“aba” 同样是符合题意的答案。
示例 2:
输入:s = “cbbd”
输出:“bb”
提示:
1 <= s.length <= 1000
s 仅由数字和英文字母组成
思路
模板 manacher算法
Manacher算法核心
1)理解回文半径数组
2)理解所有中心的回文最右边界R,和取得R时的中心点C
3)理解 L…(i’)…C…(i)…R 的结构,以及根据i’回文长度进行的状况划分
4)每一种情况划分,都可以加速求解i回文半径的过程
什么是回文?
长度为奇数偶数,分为实轴跟虚轴两种情况
暴力的方法
枚举每个i位置做为中心点往左右两边扩
以i为中心的回文有多长
只能找到长度为奇数的回文串, 找不到偶数的
偶数的时候是不行的, cckk找不到
一种比较方便的处理方式, 把字符串处理一下
怎么保证长度为奇数, 跟 长度为偶数的回文串都能找到?
开头 , 结尾 ,以及每两个字符串之间都垫上一个特殊字符
长度为奇数, 偶数的回文都能找到
在处理串中得到的长度/2 向下取整就能知道对应的原始串长度
一定要求添加的字符串是源字符串没出现的吗?
无所谓 # 号 即使原字符串包含也不影响
没有任何时候, 虚的会跟实的去想比较
所以这个虚的是什么字符都行
复杂度: 最差的情况
全是一种字符, 长度2N+1
发现扩的时候, 每次都要扩到边界才能停
没过中心时候, 都是每次扩到左边界停止
过了中心点, 每次都是扩到右边界停
等差数列: O(N^2)
Manacher: O(N)
前面扩的行为会加速我后面扩的行为
前置概念
-
回文半径, 回文直径
处理串不会有偶数长度的回文
-
回文半径数组 pArr[]:
从左往右求, 每个位置得到的答案放在pArr里 -->
加速求i位置的答案, 利用0~i-1的信息
-
最右回文边界
不管你是哪个位置扩的,只要你扩出来的回文区域这个右边界变得更靠右了,
就被我这个R变量抓住
不管你是求那个回文扩的, 只要让R更往右了, R就记录这个更往右的位置
- 取得最右回文右边界的中心 C
以0为中心扩,更新R
C: R跟新时候的中心点
C会跟着R更新, R如果不更新, C也不动, R跟C的含义是一对
四个概念
算法主流程
假设现在来到i位置
以i位置为中心向左右两边扩, 分情况
-
i 没有被R 罩住
暴力扩, 没法优化
-
i 被R 罩住, 有优化, 有三种情况
有图示拓扑关系, 小情况分类, 根据 i’ 自己扩出来的回文区域来分
i’已经求过答案, 而且当初求的答案被保存在回文半径数组里
-
i’扩出的回文区域彻底在L…R内
假设来到i位置求回文区域, i 位置扩的区域 跟 i’一样大
证明
甲, 乙 关于C 对称, 甲乙逆序
i 的回文区域至少是这么大
假设 甲前一个字符a, 后一个字符b
乙前一个字符X, 后一个字符Y
- i’的回文区域跑到L…R的外部去了
i…R的距离就是回文半径
证明:
R’~R 一定是回文 关于C 的对称是 L~L’
为什么不能更长?
假设 甲前一个字符a, 后一个字符b
乙前一个字符X, 后一个字符Y
- i’ 的左边界和L 压线
i的回文区域至少和i’一样, 会不会更大? 不知道, 需要验
复杂度分析
任何一个位置失败的次数 1次, N个位置, 一共失败了N次
如果成功, 一定会推高R, R最多到N, R不回退
R不回退, 复杂度 O(N)
代码
半径数组中的最大值减 1 就是原始串的最大回文子串长度
下标变换 2c - i2∗c−i*就代表 i’
谁小取谁
所以这就是说我给你一个至少不用验的区域。如果你正好是这个答案,你一进来就会break,
你如果恰恰需要验,那你就继续验
manacher代码
public static int manacher(String s) {
if (s == null || s.length() == 0) {
return 0;
}
// "12132" -> "#1#2#1#3#2#"
char[] str = manacherString(s);
// 回文半径的大小
int[] pArr = new int[str.length];
int C = -1;
// 讲述中:R代表最右的扩成功的位置
// coding:最右的扩成功位置的,再下一个位置
int R = -1;
int max = Integer.MIN_VALUE;
for (int i = 0; i < str.length; i++) { // 0 1 2
// R第一个违规的位置,i>= R
// i位置扩出来的答案,i位置扩的区域,至少是多大。
pArr[i] = R > i ? Math.min(pArr[2 * C - i], R - i) : 1;
while (i + pArr[i] < str.length && i - pArr[i] > -1) {
if (str[i + pArr[i]] == str[i - pArr[i]])
pArr[i]++;
else {
break;
}
}
if (i + pArr[i] > R) {
R = i + pArr[i];
C = i;
}
max = Math.max(max, pArr[i]);
}
return max - 1;
}
public static char[] manacherString(String str) {
char[] charArr = str.toCharArray();
char[] res = new char[str.length() * 2 + 1];
int index = 0;
for (int i = 0; i != res.length; i++) {
res[i] = (i & 1) == 0 ? '#' : charArr[index++];
}
return res;
}
Leetcode代码
public static String longestPalindrome(String s) {
if(s == null || s.length() == 0){
return "";
}
int[] manacher = manacher(s);
if((manacher[0] & 1) == 0){
return s.substring(manacher[1] - manacher[0] / 2 ,manacher[1] + manacher[0] / 2);
}else {
return s.substring(manacher[1] - manacher[0] / 2 ,manacher[1] + manacher[0] / 2 + 1);
}
}
public static int[] manacher(String s){
char[] str = manacherStr(s);
int[] pArr = new int[str.length];
int R = -1;
int C = -1;
int max = Integer.MIN_VALUE;
int ans = 0; //存储最大值时的位置
for (int i = 0; i < str.length; i++) {
pArr[i] = R > i ? Math.min(pArr[i],R - i) : 1;
while (i - pArr[i] >= 0 && i + pArr[i] < str.length){
if(str[i+pArr[i]] == str[i - pArr[i]]){
pArr[i]++;
}else {
break;
}
}
//注意更新R 和 C
if(i + pArr[i] > R){
R = i + pArr[i];
C = i;
}
if(max < pArr[i]){
max = pArr[i];
ans = i;
}
}
return new int[]{max - 1,ans / 2};
}
private static char[] manacherStr(String s) {
char[] str = s.toCharArray();
char[] res = new char[2 * str.length + 1];
int index = 0;
for (int i = 0; i < res.length; i++) {
res[i] = (i & 1) == 0 ? '#' : str[index++];
}
return res;
}