15-2 Longest palindrome subsequence

A palindrome is a nonempty string over some alphabet that reads the same forward and backward. Examples of palindromes are all strings of length $1$, $\text{civic}$, $\text{racecar}$, and $\text{aibohphobia}$ (fear of palindromes).

Give an efficient algorithm to find the longest palindrome that is a subsequence of a given input string. For example, given the input $\text{character}$, your algorithm should return $\text{carac}$. What is the running time of your algorithm?

Let $A[1..n]$ denote the array which contains the given word. Taking inspiration from the Longest-Common-Subsequence problem earlier in the chapter, we can consider each pair of indexes $i$ and $j$ such that $0 \le i < j < n$ as a decision point where each character-pair may be either included or excluded from the longest-palindromic substring.

Our recursion is defined as:

  • If $A[i] = A[j]$, then characters $i$ and $j$ must be in the longest palindromic substring (else we contradict that our recursion solves for the longest possible substring)
  • If $A[i] \ne A[j]$, then the longest palindromic substring is found in one of $A[i + 1..j]$ or $A[i..j + 1]$, forming our recursive subproblems

Our recursive base case is the single-character palindrome, i.e. the case where $i = j$. From there we build our subproblems up taking one character-length step at a time until we reach $n$ our size of the initial array.

LPS-LENGTH(A, n)
    initialize a table LPS of size n by n
    initialize a table DECISIONS of size n by n

    for i = 1 to n
        // Base case, a palindrome of length 1
        LPS[i][i] = 1
        DECISIONS[i][i] = "TAKE ONE"

    for length = 1 to n
        for i = 0 to (n - length)
            j = i + length
            if A[i] == A[j]
                LPS[i][j] = 2 + LPS[i + 1][j - 1]
                DECISIONS[i][j] = "TAKE BOTH"
            else if LPS[i + 1][j] > LPS[i][j - 1]
                LPS[i][j] = LPS[i + 1][j]
                DECISIONS[i][j] = "IGNORE LEFT"
            else
                LPS[i][j] = LPS[i][j - 1]
                DECISIONS[i][j] = "IGNORE RIGHT"

    return LPS, DECISIONS

As is easily intuited by the above algorithm, our algorithm $O(n^2)$, needing to iterate over $n$ subproblem-lengths and having to compare $O(n)$ index pairs for each.

And then similarly to the Longest-Common-Subsequence problem again, we can traverse the $\text{DECISIONS}$ matrix to reconstruct our palindrome:

PRINT-LPS(A, DECISIONS, i, j)
    if DECISIONS[i][j] == "TAKE ONE":
        print A[i]
    else if DECISIONS[i][j] == "TAKE BOTH":
        print A[i]
        PRINT-LCS(A, DECISIONS, i + 1, j - 1)
        print A[j]
    else if DECISIONS[i][j] == "IGNORE LEFT":
        PRINT-LCS(A, DECISIONS, i + 1, j)
    else
        // IGNORE_RIGHT
        PRINT-LCS(A, DECISIONS, i, j - 1)