Implement a function to find the longest common increasing subsequence (LCIS) between two sequences

problemsolving
Published

April 22, 2024

Finding the longest common subsequence (LCS) is a classic computer science problem. A variation, and often a more challenging one, involves finding the longest common increasing subsequence (LCIS). This means we’re looking for the longest subsequence that’s common to both input sequences and is strictly increasing.

Let’s look at how to implement a function in JavaScript to solve this problem efficiently. We’ll use dynamic programming, a technique well-suited for optimization problems with overlapping subproblems.

The core idea behind our dynamic programming approach is to build a table (typically a 2D array) where dp[i][j] represents the length of the LCIS ending at index i in the first sequence and index j in the second sequence.

Here’s the JavaScript code:

function longestCommonIncreasingSubsequence(arr1, arr2) {
  const m = arr1.length;
  const n = arr2.length;

  // Initialize a 2D array to store lengths of LCIS
  const dp = Array(m + 1).fill(0).map(() => Array(n + 1).fill(0));

  let maxLength = 0;
  let endIndex1 = -1; //To track the last index of LCIS in arr1


  for (let i = 1; i <= m; i++) {
    for (let j = 1; j <= n; j++) {
      if (arr1[i - 1] === arr2[j - 1]) {
        //If elements match and it's increasing
        let prevLength = 0;
        for (let k = 0; k < i -1; k++){
            if (arr1[k] < arr1[i-1]){
                prevLength = Math.max(prevLength, dp[k+1][j-1]);
            }
        }
        dp[i][j] = prevLength + 1;
        if (dp[i][j] > maxLength){
            maxLength = dp[i][j];
            endIndex1 = i-1;
        }

      }
    }
  }

    //Extract the LCIS from the DP table, knowing the maxLength and endIndex1
    if(maxLength === 0) return []; //Handle empty case

    const lcis = [];
    let currentVal = arr1[endIndex1];
    lcis.push(currentVal);
    let currentLength = maxLength;
    for (let i = endIndex1 -1; i>=0; i--){
        for (let j = 0; j <= n; j++){
            if(dp[i+1][j] === currentLength && arr1[i] < currentVal && arr1[i] === arr2[j-1]){
                lcis.unshift(arr1[i]);
                currentVal = arr1[i];
                currentLength--;
                break;
            }
        }

    }
    return lcis;
}


// Example usage:
const arr1 = [3, 4, 9, 1];
const arr2 = [5, 3, 9, 4, 9, 1];
const lcis = longestCommonIncreasingSubsequence(arr1, arr2);
console.log("Longest Common Increasing Subsequence:", lcis); // Output: [3, 9]


const arr3 = [2, 1, 5, 6];
const arr4 = [1, 2, 3, 4, 5, 6];
const lcis2 = longestCommonIncreasingSubsequence(arr3, arr4);
console.log("Longest Common Increasing Subsequence:", lcis2); // Output: [2, 5, 6]

const arr5 = [1,2,3];
const arr6 = [4,5,6];
const lcis3 = longestCommonIncreasingSubsequence(arr5, arr6);
console.log("Longest Common Increasing Subsequence:", lcis3); // Output: []

This code efficiently finds the LCIS using dynamic programming. The added complexity compared to a standard LCS algorithm comes from the need to ensure the subsequence is strictly increasing. The extraction of the actual LCIS from the dp table adds some lines for clarity and readability. The time complexity is O(mnmin(m,n)) and space complexity is O(m*n), where ‘m’ and ‘n’ are the lengths of the input arrays. Further optimizations might be possible depending on specific input characteristics, but this provides a solution for a wide range of inputs.