5 Best Ways to Find Length of Longest Palindromic Subsequence in Python

πŸ’‘ Problem Formulation: Addressing the question of how to calculate the length of the longest palindromic subsequence within a given string in Python. A palindromic subsequence is a sequence that reads the same backward as forward. For instance, given the input string 'BBABCBCAB', the longest palindromic subsequence is 'BABCBAB' or 'BBCABB', both of which have a length of 7.

Method 1: Dynamic Programming

This method utilizes dynamic programming, a technique for solving complex problems by breaking them down into simpler subproblems. It constructs a two-dimensional array that stores lengths of palindromic subsequences for all substrings, filling the array in a bottom-up manner. Function longest_palindromic_subsequence_dp calculates the length of the longest palindromic subsequence efficiently, making this method particularly suitable for strings with repeated processing.

Here’s an example:

def longest_palindromic_subsequence_dp(string):
    n = len(string)
    dp = [[0]*n for _ in range(n)]

    for i in range(n-1, -1, -1):
        dp[i][i] = 1
        for j in range(i+1, n):
            if string[i] == string[j]:
                dp[i][j] = dp[i+1][j-1] + 2
            else:
                dp[i][j] = max(dp[i+1][j], dp[i][j-1])
                
    return dp[0][n-1]

# Example usage
print(longest_palindromic_subsequence_dp('BBABCBCAB'))

Output: 7

The snippet defines the function longest_palindromic_subsequence_dp that initializes a 2D array dp and populates it with lengths of the longest palindromic subsequences for every possible substring. It iteratively improves upon previous computations until the value in dp[0][n-1], which represents the length of the longest palindromic subsequence for the entire string, is computed. The provided example outputs the length 7, which is correct for the string ‘BBABCBCAB’.

Method 2: Recursive Approach

The recursive approach method breaks down the problem into smaller subproblems recursively without storing the solutions, in contrast to dynamic programming. The function longest_palindromic_subsequence_recursive is a pure, recursive solution that works well for shorter strings due to its simplicity, but may not be efficient for longer strings due to excessive recomputation of subproblems.

Here’s an example:

def longest_palindromic_subsequence_recursive(string, start, end):
    if start > end:
        return 0
    if start == end:
        return 1
    if string[start] == string[end]:
        return 2 + longest_palindromic_subsequence_recursive(string, start + 1, end - 1)
    else:
        return max(longest_palindromic_subsequence_recursive(string, start, end - 1),
                   longest_palindromic_subsequence_recursive(string, start + 1, end))

# Example usage
print(longest_palindromic_subsequence_recursive('BBABCBCAB', 0, len('BBABCBCAB') - 1))

Output: 7

The code snippet creates a function longest_palindromic_subsequence_recursive that applies a recursive strategy to find the longest palindromic subsequence. It compares characters and recursively calculates the maximum length considering or discarding current characters. Despite its elegance, it is not optimal for large strings due to overlapping subproblems causing redundant calculations.

Method 3: Memoization Technique (Top-Down Dynamic Programming)

Memoization is an optimization technique used to speed up programs by storing the results of expensive function calls and returning the cached result when the same inputs are provided again. It is a top-down, recursive approach that avoids recalculating results. The function longest_palindromic_subsequence_memo enhances the recursive approach using a memoization table to save previously calculated lengths, providing a balance between the simplicity of the recursive method and the efficiency of dynamic programming.

Here’s an example:

def longest_palindromic_subsequence_memo(string):
    memo = {}

    def recurse(start, end):
        if start > end:
            return 0
        if start == end:
            return 1
        if (start, end) in memo:
            return memo[(start, end)]

        if string[start] == string[end]:
            memo[(start, end)] = 2 + recurse(start + 1, end - 1)
        else:
            memo[(start, end)] = max(recurse(start, end - 1), recurse(start + 1, end))

        return memo[(start, end)]

    return recurse(0, len(string) - 1)

# Example usage
print(longest_palindromic_subsequence_memo('BBABCBCAB'))

Output: 7

This code defines a closure function recurse inside longest_palindromic_subsequence_memo that applies memoization. If the result for a pair of indices is already computed, it returns the result from the memo dictionary. This eliminates redundant calculations, making the function far more efficient than the pure recursive version, particularly for larger strings.

Method 4: Using a Simplified Bottom-Up Dynamic Programming Approach

This method offers a bottom-up approach similar to the one described in Method 1 but optimizes space complexity by using a one-dimensional array. The function longest_palindromic_subsequence_bottom_up_optimized calculates the length of the longest palindromic subsequence using a sliding window technique to update values progressively, rather than storing the entire matrix. This method reduces space overhead while still achieving desired runtime efficiency.

Here’s an example:

def longest_palindromic_subsequence_bottom_up_optimized(string):
    n = len(string)
    dp = [0] * n
    for i in range(n-1, -1, -1):
        dp[i] = 1
        previous = 0
        for j in range(i+1, n):
            temp = dp[j]
            if string[i] == string[j]:
                dp[j] = previous + 2
            else:
                dp[j] = max(dp[j], dp[j-1])
            previous = temp
    return dp[n-1]

# Example usage
print(longest_palindromic_subsequence_bottom_up_optimized('BBABCBCAB'))

Output: 7

The function longest_palindromic_subsequence_bottom_up_optimized showcases an optimized bottom-up dynamic programming solution. It uses a single-dimensional array, dp, and a temporary variable, previous, to store intermediate results. This significantly reduces the memory footprint while maintaining the same runtime complexity as the traditional two-dimensional approach.

Bonus One-Liner Method 5: Recursive With LRU Cache

In Python, the functools.lru_cache decorator provides a quick way to cache function results. When applied to a traditional recursive function, this one-liner hack converts it into a memoized version. It’s a concise and Pythonic way to optimize a naive recursive method. The function longest_palindromic_subsequence_lru is a one-liner that exhibits the power of Python’s built-in decorators to greatly improve the performance of recursive algorithms.

Here’s an example:

from functools import lru_cache

@lru_cache
def longest_palindromic_subsequence_lru(string, start, end):
    if start > end:
        return 0
    if start == end:
        return 1
    if string[start] == string[end]:
        return 2 + longest_palindromic_subsequence_lru(string, start + 1, end - 1)
    return max(longest_palindromic_subsequence_lru(string, start, end - 1),
               longest_palindromic_subsequence_lru(string, start + 1, end))

# Example usage
print(longest_palindromic_subsequence_lru('BBABCBCAB', 0, len('BBABCBCAB') - 1))

Output: 7

The code enrols the function longest_palindromic_subsequence_lru in Python’s least recently used (LRU) cache mechanism, by simply prefixing it with the @lru_cache decorator. This allows the function to store and reuse previously computed results, which drastically reduces the number of calculations needed and thus makes the recursive approach feasible even for larger input strings.

Summary/Discussion

  • Method 1: Dynamic Programming. Offers optimal efficiency for repeated processing; however, it has high space complexity with O(n^2) memory usage.
  • Method 2: Recursive Approach. Simple and elegant, but highly inefficient for longer strings due to a large number of redundant computations.
  • Method 3: Memoization (Top-Down DP). Strikes a balance between computational efficiency and the intuitiveness of recursion; still uses O(n^2) space but is more efficient than pure recursion.
  • Method 4: Simplified Bottom-Up DP. Efficient in both time and space, minimizing memory usage while retaining speed, making it practical for memory-constrained environments.
  • Method 5: Recursive with LRU Cache. Extremely easy to implement and gains significant efficiency with the @lru_cache decorator, but caching still entails additional memory overhead.