jazzpeh

Longest Palindromic Subsequence

Jazz Peh, May 13, 2020

Given a string S, we need to find the length of its Longest Palindromic Subsequence. Palindrome is a string that reads the same backwards as well as forward and can be odd or even length. A subsequence is a sequence that can be derived from another string by deleting some or no elements without changing the order of the remaining elements.

Example 1
-----------
s: "ELRMENMET"
output: 5
Explanation: "ELRMENMET" => "EMEME"
              ^  ^^ ^^
Example 2
-----------
s: "AMEEWMEA"
output: 6
Explanation: "AMEEWMEA" => "AMEEMA"
              ^^^^ ^ ^

Solution

This problem can be solved by applying Divide and Conquer to break down the problem into smaller subproblems. Thus, by solving these subproblems, we will arrive at our final answer. Let’s try to derive the subproblems.

If the first and last character matches: 2 + f(1,8)      ---|---max
If it doesn't match: max of [0 + f(0,8)] or [0 + f(1,9)] ---|

For palindrome, the first and last character should be the same so that it reads the same from the front and the back. Hence, to achive optimal substructure, if the first and last character matches, our answer increases by 2 and we move on to the next characters, hence, 2 + f(1,8). If it doesn’t match, we should then attempt to see if the next characters on both side matches and get the max value from it. Therefore, max of [0 + f(0,8)] or [0 + f(1,9)].

Code Algortihm

function lps(s: string, start: number, end: number): number {
  if (start > end) return 0;

  if (start === end) return 0;

  let count1 = 0;
  if (s[start] === s[end]) {
    count1 = 2 + lps(s, start + 1, end + 1);
  }

  count2 = lps(s, start, end - 1);
  count3 = lps(s, start + 1, end);

  return Math.max(count1, count2, count3);
}

Optimisation

Let’s look at the an example of the recursion tree see if we need to optimise this solution.

                lps(0,3)
    /               |               \
  lps(0,2)        lps(1,3)        lps(1,2)
  |                |                |
  |-lps(1,2)       |-lps(2,3)       |-lps(2,2)
  |-lps(0,1)       |-lps(1,2)       |-lps(1,1)
  |-lps(1,1)       |-lps(2,2)       |-lps(2,1)

We are computing a new of the subproblems more than once such as lps(1,2). We can then apply Dynamic Programming to optimise the solution.

Top Down Approach

We will start off by applying Top Down Approach (also known as Memoization).

type HashTable = {
  [x: number]: {
    [y: number]: number;
  };
};

const dp: HashTable = {};

function lps(s: string, start: number, end: number): number {
  if (start > end) return 0;

  if (start === end) return 0;

  if (!(start in dp) && !(end in dp[start])) {
    let count1 = 0;
    if (s[start] === s[end]) {
      count1 = 2 + lps(s, start + 1, end + 1);
    }

    count2 = lps(s, start, end - 1);
    count3 = lps(s, start + 1, end);

    if (!dp[start]) {
      dp[start] = {};
    }

    dp[start][end] = Math.max(count1, count2, count3);
  }

  return dp[start][end];
}

Bottom Up Approach

We can now then apply some reverse engineer to use the Bottom Up Approach (also known as Tabulation). Firstly, let’s start from looking at the Top Down Approach using a matrix table.

B1B2B3B4B5B6B7B8B9
ELRMENMET
A1E1max(0,A1B1,A2B2)max(0,A1B2,A2B3)max(0,A1B3,A2B4)max(2+A2B4,A1B4,A2B5)max(0,A1B5,A2B6)max(0,A1B6,A2B7)max(2+A2B7,A1B7,A2B8)max(0,A1B8,A2B9)
A2L01max(0,A2B2,A3B3)max(0,A2B3,A3B4)max(0,A2B4,A3B5)max(0,A2B5,A3B6)max(0,A2B6,A3B7)max(0,A2B7,A3B8)max(0,A2B8,A3B9)
A3R001max(0,A3B3,A4B4)max(0,A3B4,A4B5)max(0,A3B5,A4B6)max(0,A3B6,A4B7)max(0,A3B7,A4B8)max(0,A3B8,A4B9)
A4N0001max(0,A4B4,A5B5)max(0,A4B5,A5B6)max(2+A5B6,A4B6,A5B7)max(0,A4B7,A5B8)max(0,A4B8,A5B9)
A5E00001max(0,A5B5,A6B6)max(0,A5B6,A6B7)max(2+A6B7,A5B7,A6B8)max(0,A5B8,A6B9)
A6N000001max(0,A6B6,A7B7)max(0,A6B7,A7B8)max(0,A6B8,A7B9)
A7M0000001max(0,A7B7,A8B8)max(0,A7B8,A8B9)
A8E00000001max(0,A8B8,A9B9)
A9T000000001

We then need to find the cell which doesn’t have any dependency as our starting point as we will have our first answer there. That would be the last cell A9B9. And we slowly move our way up to the rest of the cells until we reach cell A1B9 which will give us our final answer.

B8, A6 rows and columns are our base conditions where we return 0.

type HashTable = {
  [x: number]: {
    [y: number]: number;
  };
};


function lps(s: string): number {
  const dp: HashTable = {};

  for (let col = 0; col < s.length; ++col) {
    for (let row = s.length - 1; row > -1; --row) {
      if (!dp[row]) {
        dp[row] = {};
      }

      if (row > col) {
        dp[row][col] = 0;
      } else if (row === col) {
        dp[row][col] = 1;
      } else {
        if (s[row] === s[col]) {
          dp[row][col] = Math.max(2 + dp[row + 1][col - 1], dp[row][col - 1], dp[row + 1, col]);
        } else {
          dp[row][col] = Math.max(dp[row][col - 1], dp[row + 1][col]);
        }
      }
    }
  }

  return dp[0][s.length - 1];
}

Explore more like this

© 2020 Jazz Peh, Built with Gatsby