算法概述
Manacher算法由Glenn Manacher于1975年提出,用于在线性时间O(n)内找出字符串中的所有回文子串,或求解最长回文子串问题。相比中心扩展法的O(n²)和动态规划的O(n²),Manacher算法将时间复杂度降至线性,是字符串处理领域的经典算法。
该算法的核心思想是利用回文的对称性:若已知一个回文中心及其右边界,则其右侧部分字符的回文信息可以通过对称位置的信息快速推导,避免重复计算。
核心原理
回文半径与预处理
对于字符串中的每个位置i,定义回文半径p[i]为以i为中心的最长回文子串的半径(包含中心字符)。
为了统一处理奇偶长度回文,先对字符串进行预处理:在每个字符之间及两端插入分隔符(如’#’)。
例如:”abba” → “#a#b#b#a#”
这样所有回文都变为奇数长度,中心唯一确定。
镜像对称加速
维护当前最右回文的中心c和右边界r。对于位置i:
- i在r左侧:设i关于c的对称点为mirror = 2c – i
- 若p[mirror]的扩展不超过r,则p[i] = p[mirror](直接镜像)
-
否则p[i]至少为r – i,需要继续向右扩展验证
-
i在r右侧或边界:p[i] = 1,然后向右扩展
-
更新最右边界:若i + p[i] > r,更新c = i,r = i + p[i]
核心不变式
始终维护c和r使得以c为中心的回文子串右边界最远。利用此信息为后续位置提供下界估计。
Java完整实现
/**
* Manacher算法:线性时间求解最长回文子串
* 利用回文对称性避免重复计算
*/
public class Manacher {
/**
* 预处理字符串:插入分隔符统一奇偶长度
* "abba" -> "#a#b#b#a#"
* @param s 原始字符串
* @return 预处理后的字符数组
*/
private static char[] preprocess(String s) {
int n = s.length();
char[] t = new char[2 * n + 3];
t[0] = '^'; // 开头哨兵,防止越界
t[1] = '#';
for (int i = 0; i < n; i++) {
t[2 * i + 2] = s.charAt(i);
t[2 * i + 3] = '#';
}
t[2 * n + 2] = '$'; // 结尾哨兵
return t;
}
/**
* Manacher核心算法
* @param s 原始字符串
* @return 最长回文子串
*/
public static String longestPalindrome(String s) {
if (s == null || s.length() < 1) return "";
if (s.length() == 1) return s;
char[] t = preprocess(s);
int n = t.length;
int[] p = new int[n]; // 回文半径数组
int center = 0; // 当前最右回文的中心
int right = 0; // 当前最右回文的右边界(不包含)
int maxLen = 0; // 最长回文长度
int centerIndex = 0; // 最长回文中心
for (int i = 1; i < n - 1; i++) {
// 利用对称性获取初始半径下界
int mirror = 2 * center - i; // i关于center的对称点
if (i < right) {
// i在当前最右回文内部,利用对称性
p[i] = Math.min(right - i, p[mirror]);
} else {
// i在最右回文右侧,从1开始扩展
p[i] = 1;
}
// 中心扩展:尝试向两边扩展回文
while (t[i + p[i]] == t[i - p[i]]) {
p[i]++;
}
// 更新最右回文边界
if (i + p[i] > right) {
center = i;
right = i + p[i];
}
// 记录最长回文
if (p[i] > maxLen) {
maxLen = p[i];
centerIndex = i;
}
}
// 从预处理字符串中提取原始回文子串
// 起始位置:(centerIndex - maxLen + 1 - 1) / 2 = (centerIndex - maxLen) / 2
int start = (centerIndex - maxLen) / 2;
return s.substring(start, start + maxLen - 1);
}
/**
* 求解所有回文子串的数量
* 每个不同的回文子串只计数一次
*/
public static int countPalindromicSubstrings(String s) {
if (s == null || s.length() == 0) return 0;
char[] t = preprocess(s);
int n = t.length;
int[] p = new int[n];
int center = 0, right = 0;
int count = 0;
for (int i = 1; i < n - 1; i++) {
int mirror = 2 * center - i;
if (i < right) {
p[i] = Math.min(right - i, p[mirror]);
} else {
p[i] = 1;
}
while (t[i + p[i]] == t[i - p[i]]) {
p[i]++;
}
if (i + p[i] > right) {
center = i;
right = i + p[i];
}
// 以i为中心的回文子串数量为 p[i] / 2(向下取整)
count += p[i] / 2;
}
return count;
}
/**
* 获取以每个位置为中心的最长回文半径
* 可用于后续回文相关查询
*/
public static int[] getPalindromeRadii(String s) {
char[] t = preprocess(s);
int n = t.length;
int[] p = new int[n];
int center = 0, right = 0;
for (int i = 1; i < n - 1; i++) {
int mirror = 2 * center - i;
if (i < right) {
p[i] = Math.min(right - i, p[mirror]);
} else {
p[i] = 1;
}
while (t[i + p[i]] == t[i - p[i]]) {
p[i]++;
}
if (i + p[i] > right) {
center = i;
right = i + p[i];
}
}
return p;
}
/**
* 判断子串s[l..r]是否为回文(O(1)查询,需先预处理)
*/
public static boolean isPalindromeQuery(int[] radii, int l, int r) {
// 预处理后的位置:字符s[i]对应t[2*i+2]
// 子串s[l..r]对应中心在t[l+r+2]
int center = l + r + 2;
int radius = r - l + 1;
return radii[center] >= radius + 1; // 包含#的半径条件
}
/**
* 寻找最长双回文串(由两个回文拼接而成)
*/
public static String longestDoublePalindrome(String s) {
int n = s.length();
if (n < 2) return s;
int[] radii = getPalindromeRadii(s);
int maxLen = 0;
int split = 0;
for (int i = 0; i < n - 1; i++) {
// 检查s[0..i]和s[i+1..n-1]是否都是回文
int leftCenter = i + 2; // t中的中心位置
int rightCenter = i + 1 + n + 1;
boolean leftPal = (i % 2 == 0) ?
radii[leftCenter] >= i + 2 :
radii[leftCenter] >= i + 2;
// 简化:直接调用最长回文
String left = s.substring(0, i + 1);
String right = s.substring(i + 1);
String lp = longestPalindrome(left);
String rp = longestPalindrome(right);
if (lp.length() + rp.length() > maxLen) {
maxLen = lp.length() + rp.length();
split = i;
}
}
return s.substring(0, split + 1) + s.substring(split + 1);
}
/**
* 主程序:测试Manacher算法
*/
public static void main(String[] args) {
System.out.println("===== Manacher最长回文子串算法 =====\n");
// 测试1:基本回文检测
System.out.println("--- 测试1:基本回文子串 ---");
String[] testCases = {
"babad", // "bab" 或 "aba"
"cbbd", // "bb"
"a", // "a"
"ac", // "a" 或 "c"
"racecar", // "racecar"
"bananas", // "anana"
"abacdfgdcaba", // "aba"
"aaaa", // "aaaa"
"abcbaabcba", // "abcbaabcba"
};
for (String test : testCases) {
long start = System.nanoTime();
String result = longestPalindrome(test);
long end = System.nanoTime();
System.out.printf("输入: %-20s -> 最长回文: %-15s (耗时: %dns)%n",
"\"" + test + "\"", "\"" + result + "\"", (end - start));
}
// 测试2:回文子串计数
System.out.println("\n--- 测试2:回文子串计数 ---");
String[] countTests = {"aaa", "abc", "abba", "racecar"};
for (String test : countTests) {
int count = countPalindromicSubstrings(test);
System.out.printf("\"%s\" 中的回文子串数量: %d%n", test, count);
}
// 测试3:预处理验证
System.out.println("\n--- 测试3:预处理字符串 ---");
String sample = "abba";
char[] processed = preprocess(sample);
System.out.println("原始: " + sample);
System.out.println("预处理后: " + new String(processed));
// 测试4:大规模性能测试
System.out.println("\n--- 测试4:大规模性能测试 ---");
StringBuilder sb = new StringBuilder();
java.util.Random rand = new java.util.Random(42);
for (int i = 0; i < 100000; i++) {
sb.append((char) ('a' + rand.nextInt(26)));
}
String large = sb.toString();
long start = System.currentTimeMillis();
String largeResult = longestPalindrome(large);
long manacherTime = System.currentTimeMillis() - start;
start = System.currentTimeMillis();
String dpResult = longestPalindromeDP(large);
long dpTime = System.currentTimeMillis() - start;
System.out.println("字符串长度: " + large.length());
System.out.println("Manacher结果长度: " + largeResult.length());
System.out.println("Manacher耗时: " + manacherTime + "ms");
System.out.println("DP耗时: " + dpTime + "ms");
System.out.println("加速比: " + (dpTime / Math.max(manacherTime, 1)) + "x");
// 测试5:边界情况
System.out.println("\n--- 测试5:边界情况 ---");
System.out.println("空字符串: \"" + longestPalindrome("") + "\"");
System.out.println("单字符: \"" + longestPalindrome("x") + "\"");
System.out.println("全相同: \"" + longestPalindrome("zzzzzz") + "\"");
System.out.println("无回文(>1): \"" + longestPalindrome("abcdef") + "\"");
}
/**
* 动态规划版本(O(n²)),用于性能对比
*/
private static String longestPalindromeDP(String s) {
int n = s.length();
if (n < 2) return s;
boolean[][] dp = new boolean[n][n];
int start = 0, maxLen = 1;
// 单个字符都是回文
for (int i = 0; i < n; i++) {
dp[i][i] = true;
}
// 填充表格
for (int len = 2; len <= n; len++) {
for (int i = 0; i <= n - len; i++) {
int j = i + len - 1;
if (s.charAt(i) == s.charAt(j)) {
if (len == 2 || dp[i + 1][j - 1]) {
dp[i][j] = true;
if (len > maxLen) {
maxLen = len;
start = i;
}
}
}
}
}
return s.substring(start, start + maxLen);
}
}
算法执行流程图解
以字符串”abacaba”为例:
预处理: ^#a#b#a#c#a#b#a#$
索引: 0123456789...
i=2 (字符'a'): p[2]=1, 扩展得 p[2]=2("#a#")
i=4 (字符'b'): p[4]=1, 扩展得 p[4]=2("#b#")
i=6 (字符'a'): 在右边界内,mirror=2, p[6]=min(r-6, p[2])=2, 扩展得 p[6]=4("#a#b#a#")
i=8 (字符'c'): 在右边界内,mirror=4, p[8]=min(r-8, p[4])=2, 扩展得 p[8]=8(整个回文)
复杂度分析
| 指标 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(n) | 每个字符最多被访问常数次 |
| 空间复杂度 | O(n) | 存储半径数组和预处理字符串 |
| 中心扩展 | O(n²) | 每个中心向两边扩展 |
| 动态规划 | O(n²) | 二维表格填充 |
| 最优性 | 最优 | 必须至少读取每个字符一次 |
扩展应用
回文树(Eertree)
用于统计不同回文子串的数量和出现位置,也是线性时间算法。
回文因子分解
将字符串分解为最少数量的回文子串,可用Manacher预处理结合DP求解。
最长回文子序列
与最长回文子串不同,允许删除字符。需要用动态规划O(n²)求解。
应用场景
- DNA序列分析:寻找基因组中的回文结构
- 音乐信息检索:旋律片段的对称性检测
- 自然语言处理:回文词检测、文本对称性分析
- 数据压缩:利用回文结构进行编码优化
- 竞赛编程:字符串类问题的标准工具
总结
Manacher算法通过对称性利用和预处理技巧,将回文子串问题的复杂度降至理论下界O(n)。其核心思想——维护最右回文边界并利用镜像信息——展示了算法设计中”避免重复计算”原则的精妙应用。理解Manacher算法有助于培养将几何直觉转化为高效代码的能力。