Appearance
Manacher(回文)
文字列の各位置を中心とする最長回文の半径を O(N) で求めるアルゴリズム。
奇数長・偶数長の回文をそれぞれ管理する。
イメージ
奇数長回文
S = "abacaba"
| i | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|---|
| S[i] | a | b | a | c | a | b | a |
| odd[i] | 1 | 2 | 1 | 4 | 1 | 2 | 1 |
odd[i] は中心の文字自身を含む半径。
例えば odd[3] = 4 は、中心 c の回文 abacaba(長さ7)を意味する。
最長回文の長さは 2 * odd[i] - 1。
偶数長回文
S = "abba"
| i | 0 | 1 | 2 | 3 |
|---|---|---|---|---|
| S[i] | a | b | b | a |
| even[i] | 0 | 0 | 2 | 0 |
even[i] は S[i-1] と S[i] の間を中心とする半径。
例えば even[2] = 2 は、2つの b の間を中心とする回文 abba(長さ4)を意味する。
最長回文の長さは 2 * even[i]。
特徴
| 項目 | 内容 |
|---|---|
| 用途 | 回文判定・最長回文・回文の数え上げ |
| 構築 | O(N) |
| 区間の回文判定 | O(1) |
| 最長回文の長さ・区間取得 | O(1) |
| 回文部分文字列の総数 | O(N) |
| 空間計算量 | O(N) |
実装
cpp
#pragma once
#include <algorithm>
#include <cassert>
#include <string>
#include <utility>
#include <vector>
struct Manacher {
std::string s;
int n = 0;
std::vector<int> odd, even;
int longest_left = 0, longest_right = 0;
Manacher() = default;
explicit Manacher(const std::string& s) { build(s); }
void build(const std::string& str) {
s = str;
n = (int)s.size();
odd.assign(n, 0);
even.assign(n, 0);
build_odd();
build_even();
find_longest();
}
void build_odd() {
int l = 0, r = -1;
for (int i = 0; i < n; i++) {
int k = i > r ? 1 : std::min(odd[l + r - i], r - i + 1);
while (i - k >= 0 && i + k < n && s[i - k] == s[i + k]) k++;
odd[i] = k;
if (i + k - 1 > r) {
l = i - k + 1;
r = i + k - 1;
}
}
}
void build_even() {
int l = 0, r = -1;
for (int i = 0; i < n; i++) {
int k = i > r ? 0 : std::min(even[l + r - i + 1], r - i + 1);
while (i - k - 1 >= 0 && i + k < n && s[i - k - 1] == s[i + k]) k++;
even[i] = k;
if (i + k - 1 > r) {
l = i - k;
r = i + k - 1;
}
}
}
void update_longest(int l, int r) {
if (r - l > longest_right - longest_left ||
(r - l == longest_right - longest_left && l < longest_left)) {
longest_left = l;
longest_right = r;
}
}
void find_longest() {
longest_left = longest_right = 0;
for (int i = 0; i < n; i++) {
update_longest(i - odd[i] + 1, i + odd[i]);
update_longest(i - even[i], i + even[i]);
}
}
// s[l..r) が回文か O(1) で判定
bool is_palindrome(int l, int r) const {
if (l < 0 || r > n || l > r) return false;
int len = r - l;
if (len == 0) return true;
if (len & 1) return odd[(l + r - 1) / 2] >= len / 2 + 1;
return even[(l + r) / 2] >= len / 2;
}
int odd_length(int i) const { assert(0 <= i && i < n); return 2 * odd[i] - 1; }
int even_length(int i) const { assert(0 <= i && i < n); return 2 * even[i]; }
std::pair<int, int> odd_range(int i) const {
assert(0 <= i && i < n);
return {i - odd[i] + 1, i + odd[i]};
}
std::pair<int, int> even_range(int i) const {
assert(0 <= i && i < n);
return {i - even[i], i + even[i]};
}
int longest_length() const { return longest_right - longest_left; }
std::pair<int, int> longest_range() const { return {longest_left, longest_right}; }
std::string longest_palindrome() const { return s.substr(longest_left, longest_length()); }
// 出現位置が異なる回文は別々に数える
long long count_palindromes() const {
long long ans = 0;
for (int x : odd) ans += x;
for (int x : even) ans += x;
return ans;
}
};使用例
cpp
Manacher ma("abacaba");
cout << ma.odd[3] << '\n'; // 4
cout << ma.odd_length(3) << '\n'; // 7
cout << ma.is_palindrome(0, 7); // 1
cout << ma.longest_palindrome(); // abacaba
cout << ma.count_palindromes(); // 12
auto [l, r] = ma.longest_range(); // [0, 7)偶数長回文の例。
cpp
Manacher ma("abba");
cout << ma.even[2] << '\n'; // 2
cout << ma.even_length(2) << '\n'; // 4
cout << ma.is_palindrome(0, 4); // 1
cout << ma.count_palindromes(); // 6主な操作
| メソッド | 機能 |
|---|---|
build(s) | 回文半径を再計算 |
odd[i] | 奇数長回文の半径 |
even[i] | 偶数長回文の半径 |
odd_length(i) | 奇数長回文の最大長 |
even_length(i) | 偶数長回文の最大長 |
odd_range(i) | 奇数長回文の半開区間 |
even_range(i) | 偶数長回文の半開区間 |
is_palindrome(l, r) | 区間 [l, r) の回文判定 |
longest_range() | 最長回文の半開区間 |
longest_length() | 最長回文の長さ |
longest_palindrome() | 最長回文の文字列 |
count_palindromes() | 回文部分文字列の総数 |
注意点
odd[i]は中心自身を含むため、最小値は1。even[i]は中心の左右にある文字の組数で、最小値は0。- 区間はすべて半開区間
[l, r)。 - 空文字列も回文として扱う。
- 最長回文が複数ある場合は、最も左側のものを返す。
count_palindromes()は同じ内容でも出現位置が異なれば別々に数える。longest_palindrome()は文字列をコピーするため O(回文長)。- この実装は
char単位で処理する。
関連
- Z-Algorithm
- 回文
- suffix最長回文