剑指 Offer II 020. 回文子字符串的个数(647. 回文子串)
题目:
思路:
【1】动态规划的方式
动态规划的原理,如ababa为例
二维数组的情况大致是这样:
[ a , b , a , b , a
a:[true, false, true, false, true],
b:[false, true, false, true, false],
a:[false, false, true, false, true],
b:[false, false, false, true, false],
a:[false, false, false, false, true]
]
其实可以看做是双指针的另类思想的体现
如纵坐标代表的是左指针,横坐标代表的是右指针
那么这块
a , b , a , b , a
a:[true, false, true, false, true],
代表的是左指针不动,右指针不断往右移动,是否是回文字符串的判断
【2】中心扩展法(实质的思路和动态规划的思路类似)
核心问题理解:
为什么有 2 * len - 1 个中心点?
aba 有5个中心点,分别是 a、b、c、ab、ba
abba 有7个中心点,分别是 a、b、b、a、ab、bb、ba
什么是中心点?
中心点即 left 指针和 right 指针初始化指向的地方,可能是一个也可能是两个
为什么不可能是三个或者更多?
因为 3 个可以由 1 个扩展一次得到,4 个可以由两个扩展一次得到
【3】Manacher 算法
代码展示:
Manacher 算法的方式(要着重理解思路):
//时间3 ms击败86.24% //内存39.7 MB击败56.1% //时间复杂度:O(n)。即 Manacher 算法的时间复杂度,由于最大回文右端点 rm 只会增加而不会减少,故中心拓展进行的次数最多为 O(n),此外我们只会遍历字符串一次,故总复杂度为 O(n)。 //空间复杂度:O(n)。 class Solution { public int countSubstrings(String s) { int n = s.length(); StringBuffer t = new StringBuffer("$#"); for (int i = 0; i < n; ++i) { t.append(s.charAt(i)); t.append('#'); } n = t.length(); t.append('!'); int[] f = new int[n]; int iMax = 0, rMax = 0, ans = 0; for (int i = 1; i < n; ++i) { // 初始化 f[i] f[i] = i <= rMax ? Math.min(rMax - i + 1, f[2 * iMax - i]) : 1; // 中心拓展 while (t.charAt(i + f[i]) == t.charAt(i - f[i])) { ++f[i]; } // 动态维护 iMax 和 rMax if (i + f[i] - 1 > rMax) { iMax = i; rMax = i + f[i] - 1; } // 统计答案, 当前贡献为 (f[i] - 1) / 2 上取整 ans += f[i] / 2; } return ans; } }
中心扩展的方式:
//时间3 ms击败86.24% //内存39.7 MB击败59.99% //时间复杂度是 O(N^2) //空间复杂度是 O(1) class Solution { public int countSubstrings(String s) { // 中心扩展法 int ans = 0; for (int center = 0; center < 2 * s.length() - 1; center++) { // left和right指针和中心点的关系是? // 首先是left,有一个很明显的2倍关系的存在,其次是right,可能和left指向同一个(偶数时),也可能往后移动一个(奇数) // 大致的关系出来了,可以选择带两个特殊例子进去看看是否满足。 int left = center / 2; int right = left + center % 2; while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) { ans++; left--; right++; } } return ans; } } //中心扩展法(奇偶分开的方式) //时间1 ms击败100% //内存39.7 MB击败52.23% class Solution { public int countSubstrings(String s) { char[] cs = s.toCharArray(); int n = cs.length; int ans = 1; for (int i = 0; i < n - 1; i++) ans += centerSpread(cs, i, i) + centerSpread(cs, i, i + 1); return ans; } private int centerSpread(char[] cs, int l, int r) { while (0 <= l && r < cs.length && cs[l] == cs[r]) { l--; r++; } return (r - l) / 2; } }
动态规划的方式:
//时间9 ms击败38.13% //内存41.8 MB击败17.32% //时间复杂度为 O(N^2) //空间复杂度为 O(N^2) class Solution { public int countSubstrings(String s) { // 动态规划法 boolean[][] dp = new boolean[s.length()][s.length()]; int ans = 0; // 其实这个双循环相当于对字符串进行分割(进行了全分割) // 如ababa // 第一次被分为 a // 第二次被分为 ab 和 b // 第三次被分为 aba 和 ba 和 a // 第四次被分为 abab 和 bab 和 ab 和 b // 第五次被分为 ababa 和 baba 和 aba 和 ba 和 a for (int j = 0; j < s.length(); j++) { for (int i = 0; i <= j; i++) { // 这里将条件拆分一下 // 1)s.charAt(i) == s.charAt(j) && (j - i < 2) // 两字符相等,且是同一字符,必然是回文字符串 // 2)s.charAt(i) == s.charAt(j) && (j - i >= 2 && dp[i + 1][j - 1])
// 两字符相等,但长度大于1,即ababa这种,那么就要判断左右各减去一个字符的情况,即bab这部分 // 由于之前已经做了判断所以直接查找即可 if (s.charAt(i) == s.charAt(j) && (j - i < 2 || dp[i + 1][j - 1])) { dp[i][j] = true; ans++; } } } return ans; } }
中心扩展法