Palindromic Tree

Definition

A palindromic tree (EER tree, also called a palindromic automaton) is an efficient data structure that stores all palindromic substrings of a string. It was first published by Mikhail Rubinchik and Arseny M. Shur in 2015. Inspired by suffix trees and other suffix-oriented string structures, it gives simple and efficient solutions to many palindrome problems.

Structure

A palindromic tree looks roughly like this.

Figure: pam1

As with other automata, a palindromic tree consists of transition edges and suffix links (fail pointers), and each node represents a palindromic substring.

Because palindromes can have odd or even length, one could insert a character outside the alphabet, such as '#', as a separator, as in Manacher's algorithm, so that all palindrome lengths become odd. This is inconvenient, however. Is there a better method?

Yes. Build two trees: nodes in one tree represent palindromes of odd length, and nodes in the other represent palindromes of even length.

As in other automata, a node's fail pointer points to the node representing the longest palindromic suffix of its palindrome. A transition edge does not append one character to the original palindrome; it adds the same character to both ends, which is necessary to preserve palindromicity.

Each node also stores len, the length of its palindrome. This information makes the tree easy to construct.

Construction

A palindromic tree has two initial states representing palindromes of lengths −1,0-1,0, called the odd root and even root. They do not represent actual strings; they only serve as initial states, just like roots of other automata.

The even root's fail pointer points to the odd root. The odd root's fail pointer is irrelevant: the odd root cannot fail, because every transition from it has length 11, a single-character palindrome.

As with a suffix automaton, construct the palindromic tree incrementally.

Suppose the tree for the first p−1p-1 characters has been built, and we append the character at position pp of the original string.

Starting from the node representing the longest palindromic suffix ending at the previous character, repeatedly follow fail pointers until finding a node satisfying sp=sp−len−1s_{p}=s_{p-len-1}; that is, the character immediately before the represented palindrome equals the character being appended.

Here is the corresponding figure from the paper.

Figure: pam2

Following fail pointers finds the node for A; adding X to both ends gives the current palindrome XAX. This node is the tree node for the longest palindromic suffix ending at pp. The length-−1-1 node is useful here: if no X matches, the condition reduces to sp=sps_p=s_p, so the node representing the one-character palindrome X is obtained naturally. The pair X and XAX is therefore handled by the same transition. If this node does not yet exist, create it.

We must then find the fail pointer of the new node. Again follow fail pointers from A to find the longest palindromic suffix XBX of XAX, and set the fail pointer to its node.

This node never needs to be created: the first lenBlen_B characters and last lenBlen_B characters of A are both B; the two ends of the first part correspond to X by palindromicity, and the second part is fixed as X, so the same lenBlen_B-length construction gives XBX, which must already be present.

If no fail transition matches, connect the node to the length-00 node, which is valid because the empty string is a suffix of every node.

Proof of the linear number of states

Theorem

For a string ss, the number of distinct palindromic substrings is at most ∣s∣|s|.

Proof

Use mathematical induction.

  • If ∣s∣=1|s| =1, the string ss has one character and one substring, which is a palindrome, so the claim holds.

  • If ∣s∣>1|s| >1, let t=sct=sc, where tt is obtained by appending cc to ss, and assume the claim holds for ss. Consider palindromic substrings ending at the final character cc, and sort their left endpoints as l1,l2,…,lkl_1,l_2,\dots,l_k. Since t[l1..∣t∣]t[l_1..|t|] is a palindrome, for every l1≤p≤∣t∣l_1 \le p \le |t| we have t[p..∣t∣]=t[l1..l1+∣t∣−p]t[p..|t|]=t[l_1..l_1+|t|-p]. Hence, for 1<i≤k1 < i \le k, the palindrome t[li..∣t∣]t[l_i..|t|] already occurred in t[1..∣t∣−1]t[1..|t|-1]. Each appended character therefore introduces at most one new distinct palindrome, and the first such position is 11.

By induction, the theorem follows.

Thus the palindromic tree has O(∣s∣)O(|s|) states. Each state represents exactly one distinct palindrome, and the state that transitions to a node is unique, so the total number of transitions is also O(∣s∣)O(|s|).

Correctness proof

In the figure above, when the current character X is added, the linear-state proof says that we only need to find the longest palindromic suffix containing the final character, namely XAX. We then find its longest palindromic suffix XBX and create the suffix link. The state for XBX already exists; its palindromic suffix containing the final character is XAX, and XBX itself and all ancestors of its state in the fail tree are therefore already represented.

For the palindromic tree of ss, let n=∣s∣n=|s|. Apart from following fail pointers, all operations are clearly O(n)O(n).

When a character is added, each fail jump changes the fail-tree depth by −1-1, while adding a fail link changes it by −1-1 in the accounting below. When fail is 00, reaching −1-1, the net increase is +2+2 from the negative-one node.

Only nn characters are added, so the depth can increase at most nn times and there can be at most 2n2n fail jumps.

Therefore, constructing the palindromic tree of ss takes O(∣s∣)O(|s|) time.

Applications

Number of distinct palindromic substrings

By the linear-state theorem, the number of distinct palindromic substrings of a string equals the number of palindromic-tree states, excluding the odd and even roots.

Number of occurrences of palindromic substrings

Build the palindromic tree and count occurrences similarly to a suffix automaton.

Nodes are inserted in topological order during construction. Enumerate all states in reverse order and add each state's occurrence count to the count of the state pointed to by its fail link.

Example: 「APIO2014」Palindrome

Define the existence value of a substring of ss as its number of occurrences in ss multiplied by its length. Given ss, find the maximum existence value among its palindromic substrings.

#include <algorithm>
#include <cstring>
#include <iostream>
#include <string>
using namespace std;
constexpr int MAXN = 300000 + 5;

namespace pam {
int sz, tot, last;
int cnt[MAXN], ch[MAXN][26], len[MAXN], fail[MAXN];
char s[MAXN];

int node(int l) {  // Create a new node of length l
  sz++;
  memset(ch[sz], 0, sizeof(ch[sz]));
  len[sz] = l;
  fail[sz] = cnt[sz] = 0;
  return sz;
}

void clear() {  // Initialize
  sz = -1;
  last = 0;
  s[tot = 0] = '$';
  node(0);
  node(-1);
  fail[0] = 1;
}

int getfail(int x) {  // Find a palindromic suffix
  while (s[tot - len[x] - 1] != s[tot]) x = fail[x];
  return x;
}

void insert(char c) {  // Build the tree
  s[++tot] = c;
  int now = getfail(last);
  if (!ch[now][c - 'a']) {
    int x = node(len[now] + 2);
    fail[x] = ch[getfail(fail[now])][c - 'a'];
    ch[now][c - 'a'] = x;
  }
  last = ch[now][c - 'a'];
  cnt[last]++;
}

long long solve() {
  long long ans = 0;
  for (int i = sz; i >= 0; i--) {
    cnt[fail[i]] += cnt[i];
  }
  for (int i = 1; i <= sz; i++) {  // Update the answer
    ans = max(ans, 1ll * len[i] * cnt[i]);
  }
  return ans;
}
}  // namespace pam

string s;

int main() {
  cin.tie(nullptr)->sync_with_stdio(false);
  pam::clear();
  cin >> s;
  for (int i = 0; i < s.size(); i++) {
    pam::insert(s[i]);
  }
  cout << pam::solve() << '\n';
  return 0;
}

Minimum palindromic partition

Given a string s(1≤∣s∣≤105)s(1\le |s| \le 10^5), find the minimum kk such that there are strings s1,s2,…,sks_1,s_2,\dots,s_k, each si(1≤i≤k)s_i(1\le i \le k) a palindrome, whose concatenation s1,s2,…,sks_1,s_2, \dots ,s_k in order equals ss.

Consider DP, where dp[i]dp[i] is the minimum number of parts for the length-ii prefix of ss. The transition enumerates all palindromes ending at character ii:

dp[i]=1+min⁡s[j+1..i] 为回文串dp[j]dp[i]=1+\min_{ s[j+1..i] \text{ 为回文串} } dp[j]

There can be O(n2)O(n^2) palindromic substrings, so this takes O(n2)O(n^2) time and is too slow. The following lemmas optimize the transition.

Let the prefix of ss of length ii be pre(s,i)pre(s,i) and the suffix of length ii be suf(s,i)suf(s,i).

Period: if 0<p≤∣s∣0< p \le |s| and ∀1≤i≤∣s∣−p,s[i]=s[i+p]\forall 1 \le i \le |s|-p,s[i]=s[i+p], then pp is a period of ss.

Border: if 0≤r<∣s∣0 \le r < |s| and pre(s,r)=suf(s,r)pre(s,r)=suf(s,r), then pre(s,r)pre(s,r) is a border of ss.

Relation between periods and borders: tt is a border of ss if and only if ∣s∣−∣t∣|s|-|t| is a period of ss.

Proof

If tt is a border of ss, then pre(s,∣t∣)=suf(s,∣t∣)pre(s,|t|)=suf(s,|t|), so ∀1≤i≤∣t∣,s[i]=s[∣s∣−∣t∣+i]\forall 1\le i \le |t|, s[i]=s[|s|-|t|+i]. Hence ∣s∣−∣t∣|s|-|t| is a period of ss.

If ∣s∣−∣t∣|s|-|t| is a period of ss, then ∀1≤i≤∣s∣−(∣s∣−∣t∣)=∣t∣,s[i]=s[∣s∣−∣t∣+i]\forall 1 \le i \le |s|-(|s|-|t|)=|t|,s[i]=s[|s|-|t|+i], so pre(s,∣t∣)=suf(s,∣t∣)pre(s,|t|)=suf(s,|t|) and tt is a border of ss.

Lemma 1

For a palindromic string ss and a suffix tt of the string, the suffix tt is a border of ss if and only if tt is a palindrome.

Proof

For 1≤i≤∣t∣1 \le i \le |t|, since ss and tt are palindromes, s[i]=s[∣s∣−i+1]=s[∣s∣−∣t∣+i]s[i]=s[|s|-i+1]=s[|s|-|t|+i], so tt is a border of ss.

Conversely, for 1≤i≤∣t∣1 \le i \le |t|, if tt is a border of ss, s[i]=s[∣s∣−∣t∣+i]s[i]=s[|s|-|t|+i]. Since ss is a palindrome, the same string gives s[i]=s[∣s∣−i+1]s[i]=s[|s|-i+1], hence s[∣s∣−i+1]=s[∣s∣−∣t∣+i]s[|s|-i+1]=s[|s|-|t|+i] and tt is a palindrome.

The positions with the same color in the figure represent equal characters.

Figure: pam3

Lemma 2

Let tt be a border of ss with ∣s∣≤2∣t∣|s|\le 2|t|. Then ss is a palindrome if and only if tt is a palindrome.

Proof

If ss is a palindrome, Lemma 11 shows that the suffix tt and this ss are also palindromes.

If tt is a palindrome, this tt has the border relation ∀1≤i≤∣t∣,s[i]=s[∣s∣−∣t∣+i]=s[∣s∣−i+1]\forall 1 \le i \le |t|, s[i]=s[|s|-|t|+i]=s[|s|-i+1]. Since ∣s∣≤2∣t∣|s| \le 2|t|, ss is also a palindrome.

Lemma 3

If tt is a border of a palindromic string, then ∣s∣−∣t∣|s|-|t| is a period of ss. It is the minimum period ∣s∣−∣t∣|s|-|t| of ss if and only if tt is the longest proper palindromic suffix of ss; both quantities refer to ss.

Lemma 4

Let xx be a palindrome, and let yy be the longest proper palindromic suffix of xx; let zz be the longest proper palindromic suffix of yy. Let u,vu,v satisfy x=uy,y=vzx=uy,y=vz. Then:

  1. ∣u∣≥∣v∣|u| \ge |v|;

  2. If ∣u∣>∣v∣|u| > |v|, then ∣u∣>∣z∣|u| > |z|;

  3. If ∣u∣=∣v∣|u| = |v|, then u=vu=v.

Figure: pam4
Proof
  1. By the consequence of Lemma 33, ∣u∣=∣x∣−∣y∣|u|=|x|-|y| is the minimum period of xx, and ∣v∣=∣y∣−∣z∣|v|=|y|-|z| is the minimum period of yy. If ∣u∣<∣v∣|u| < |v|, then, because yy is a suffix of xx, uu is a period of both xx and yy, contradicting the minimality of ∣v∣|v| for yy. Thus ∣u∣≥∣v∣|u| \ge |v|.
  2. Since yy is a border of xx, vv is a prefix of xx. Write x=vwx=vw, so zz is a border of ww. If ∣u∣≤∣z∣|u| \le |z|, then ∣zu∣≤2∣z∣|zu| \le 2|z|; Lemma 22 makes ww a palindrome, and Lemma 11 makes ww a border of xx. Since ∣u∣>∣v∣|u| > |v|, this gives ∣w∣>∣y∣|w| > |y|, a contradiction. Therefore ∣u∣>∣z∣|u| > |z|, with the same ww notation.
  3. Both u,vu,v are prefixes of xx and have ∣u∣=∣v∣|u|=|v|, so u=vu=v.
Figure: pam5

Corollary

When all palindromic suffixes of ss are sorted by length, they can be divided into log⁡∣s∣\log |s| arithmetic-progression segments.

Proof

Sort the lengths of the palindromic suffixes of ss increasingly as l1,l2,…,lkl_1,l_2,\dots,l_k. For 2≤i≤k−12 \le i \le k-1, if li−li−1=li+1−lil_{i}-l_{i-1}=l_{i+1}-l_{i}, then li−1,li,li+1l_{i-1},l_{i},l_{i+1} form an arithmetic progression. Otherwise, li−li−1≠li+1−lil_{i}-l_{i-1}\neq l_{i+1}-l_{i}, and Lemma 44 gives li+1−li>li−li−1l_{i+1}-l_{i}>l_{i}-l_{i-1} and li+1−li>li−1l_{i+1}-l_{i}>l_{i-1}, so li+1>2li−1l_{i+1}>2l_{i-1}. Whenever the difference changes, the largest length therefore more than doubles relative to the smallest. This can happen only O(log⁡∣s∣)O(\log |s|) times, so the lengths of the palindromic suffixes of ss form log⁡∣s∣\log |s| arithmetic-progression segments.

The corollary can also be proved with the weak periodicity lemma by grouping the borders of the longest palindromic suffix ss according to their length xx, where x∈[20,21),[21,22),…,[2k,n)x \in [2^0,2^1),[2^1,2^2),\dots,[2^k,n). Considering the longest border in each group gives log⁡∣s∣\log |s| groups. Detailed proofs can be found in Jin Ce's Selected String Algorithms and Chen Sunli's 2019 IOI National Candidate Team paper Algorithms and Applications for Substring Period Queries.

With this result, we can optimize the transition of dpdp.

Optimization

For every node uu in the palindromic tree, maintain diff[u]diff[u] and slink[u]slink[u]. Here diff[u]diff[u] is the difference between the lengths represented by uu and fail[u]fail[u], namely len[u]−len[fail[u]]len[u]-len[fail[u]]. The pointer slink[u]slink[u] follows fail pointers from uu to the first node vv with diff[v]≠diff[u]diff[v] \neq diff[u]; it is the shortest-length node in the arithmetic-progression segment containing uu.

By the result above, following slinkslink requires only O(log⁡∣s∣)O(\log |s|) jumps for each appended character. We can therefore store the sum of the dpdp values of all palindromes in one arithmetic-progression segment (the original problem uses min⁡\min) at the node representing its longest palindrome.

Let g[v]g[v] be the sum of the dpdp values in the arithmetic-progression segment containing vv, where vv is its longest node. Then g[v]=∑slink[x]=slink[v]dp[i−len[x]]g[v]=\sum_{slink[x]=slink[v]} dp[i-len[x]], with ii the current index.

To update gg and dpdp, suppose the current character is at index ii and its palindromic-tree node is xx. The orange positions in the figure contribute to g[x]g[x]; the shortest palindrome slink[x]slink[x] belongs to the next segment. The previous occurrence of fail[x]fail[x] ends at i−diff[x]i-diff[x], and g[fail[x]]g[fail[x]] contains the blue dpdp values. Thus g[x]g[x] is g[fail[x]]g[fail[x]] plus one additional dpdp value at i−(len[slink[x]]+diff[x])i-(len[slink[x]]+diff[x]), where the previous endpoint is also i−diff[x]i-diff[x]. Use g[x]g[x] to update dp[i]dp[i], then repeat while following slink[x]slink[x]; the new dpdp contribution is included in each step.

Figure: pam6

The correctness relies on the following fact: if xx and fail[x]fail[x] belong to the same arithmetic-progression segment, the previous occurrence of fail[x]fail[x] is at i−diff[x]i-diff[x].

Proof
  1. By Lemma 11, fail[x]fail[x] is a border of xx, so it occurs at i−diff[x]i-diff[x].

Suppose fail[x]fail[x] occurs at position jj in (i−diff[x],i)(i-diff[x],i). Since xx and fail[x]fail[x] are in the same arithmetic progression, 2∣fail[x]∣≥x2|fail[x]| \ge x. The extra occurrence of fail[x]fail[x] overlaps the occurrence at i−diff[x]i-diff[x]; call the overlap ww, and let uu satisfy uw=fail[x]uw=fail[x]. As in Lemma 11, ww is a palindrome, and the prefix s[i−len[x]+1..j]=uwus[i-len[x]+1..j]=uwu of xx is also a palindrome. This contradicts the fact that fail[x]fail[x] is the longest palindromic prefix (and suffix) of xx, whose previous occurrence is again fail[x]fail[x].

Example: Codeforces 932G Palindrome Partition

Given a string ss, partition it into t1,t2,…,tkt_1, t_2, \dots, t_k, where kk is even and ti=tk−i+1t_i=t_{k-i+1}. Count the valid partitions of ss.

Solution

Construct t= s[0]s[n − 1]s[1]s[n − 2]s[2]s[n − 3]…s[n / 2 − 1]s[n / 2]t= s[0]s[n - 1]s[1]s[n - 2]s[2]s[n - 3] \dots s[n / 2 - 1]s[n / 2]. The problem becomes counting even-palindrome partitions of tt: change the transition above from a minimum to a sum and update dpdp only at even positions. The time complexity is O(nlog⁡n)O(n \log n) and the space complexity is O(n)O(n).

#include <cstring>
#include <iostream>
#include <string>
using namespace std;
using ll = long long;
constexpr int mod = 1e9 + 7;
constexpr int MAXN = 1000000 + 5;

int add(int x, int y) {
  x += y;
  return x >= mod ? x -= mod : x;
}

namespace pam {
int sz, tot, last;
int ch[MAXN][26], len[MAXN], fail[MAXN];
int cnt[MAXN], dep[MAXN], dif[MAXN], slink[MAXN];
char s[MAXN];

int node(int l) {  // Create a new node of length l
  sz++;
  memset(ch[sz], 0, sizeof(ch[sz]));
  len[sz] = l;
  fail[sz] = 0;
  cnt[sz] = 0;
  dep[sz] = 0;
  return sz;
}

void clear() {  // Initialize
  sz = -1;
  last = 0;
  s[tot = 0] = '$';
  node(0);
  node(-1);
  fail[0] = 1;
}

int getfail(int x) {  // Find a palindromic suffix
  while (s[tot - len[x] - 1] != s[tot]) x = fail[x];
  return x;
}

void insert(char c) {  // Build the tree
  s[++tot] = c;
  int now = getfail(last);
  if (!ch[now][c - 'a']) {
    int x = node(len[now] + 2);
    fail[x] = ch[getfail(fail[now])][c - 'a'];
    dep[x] = dep[fail[x]] + 1;
    ch[now][c - 'a'] = x;
    dif[x] = len[x] - len[fail[x]];
    if (dif[x] == dif[fail[x]])
      slink[x] = slink[fail[x]];
    else
      slink[x] = fail[x];
  }
  last = ch[now][c - 'a'];
  cnt[last]++;
}
}  // namespace pam

using pam::dif;
using pam::fail;
using pam::len;
using pam::slink;
int n, dp[MAXN], g[MAXN];
string s;
char t[MAXN];

int main() {
  cin.tie(nullptr)->sync_with_stdio(false);
  pam::clear();
  cin >> s;
  n = s.size();
  s = " " + s;
  for (int i = 1, j = 0; i <= n; i++) t[++j] = s[i], t[++j] = s[n - i + 1];
  dp[0] = 1;
  for (int i = 1; i <= n; i++) {
    pam::insert(t[i]);
    for (int x = pam::last; x > 1; x = slink[x]) {
      g[x] = dp[i - len[slink[x]] - dif[x]];
      if (dif[x] == dif[fail[x]]) g[x] = add(g[x], g[fail[x]]);
      if (i % 2 == 0) dp[i] = add(dp[i], g[x]);  // Update the dp array at even positions
    }
  }
  cout << dp[n];
  return 0;
}

Example problems

Further reading

This content comes from OI Wiki: Palindromic Tree. See the original for authors and revision history. AIOJ has adapted the formatting, in-site links, and resource paths.

Content is shared under CC BY-SA 4.0 and SATA license; additional terms are governed by the original notice. Thanks to the OI Wiki authors and contributors; per the SATA terms, please visit the OI Wiki repository and star it to show your support.