马拉车算法是寻找最长回文子串的高效算法,时间复杂度为O(n)
#include <iostream>
#include <string>
#include <vector>
using namespace std;
string longestPalindrome(string s) {
// 步骤1: 预处理,在字符间插入特殊字符'#'
string t = "#";
for (char c : s) {
t += c;
t += "#";
}
// 步骤2: 初始化变量
int n = t.length();
vector<int> P(n, 0); // P[i]存储以i为中心的回文半径
int C = 0; // 当前回文串的中心
int R = 0; // 当前回文串的右边界
int maxLen = 0; // 最长回文子串的长度
int centerIndex = 0; // 最长回文子串的中心位置
// 步骤3: 主循环,计算每个位置的回文半径
for (int i = 0; i < n; i++) {
// 确定初始回文半径
if (i < R) {
int mirror = 2 * C - i; // i关于C的对称点
P[i] = min(R - i, P[mirror]); // 利用对称性
}
// 尝试扩展回文
int a = i + (1 + P[i]);
int b = i - (1 + P[i]);
while (a < n && b >= 0 && t[a] == t[b]) {
P[i]++;
a++;
b--;
}
// 更新C和R
if (i + P[i] > R) {
C = i;
R = i + P[i];
}
// 更新最长回文子串信息
if (P[i] > maxLen) {
maxLen = P[i];
centerIndex = i;
}
}
// 步骤4: 提取最长回文子串
int start = (centerIndex - maxLen) / 2;
return s.substr(start, maxLen);
}
int main() {
string s = "babad";
cout << "输入字符串: " << s << endl;
cout << "最长回文子串: " << longestPalindrome(s) << endl;
return 0;
}
1.首先要进行预处理在每个字符之间添加"#",这样就可以统一处理奇数和偶数的字符串
2.变量有,回文半径组p,回文中心c,右边界r.
Manacher算法的核心思想是利用已经计算出的回文信息来避免重复计算。通过维护当前最右的回文边界,算法可以在大多数情况下直接得到一个很好的初始回文半径.
为了体现高继承性,下面我多使用了类来解答马拉车算法
#include <iostream>
#include <string>
#include <vector>
class Manacher {
private:
std::string original; // 原始字符串
std::string processed; // 预处理后的字符串
std::vector<int> palindromeLengths; // 回文长度数组
int maxPalindromeLength; // 最长回文子串的长度
int maxPalindromeCenter; // 最长回文子串的中心位置
// 预处理字符串,在字符间插入特殊字符'#'
void preprocess() {
processed = "#";
for (char c : original) {
processed += c;
processed += "#";
}
}
// 核心Manacher算法
void computePalindromeLengths() {
int n = processed.length();
palindromeLengths.resize(n, 0);
int center = 0; // 当前回文串的中心
int right = 0; // 当前回文串的右边界
for (int i = 0; i < n; i++) {
// 利用对称性初始化回文长度
if (i < right) {
int mirror = 2 * center - i;
palindromeLengths[i] = std::min(right - i, palindromeLengths[mirror]);
}
// 尝试扩展回文
int a = i + (1 + palindromeLengths[i]);
int b = i - (1 + palindromeLengths[i]);
while (a < n && b >= 0 && processed[a] == processed[b]) {
palindromeLengths[i]++;
a++;
b--;
}
// 更新中心和右边界
if (i + palindromeLengths[i] > right) {
center = i;
right = i + palindromeLengths[i];
}
// 更新最长回文子串信息
if (palindromeLengths[i] > maxPalindromeLength) {
maxPalindromeLength = palindromeLengths[i];
maxPalindromeCenter = i;
}
}
}
public:
// 构造函数
Manacher(const std::string& s) : original(s), maxPalindromeLength(0), maxPalindromeCenter(0) {
preprocess();
computePalindromeLengths();
}
// 获取最长回文子串
std::string getLongestPalindrome() {
int start = (maxPalindromeCenter - maxPalindromeLength) / 2;
return original.substr(start, maxPalindromeLength);
}
// 获取所有回文子串的数量
int countAllPalindromes() {
int count = 0;
for (int length : palindromeLengths) {
count += (length + 1) / 2;
}
return count;
}
// 判断子串是否为回文
bool isPalindrome(int start, int end) {
int len = end - start + 1;
int center = start + end + 1; // 在processed字符串中的中心位置
return palindromeLengths[center] >= len;
}
};
int main() {
std::string s = "babad";
Manacher manacher(s);
std::cout << "输入字符串: " << s << std::endl;
std::cout << "最长回文子串: " << manacher.getLongestPalindrome() << std::endl;
std::cout << "回文子串的总数: " << manacher.countAllPalindromes() << std::endl;
std::cout << "子串'bab'是否为回文: " << (manacher.isPalindrome(0, 2) ? "是" : "否") << std::endl;
return 0;
}
最后让我详细解释一下为什么在预处理字符串中,每个回文长度对应原字符串中的 (length + 1) / 2
个不同回文。
- 预处理字符串的特性:
- 在预处理后的字符串中,每个原始字符之间都插入了 '#'。
- 这意味着预处理字符串的长度是原始字符串长度的两倍加一。
- 回文中心的两种情况:
- 在原字符串中,回文中心可能在字符上,也可能在字符之间。
- 在预处理字符串中,所有可能的回文中心都变成了字符位置(包括 '#')。
- 回文长度的对应关系:
- 预处理字符串中长度为 1 的回文对应原字符串中长度为 1 的回文。
- 预处理字符串中长度为 3 的回文对应原字符串中长度为 2 的回文。
- 预处理字符串中长度为 5 的回文对应原字符串中长度为 3 的回文。
- 以此类推...
- 计算公式的推导:
- 设预处理字符串中的回文长度为 length。
- 对应的原字符串回文长度为 (length + 1) / 2。
- 例如:
- length = 1 时,原字符串回文长度 = (1 + 1) / 2 = 1
- length = 3 时,原字符串回文长度 = (3 + 1) / 2 = 2
- length = 5 时,原字符串回文长度 = (5 + 1) / 2 = 3
- 为什么是不同的回文:
- 在预处理字符串中,每个位置为中心的最长回文都是唯一的。
- 这个最长回文包含了以该中心的所有较短回文。
- 举例说明: 考虑原字符串 "ababa" 和预处理字符串 "#a#b#a#b#a#":
- 对于中心位置 5(原字符串中的中间 'a'):
- 回文长度为 5,对应原字符串中 "ababa"
- 回文长度为 3,对应原字符串中 "aba"
- 回文长度为 1,对应原字符串中 "a"
- 这三个回文都是不同的,且都以这个 'a' 为中心。
- 对于中心位置 5(原字符串中的中间 'a'):
- 公式的应用:
- 对于长度为 5 的回文,(5 + 1) / 2 = 3,正好对应这个中心位置的 3 个不同回文。
通过这种方式,(length + 1) / 2
既计算了原字符串中对应的回文长度,又恰好表示了以该位置为中心的不同回文数量。这个巧妙的关系使得我们可以快速计算所有回文子串的数量,而不需要逐一枚举。