The recurring difficulty is the odd/even distinction, which every technique handles in its own way. Manacher gives the maximal palindrome around every centre in (the same mirror-and-extend skeleton as the Z-function); hashing gives palindrome tests; the eertree handles distinct palindromes and factorisations.
| Task | Method | Cost |
|---|---|---|
| is a palindrome | Manacher lookup, or forward/reverse hashes | |
| longest palindromic substring | Manacher; or hashing + binary search per centre | ; |
| count palindromic substrings with multiplicity | over centres | |
| count palindromic substrings of , online | Manacher + wavelet trees (“sum of min”) | per query |
| count distinct palindromes; occurrences of each | eertree | |
| longest palindromic subsequence; min insertions | LCS with the reverse; | |
| minimum cuts into palindromes | DP with the palindrome test; with series links | |
| shortest palindrome by prepending / appending | prefix function of / | |
| palindromic pairs among words | hash each word and its reverse, match | |
| rearrangement into a palindrome | at most one character with odd count |
Facts. A string has at most distinct palindromic substrings (aaa...a has with multiplicity but only distinct). The palindromic suffixes of a string form arithmetic progressions by length, which makes the factorisation possible. The hashing approach generalises to variants Manacher cannot express (one mismatch allowed, weighted palindromes).
struct Manacher { // len[c]: longest palindrome at centre c (0..2n-2; even c = char, odd c = gap)
vector<int> len;
template <class T> Manacher(const T& s) {
int n = sz(s), m = 2 * n + 1; vector<int> rad(m); // virtual string "#s0#s1#...#": odd p is s[p/2]
for (int p = 0, c = 0, r = 0; p < m; p++) {
int k = p < r ? min(rad[2 * c - p], r - p) : 0;
while (p - k - 1 >= 0 && p + k + 1 < m && ((p - k - 1) % 2 == 0 || s[(p - k - 1) / 2] == s[(p + k + 1) / 2])) k++;
rad[p] = k; if (p + k > r) c = p, r = p + k;
}
len.resize(max(2 * n - 1, 0)); for (int c = 0; c < sz(len); c++) len[c] = rad[c + 1];
}
bool isPalindrome(int l, int r) const { return l >= r || len[l + r - 1] >= r - l; } // s[l, r)
pair<int, int> longest() const { int best = 0, from = 0; for (int c = 0; c < sz(len); c++) if (len[c] > best) best = len[c], from = (c + 1 - len[c]) / 2; return {from, from + best}; }
ll count() const { ll r = 0; for (int x : len) r += (x + 1) / 2; return r; }
};
int minPalindromeCuts(const string& s) { // fewest palindromes covering s, O(n^2)
int n = sz(s); Manacher m(s); vector<int> dp(n + 1, INT_MAX); dp[0] = 0;
for (int i = 1; i <= n; i++) for (int j = 0; j < i; j++) if (dp[j] != INT_MAX && m.isPalindrome(j, i)) dp[i] = min(dp[i], dp[j] + 1);
return dp[n];
}Proof: Manacher
In the virtual string with separators every palindrome has odd length and a unique centre ; a palindrome of the original string of length appears with radius around (separators match separators, characters match characters). If is the palindrome reaching furthest right () and , the mirror sees the same characters within the window, so , and only comparisons beyond are needed; each successful comparison pushes right, so the total is . With
len[c] = rad[c + 1], the centre index of makesisPalindromea radius check. Counting: around a centre with maximal length , the palindromes are those of lengths of the right parity, of them. Cuts DP: the last piece of an optimal cover is a palindrome . Verified by brute force.
Palindromic substrings of a range
Number of palindromic substrings (by position) of , online. An odd centre with radius (number of odd palindromes there) contributes ; left of the middle of the second term is the binding one, right of it the third. With : , a “sum of minimum with a constant” over a range, answered by a wavelet tree with prefix sums; symmetrically on the right with , and the same for even centres. Uses a wavelet tree (full version, also with -th smallest, counting equal values and adjacent swaps, as in Range Order Statistics).
template <class T> struct WaveletTree {
struct Node { int lo = 0, hi = 0, left = -1, right = -1; vector<int> b; vector<T> c; }; // b[i]: #first i going left; c[i]: sum of first i
int n; vector<T> vals; vector<int> a; vector<Node> t;
WaveletTree(const vector<T>& arr) : n(sz(arr)), vals(arr) {
sort(all(vals)); vals.erase(unique(all(vals)), vals.end()); a.resize(n);
for (int i = 0; i < n; i++) a[i] = lower_bound(all(vals), arr[i]) - vals.begin();
vector<int> seq = a; build(seq, 0, max<int>(0, sz(vals) - 1));
}
int build(vector<int>& seq, int lo, int hi) {
int id = sz(t); t.emplace_back(); Node nd; nd.lo = lo, nd.hi = hi; int mid = (lo + hi) / 2;
nd.b.assign(sz(seq) + 1, 0); nd.c.assign(sz(seq) + 1, T{});
for (int i = 0; i < sz(seq); i++) nd.b[i + 1] = nd.b[i] + (seq[i] <= mid), nd.c[i + 1] = nd.c[i] + vals[seq[i]];
if (lo < hi && !seq.empty()) {
vector<int> L, R; for (int x : seq) (x <= mid ? L : R).push_back(x);
seq.clear(); seq.shrink_to_fit();
nd.left = build(L, lo, mid); nd.right = build(R, mid + 1, hi);
}
t[id] = move(nd); return id;
}
T kth(int l, int r, int k) const { // k-th smallest (0-indexed) in [l, r)
int u = 0;
while (t[u].lo < t[u].hi) {
const Node& nd = t[u]; int inLeft = nd.b[r] - nd.b[l];
if (k < inLeft) l = nd.b[l], r = nd.b[r], u = nd.left; else k -= inLeft, l -= nd.b[l], r -= nd.b[r], u = nd.right;
}
return vals[t[u].lo];
}
pair<int, T> rankSum(int u, int l, int r, int x) const { // count and sum of compressed values <= x in [l, r)
if (u < 0 || l >= r || x < t[u].lo) return {0, T{}};
const Node& nd = t[u]; if (nd.hi <= x) return {r - l, nd.c[r] - nd.c[l]};
auto [c1, s1] = rankSum(nd.left, nd.b[l], nd.b[r], x); auto [c2, s2] = rankSum(nd.right, l - nd.b[l], r - nd.b[r], x);
return {c1 + c2, s1 + s2};
}
int compressUpper(const T& x) const { return upper_bound(all(vals), x) - vals.begin() - 1; }
int countLessEq(int l, int r, const T& x) const { return vals.empty() ? 0 : rankSum(0, l, r, compressUpper(x)).first; }
T sumLessEq(int l, int r, const T& x) const { return vals.empty() ? T{} : rankSum(0, l, r, compressUpper(x)).second; }
int countEqual(int l, int r, const T& x) const {
auto it = lower_bound(all(vals), x); if (it == vals.end() || *it != x) return 0;
int c = it - vals.begin(); return rankSum(0, l, r, c).first - (c ? rankSum(0, l, r, c - 1).first : 0);
}
void swapAdjacent(int i) { // swap a[i], a[i+1] in O(log)
int x = a[i], y = a[i + 1], u = 0, p = i;
while (true) {
Node& nd = t[u]; nd.c[p + 1] = nd.c[p] + vals[y];
if (nd.lo == nd.hi) break;
int mid = (nd.lo + nd.hi) / 2; bool gx = x <= mid, gy = y <= mid;
if (gx != gy) { nd.b[p + 1] = nd.b[p] + gy; break; }
if (gx) p = nd.b[p], u = nd.left; else p = p - nd.b[p], u = nd.right;
}
swap(a[i], a[i + 1]);
}
};
struct PalindromesInRange {
int n; vector<ll> A, B, C, D; WaveletTree<ll> wa, wb, wc, wd; // odd: R - i, R + i; even (centre before i): E - i, E + i
template <class S> static vector<ll> build(const S& s, int kind) {
Manacher m(s); int n = sz(s); vector<ll> v(n, 0);
for (int i = 0; i < n; i++) {
if (kind < 2) { ll R = (m.len[2 * i] + 1) / 2; v[i] = kind == 0 ? R - i : R + i; }
else { ll E = i > 0 ? m.len[2 * i - 1] / 2 : 0; v[i] = kind == 2 ? E - i : E + i; }
}
return v;
}
template <class S> PalindromesInRange(const S& s) : n(sz(s)), A(build(s, 0)), B(build(s, 1)), C(build(s, 2)), D(build(s, 3)), wa(A), wb(B), wc(C), wd(D) {}
static ll sumMin(const WaveletTree<ll>& w, int l, int r, ll x) { if (l >= r) return 0; ll le = w.countLessEq(l, r, x); return w.sumLessEq(l, r, x) + x * ((r - l) - le); }
static ll sumIdx(ll l, ll r) { return l < r ? (l + r - 1) * (r - l) / 2 : 0; }
ll query(int l, int r) const { // palindromic substrings s[a, b) with l <= a < b <= r
if (l >= r) return 0;
ll res = 0; int m1 = (l + r - 1) / 2; // odd centres i <= m1: min(R, i - l + 1)
res += sumIdx(l, m1 + 1) + sumMin(wa, l, m1 + 1, 1 - l);
res += -sumIdx(m1 + 1, r) + sumMin(wb, m1 + 1, r, r);
int m2 = (l + r) / 2, lo = l + 1, hi = r; // even centres i in [l+1, r-1]: min(E, i - l, r - i)
if (lo < hi) { int mid = min(max(m2 + 1, lo), hi); res += sumIdx(lo, mid) + sumMin(wc, lo, mid, -l); res += -sumIdx(mid, hi) + sumMin(wd, mid, hi, r); }
return res;
}
};Proof: the range-count decomposition and the wavelet tree
An odd centre supports odd palindromes (radii ); inside the radius is also limited by and , so the count is . For , , so the count is ; for it is . Even centres between and with palindromes behave the same with and split point . Each term equals (sum of values ) (count of values ). Wavelet tree: each level partitions the values by the midpoint of the value range and stores prefix counts and sums, so a range splits into canonical pieces;
kthdescends by comparing with the number going left;swapAdjacentonly changes the prefix arrays at one position on the levels where both elements still share a node. Verified against brute force for all ranges of random strings.
Related notes
- Eertree
- Prefix Function and Z (shortest palindrome by appending)
- Hashing (palindrome hashes)